Browse Source

Re-construct the JS Timeline page

Gaël Métais 10 years ago
parent
commit
cb20075599

+ 0 - 1
bower.json

@@ -2,7 +2,6 @@
   "name": "yellowlabtools",
   "dependencies": {
     "angular": "~1.3.5",
-    "ngModal": "git://github.com/gmetais/ngModal.git#1.2.3",
     "angular-route": "~1.3.6",
     "angular-resource": "~1.3.6"
   }

+ 0 - 260
front/src/css/dashboard.css

@@ -1,4 +1,3 @@
-/* Timeline colors, related to Window Performances */
 .resultsMenu {
   margin-top: 2em;
 }
@@ -174,265 +173,6 @@ h4 {
 .notations .criteria .icon-question {
   color: transparent;
 }
-.timeline {
-  margin: 2em 0 5em;
-}
-.timeline .chart {
-  position: relative;
-  width: 100%;
-  border-bottom: 1px solid #000;
-}
-.timeline .startTime,
-.timeline .endTime {
-  position: absolute;
-  bottom: 0.5em;
-  font-size: 0.8em;
-}
-.timeline .startTime {
-  left: 0em;
-}
-.timeline .endTime {
-  right: 0em;
-}
-.timeline .chartPoints {
-  display: table;
-  height: 100px;
-  width: 99%;
-  margin: 0 auto;
-}
-.timeline .interval {
-  display: table-cell;
-  position: relative;
-  height: 100px;
-  width: 0.5%;
-}
-.timeline .interval .color {
-  position: absolute;
-  bottom: 0;
-  width: 100%;
-}
-.timeline div.interval:hover {
-  background: #9C4274;
-}
-.timeline .interval:hover .color {
-  background: #F04DA7;
-}
-.timeline .domComplete.interval {
-  background: #ede3ff;
-}
-.timeline .domComplete .color {
-  background: #c2a3ff;
-}
-.timeline .domContentLoadedEnd.interval {
-  background: #d8f0f0;
-}
-.timeline .domContentLoadedEnd .color {
-  background: #7ecccc;
-}
-.timeline .domContentLoaded.interval {
-  background: #e0ffd1;
-}
-.timeline .domContentLoaded .color {
-  background: #a7e846;
-}
-.timeline .domInteractive.interval {
-  background: #fffccc;
-}
-.timeline .domInteractive .color {
-  background: #ffe433;
-}
-.timeline .domCreation.interval {
-  background: #ffe0cc;
-}
-.timeline .domCreation .color {
-  background: #ff6600;
-}
-.timeline .tooltip.detailsOverlay {
-  position: absolute;
-  display: none;
-  width: auto;
-  padding: 0.5em 1em;
-  top: -1.5em;
-  right: 1em;
-}
-.timeline .interval:hover .tooltip {
-  display: block;
-}
-.timeline .legend {
-  display: table;
-  width: 100%;
-  margin-top: 1em;
-}
-.timeline .legend > div {
-  display: table-row;
-}
-.timeline .legend > div > div {
-  position: relative;
-  display: table-cell;
-  margin-top: 1em;
-}
-.timeline .titles {
-  font-weight: bold;
-}
-.timeline .titles > div {
-  padding: 0 1em 0 2em;
-}
-.timeline .tips {
-  font-size: 0.7em;
-}
-.timeline .tips > div {
-  padding: 1em 1em 0 0;
-}
-.timeline .legend .color {
-  display: block;
-  position: absolute;
-  left: 0;
-  height: 1.5em;
-  width: 1.5em;
-  border-radius: 0.2em;
-}
-.metrics h4 {
-  padding-left: 2em;
-}
-.metrics .module {
-  padding-left: 4em;
-  padding-top: 0.5em;
-}
-.metrics .legend {
-  font-style: italic;
-  color: #aaa;
-}
-.metrics .offenders {
-  padding-left: 0em;
-  font-size: 0.8em;
-}
-.metrics .offenders div {
-  cursor: pointer;
-}
-.metrics .offenders ul {
-  margin-top: 0.5em;
-}
-.filters {
-  margin: 1em 0;
-  padding: 0.5em;
-  border: 1px dotted #aaa;
-}
-.slowRequestsLimit {
-  width: 3em;
-  font-size: 1em;
-  text-align: right;
-  border: 1px solid #aaa;
-}
-input.textFilter {
-  box-shadow: none;
-  font-size: 1em;
-  padding: 0 0.2em;
-  border: 1px solid #aaa;
-  border-radius: none;
-  width: 15em;
-}
-.table {
-  display: table;
-  width: 100%;
-  border-spacing: 0.25em;
-}
-.table > div {
-  display: table-row;
-}
-.table > .headers > div {
-  font-weight: bold;
-  padding: 0.5em 1em;
-}
-.table > div > div {
-  padding: 0.1em 1em;
-  background: #f2f2f2;
-  display: table-cell;
-  text-align: left;
-}
-.table > div.jsError > .type,
-.table > div.jsError > .value {
-  color: #e74c3c;
-  font-weight: bold;
-}
-.table > div.windowPerformance > div,
-.table > div.windowPerformance > div.startTime {
-  background: #EBD8E2;
-}
-.table > div.showingDetails > div {
-  background: #f1c40f;
-}
-.table > div > .index {
-  color: #bbb;
-}
-.table > div > .type {
-  white-space: nowrap;
-}
-.table > div > .value {
-  width: 70%;
-  word-break: break-all;
-}
-.table > div > .details {
-  position: relative;
-}
-.table .details .icon-question {
-  color: #f1c40f;
-  cursor: pointer;
-}
-.table .icon-warning {
-  display: inline-block;
-  width: 0.8em;
-}
-.detailsOverlay {
-  position: absolute;
-  right: 3em;
-  top: -3em;
-  width: 45em;
-  min-height: 1em;
-  padding: 0 1em 1em;
-  background: #fff;
-  border: 2px solid #f1c40f;
-  border-radius: 0.5em;
-  z-index: 1;
-}
-@media screen and (max-width: 1024px) {
-  .detailsOverlay {
-    width: 25em;
-  }
-}
-.detailsOverlay .closeBtn {
-  position: absolute;
-  top: 0.5em;
-  right: 0.5em;
-  color: #f1c40f;
-  cursor: pointer;
-}
-.detailsOverlay .advice {
-  color: #e74c3c;
-  font-weight: bold;
-}
-.detailsOverlay .trace {
-  word-break: break-all;
-}
-.table > div > .duration,
-.table > div > .startTime {
-  text-align: center;
-  white-space: nowrap;
-}
-.table > div > .startTime.domComplete {
-  background: #ede3ff;
-}
-.table > div > .startTime.domContentLoadedEnd {
-  background: #d8f0f0;
-}
-.table > div > .startTime.domContentLoaded {
-  background: #e0ffd1;
-}
-.table > div > .startTime.domInteractive {
-  background: #fffccc;
-}
-.table > div > .startTime.domCreation {
-  background: #ffe0cc;
-}
 /**** NgModal popin (have a look inside bower_components) ****/
 .ng-modal {
   position: fixed;

+ 0 - 0
front/src/css/rule.css


+ 243 - 0
front/src/css/timeline.css

@@ -0,0 +1,243 @@
+/* Timeline colors, related to Window Performances */
+.timeline {
+  margin: 2em 0 5em;
+}
+.timeline .chart {
+  position: relative;
+  width: 100%;
+  border-bottom: 1px solid #000;
+}
+.timeline .startTime,
+.timeline .endTime {
+  position: absolute;
+  bottom: 0.5em;
+  font-size: 0.8em;
+}
+.timeline .startTime {
+  left: 0em;
+}
+.timeline .endTime {
+  right: 0em;
+}
+.timeline .chartPoints {
+  display: table;
+  height: 100px;
+  width: 99%;
+  margin: 0 auto;
+}
+.timeline .interval {
+  display: table-cell;
+  position: relative;
+  height: 100px;
+  width: 0.5%;
+}
+.timeline .interval .color {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+}
+.timeline div.interval:hover {
+  background: #9C4274;
+}
+.timeline .interval:hover .color {
+  background: #F04DA7;
+}
+.timeline .domComplete.interval {
+  background: #ede3ff;
+}
+.timeline .domComplete .color {
+  background: #c2a3ff;
+}
+.timeline .domContentLoadedEnd.interval {
+  background: #d8f0f0;
+}
+.timeline .domContentLoadedEnd .color {
+  background: #7ecccc;
+}
+.timeline .domContentLoaded.interval {
+  background: #e0ffd1;
+}
+.timeline .domContentLoaded .color {
+  background: #a7e846;
+}
+.timeline .domInteractive.interval {
+  background: #fffccc;
+}
+.timeline .domInteractive .color {
+  background: #ffe433;
+}
+.timeline .domCreation.interval {
+  background: #ffe0cc;
+}
+.timeline .domCreation .color {
+  background: #ff6600;
+}
+.timeline .tooltip.detailsOverlay {
+  position: absolute;
+  display: none;
+  width: auto;
+  padding: 0.5em 1em;
+  top: -1.5em;
+  right: 1em;
+}
+.timeline .interval:hover .tooltip {
+  display: block;
+}
+.timeline .legend {
+  display: table;
+  width: 100%;
+  margin-top: 1em;
+}
+.timeline .legend > div {
+  display: table-row;
+}
+.timeline .legend > div > div {
+  position: relative;
+  display: table-cell;
+  margin-top: 1em;
+}
+.timeline .titles {
+  font-weight: bold;
+}
+.timeline .titles > div {
+  padding: 0 1em 0 2em;
+}
+.timeline .tips {
+  font-size: 0.7em;
+}
+.timeline .tips > div {
+  padding: 1em 1em 0 0;
+}
+.timeline .legend .color {
+  display: block;
+  position: absolute;
+  left: 0;
+  height: 1.5em;
+  width: 1.5em;
+  border-radius: 0.2em;
+}
+.filters {
+  margin: 1em 0;
+  padding: 0.5em;
+  border: 1px dotted #aaa;
+}
+.slowRequestsLimit {
+  width: 3em;
+  font-size: 1em;
+  text-align: right;
+  border: 1px solid #aaa;
+}
+input.textFilter {
+  box-shadow: none;
+  font-size: 1em;
+  padding: 0 0.2em;
+  border: 1px solid #aaa;
+  border-radius: none;
+  width: 15em;
+}
+.table {
+  display: table;
+  width: 100%;
+  border-spacing: 0.25em;
+}
+.table > div {
+  display: table-row;
+}
+.table > .headers > div {
+  font-weight: bold;
+  padding: 0.5em 1em;
+}
+.table > div > div {
+  padding: 0.1em 1em;
+  background: #f2f2f2;
+  display: table-cell;
+  text-align: left;
+}
+.table > div.jsError > .type,
+.table > div.jsError > .value {
+  color: #e74c3c;
+  font-weight: bold;
+}
+.table > div.windowPerformance > div,
+.table > div.windowPerformance > div.startTime {
+  background: #EBD8E2;
+}
+.table > div.showingDetails > div {
+  background: #f1c40f;
+}
+.table > div > .index {
+  color: #bbb;
+}
+.table > div > .type {
+  white-space: nowrap;
+}
+.table > div > .value {
+  width: 70%;
+  word-break: break-all;
+}
+.table > div > .details {
+  position: relative;
+}
+.table .details .icon-question {
+  color: #f1c40f;
+  cursor: pointer;
+}
+.table .icon-warning {
+  display: inline-block;
+  width: 0.8em;
+}
+.detailsOverlay {
+  position: absolute;
+  right: 3em;
+  top: -3em;
+  width: 45em;
+  min-height: 1em;
+  padding: 0 1em 1em;
+  background: #fff;
+  border: 2px solid #f1c40f;
+  border-radius: 0.5em;
+  z-index: 1;
+}
+@media screen and (max-width: 1024px) {
+  .detailsOverlay {
+    width: 25em;
+  }
+}
+.detailsOverlay .closeBtn {
+  position: absolute;
+  top: 0.5em;
+  right: 0.5em;
+  color: #f1c40f;
+  cursor: pointer;
+}
+.detailsOverlay .advice {
+  color: #e74c3c;
+  font-weight: bold;
+}
+.detailsOverlay .trace {
+  word-break: break-all;
+}
+.table > div > .duration,
+.table > div > .startTime {
+  text-align: center;
+  white-space: nowrap;
+}
+.table > div > .startTime.domComplete {
+  background: #ede3ff;
+}
+.table > div > .startTime.domContentLoadedEnd {
+  background: #d8f0f0;
+}
+.table > div > .startTime.domContentLoaded {
+  background: #e0ffd1;
+}
+.table > div > .startTime.domInteractive {
+  background: #fffccc;
+}
+.table > div > .startTime.domCreation {
+  background: #ffe0cc;
+}
+.execution .icon-warning {
+  color: #e74c3c;
+  cursor: pointer;
+}

+ 6 - 0
front/src/js/app.js

@@ -4,12 +4,18 @@ var yltApp = angular.module('YellowLabTools', [
     'aboutCtrl',
     'dashboardCtrl',
     'queueCtrl',
+    'ruleCtrl',
+    'timelineCtrl',
     'runsFactory',
     'resultsFactory',
     'menuService',
     'gradeDirective',
 ]);
 
+yltApp.run(function($rootScope) {
+    $rootScope.loadedRunId = null;
+});
+
 yltApp.config(['$routeProvider', '$locationProvider',
     function($routeProvider, $locationProvider) {
         $routeProvider.

+ 14 - 5
front/src/js/controllers/dashboardCtrl.js

@@ -1,15 +1,24 @@
 var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
 
-dashboardCtrl.controller('DashboardCtrl', ['$scope', '$routeParams', '$location', 'Results', 'Menu', function($scope, $routeParams, $location, Results, Menu) {
+dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, Menu) {
     $scope.runId = $routeParams.runId;
     $scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
     
-    Results.get({runId: $routeParams.runId}, function(result) {
-        $scope.result = result;
-        console.log(result);
-    });
+    function loadResults() {
+        // Load result if needed
+        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
+            Results.get({runId: $routeParams.runId}, function(result) {
+                $rootScope.loadedResult = result;
+                $scope.result = result;
+            });
+        } else {
+            $scope.result = $rootScope.loadedResult;
+        }
+    }
 
     $scope.showRulePage = function(ruleName) {
         $location.path('/result/' + $scope.runId + '/rule/' + ruleName);
     };
+
+    loadResults();
 }]);

+ 5 - 0
front/src/js/controllers/ruleCtrl.js

@@ -0,0 +1,5 @@
+var ruleCtrl = angular.module('ruleCtrl', []);
+
+ruleCtrl.controller('RuleCtrl', ['$scope', function($scope) {
+    
+}]);

+ 132 - 0
front/src/js/controllers/timelineCtrl.js

@@ -0,0 +1,132 @@
+var timelineCtrl = angular.module('timelineCtrl', []);
+
+timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$timeout', 'Menu', 'Results', function($scope, $rootScope, $routeParams, $timeout, Menu, Results) {
+    $scope.runId = $routeParams.runId;
+    $scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
+
+    function loadResults() {
+        // Load result if needed
+        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
+            Results.get({runId: $routeParams.runId}, function(result) {
+                $rootScope.loadedResult = result;
+                $scope.result = result;
+                render();
+            });
+        } else {
+            $scope.result = $rootScope.loadedResult;
+            render();
+        }
+    }
+
+    function render() {
+        initExecutionTree();
+        initTimeline();
+        $timeout(initProfiler, 100);
+    }
+
+    function initExecutionTree() {
+        var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
+        $scope.executionTree = [];
+
+        originalExecutions.forEach(function(node) {
+
+            // Prepare a faster angular search by creating a kind of search index
+            node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
+
+            $scope.executionTree.push(node);
+        });
+    }
+
+    function initTimeline() {
+
+        // Split the timeline into 200 intervals
+        var numberOfIntervals = 199;
+        var lastEvent = $scope.executionTree[$scope.executionTree.length - 1];
+        $scope.endTime =  lastEvent.data.timestamp + (lastEvent.data.time || 0);
+        $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
+        
+        // Pre-fill array of as many elements as there are milleseconds
+        var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
+        
+        // Create the milliseconds array from the execution tree
+        $scope.executionTree.forEach(function(node) {
+            if (node.data.time !== undefined) {
+
+                // Ignore artefacts (durations > 100ms)
+                var time = Math.min(node.data.time, 100) || 1;
+
+                for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
+                    millisecondsArray[i] |= 1;
+                }
+            }
+        });
+
+        // Pre-fill array of 200 elements
+        $scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
+
+        // Create the timeline from the milliseconds array
+        millisecondsArray.forEach(function(value, timestamp) {
+            if (value === 1) {
+                $scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
+            }
+        });
+        
+        // Get the maximum value of the array (needed for display)
+        $scope.timelineMax = Math.max.apply(Math, $scope.timeline);
+    }
+
+
+    function initProfiler() {
+        $scope.profilerData = $scope.executionTree;
+    }
+
+
+    function parseBacktrace(str) {
+        if (!str) {
+            return null;
+        }
+
+        var out = [];
+        var splited = str.split(' / ');
+        splited.forEach(function(trace) {
+            var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
+            if (result && result[2].length > 0) {
+                var filePath = result[2];
+                var chunks = filePath.split('/');
+                var fileName = chunks[chunks.length - 1];
+
+                out.push({
+                    fnName: result[1],
+                    fileName: fileName,
+                    filePath: filePath,
+                    line: result[3]
+                });
+            }
+        });
+        return out;
+    }
+
+    $scope.filter = function(textFilter, scriptName) {
+
+    };
+
+    $scope.onNodeDetailsClick = function(node) {
+        var isOpen = node.showDetails;
+        if (!isOpen) {
+            // Close all other nodes
+            $scope.executionTree.forEach(function(currentNode) {
+                currentNode.showDetails = false;
+            });
+
+            // Parse the backtrace
+            if (!node.parsedBacktrace) {
+                node.parsedBacktrace = parseBacktrace(node.data.backtrace);
+            }
+
+        }
+        node.showDetails = !isOpen;
+    };
+
+    loadResults();
+
+}]);

+ 1 - 1
front/src/js/models/resultsFactory.js

@@ -2,6 +2,6 @@ var resultsFactory = angular.module('resultsFactory', ['ngResource']);
 
 resultsFactory.factory('Results', ['$resource', function($resource) {
     return $resource('/api/results/:runId', {
-        'get': {method: 'GET', params: {runId: '@runId'}}
+        
     });
 }]);

+ 0 - 291
front/src/less/dashboard.less

@@ -1,17 +1,4 @@
 
-/* Timeline colors, related to Window Performances */
-@domCreationColor: #FF6600;
-@domCreationBg: #FFE0CC;
-@domContentLoadedColor: #A7E846;
-@domContentLoadedBg: #E0FFD1;
-@domContentLoadedEndColor: #7ECCCC;
-@domContentLoadedEndBg: #D8F0F0;
-@domCompleteColor: #C2A3FF;
-@domCompleteBg: #EDE3FF;
-@domInteractiveColor: #FFE433;
-@domInteractiveBg: #FFFCCC;
-
-
 .resultsMenu {
     margin-top: 2em;
 }
@@ -181,284 +168,6 @@ h4 {
 }
 
 
-.timeline {
-    margin: 2em 0 5em;
-}
-.timeline .chart {
-    position: relative;
-    width: 100%;
-    border-bottom: 1px solid #000;
-}
-.timeline .startTime, .timeline .endTime {
-    position: absolute;
-    bottom: 0.5em;
-    font-size: 0.8em;
-}
-.timeline .startTime {
-    left: 0em;
-}
-.timeline .endTime {
-    right: 0em;
-}
-.timeline .chartPoints {
-    display: table;
-    height: 100px;
-    width: 99%;
-    margin: 0 auto;
-}
-.timeline .interval {
-    display: table-cell;
-    position: relative;
-    height: 100px;
-    width: 0.5%;
-}
-.timeline .interval .color {
-    position: absolute;
-    bottom: 0;
-    width: 100%;
-}
-.timeline div.interval:hover {
-    background: #9C4274;
-}
-.timeline .interval:hover .color {
-    background: #F04DA7;
-}
-.timeline .domComplete.interval {
-    background: @domCompleteBg;
-}
-.timeline .domComplete .color {
-    background: @domCompleteColor;
-}
-.timeline .domContentLoadedEnd.interval {
-    background: @domContentLoadedEndBg;
-}
-.timeline .domContentLoadedEnd .color {
-    background: @domContentLoadedEndColor;
-}
-.timeline .domContentLoaded.interval {
-    background: @domContentLoadedBg;
-}
-.timeline .domContentLoaded .color {
-    background: @domContentLoadedColor;
-}
-.timeline .domInteractive.interval {
-    background: @domInteractiveBg;
-}
-.timeline .domInteractive .color {
-    background: @domInteractiveColor;
-}
-.timeline .domCreation.interval {
-    background: @domCreationBg;
-}
-.timeline .domCreation .color {
-    background: @domCreationColor;
-}
-.timeline .tooltip.detailsOverlay {
-    position: absolute;
-    display: none;
-    width: auto;
-    padding: 0.5em 1em;
-    top: -1.5em;
-    right: 1em;
-}
-.timeline .interval:hover .tooltip {
-    display: block;
-}
-
-.timeline .legend {
-    display: table;
-    width: 100%;
-    margin-top: 1em;
-}
-.timeline .legend > div {
-    display: table-row;
-}
-.timeline .legend > div > div {
-    position: relative;
-    display: table-cell;
-    margin-top: 1em;
-}
-.timeline .titles {
-    font-weight: bold;
-}
-.timeline .titles > div {
-    padding: 0 1em 0 2em;
-}
-.timeline .tips {
-    font-size: 0.7em;
-}
-.timeline .tips > div {
-    padding: 1em 1em 0 0;
-}
-.timeline .legend .color {
-    display: block;
-    position: absolute;
-    left: 0;
-    height: 1.5em;
-    width: 1.5em;
-    border-radius: 0.2em;
-}
-
-
-.metrics h4 {
-    padding-left: 2em;
-}
-
-.metrics .module {
-    padding-left: 4em;
-    padding-top: 0.5em;
-}
-
-.metrics .legend {
-    font-style: italic;
-    color: #aaa;
-}
-
-.metrics .offenders {
-    padding-left: 0em;
-    font-size: 0.8em;
-}
-
-.metrics .offenders div {
-    cursor: pointer;
-}
-
-.metrics .offenders ul {
-    margin-top: 0.5em;
-}
-
-.filters {
-    margin: 1em 0;
-    padding: 0.5em;
-    border: 1px dotted #aaa;
-}
-
-.slowRequestsLimit {
-    width: 3em;
-    font-size: 1em;
-    text-align: right;
-    border: 1px solid #aaa;
-}
-
-input.textFilter {
-    box-shadow: none;
-    font-size: 1em;
-    padding: 0 0.2em;
-    border: 1px solid #aaa;
-    border-radius: none;
-    width: 15em;
-}
-
-.table {
-    display: table;
-    width: 100%;
-    border-spacing: 0.25em;
-}
-
-.table > div {
-    display: table-row;
-}
-
-.table > .headers > div {
-    font-weight: bold;
-    padding: 0.5em 1em;
-}
-
-.table > div > div {
-    padding: 0.1em 1em;
-    background: #f2f2f2;
-    display: table-cell;
-    text-align: left;
-}
-.table > div.jsError > .type, .table > div.jsError > .value {
-    color: #e74c3c;
-    font-weight: bold;
-}
-.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime {
-    background: #EBD8E2;
-}
-.table > div.showingDetails > div {
-    background: #f1c40f;
-}
-
-.table > div > .index {
-    color: #bbb;
-}
-
-.table > div > .type {
-    white-space:nowrap;
-}
-
-.table > div > .value {
-    width: 70%;
-    word-break: break-all;
-}
-
-.table > div > .details {
-    position: relative;
-}
-.table .details .icon-question {
-    color: #f1c40f;
-    cursor: pointer;
-}
-.table .icon-warning {
-    display: inline-block;
-    width: 0.8em;
-}
-
-.detailsOverlay {
-    position: absolute;
-    right: 3em;
-    top: -3em;
-    width: 45em;
-    min-height: 1em;
-    padding: 0 1em 1em;
-    background: #fff;
-    border: 2px solid #f1c40f;
-    border-radius: 0.5em;
-    z-index: 1;
-}
-@media screen and (max-width: 1024px) {
-    .detailsOverlay {
-        width: 25em;
-    }
-}
-.detailsOverlay .closeBtn {
-    position: absolute;
-    top: 0.5em;
-    right: 0.5em;
-    color: #f1c40f;
-    cursor: pointer;
-}
-.detailsOverlay .advice {
-    color: #e74c3c;
-    font-weight: bold;
-}
-.detailsOverlay .trace {
-    word-break: break-all;
-}
-
-.table > div > .duration, .table > div > .startTime {
-    text-align: center;
-    white-space:nowrap;
-}
-.table > div > .startTime.domComplete {
-    background: @domCompleteBg;
-}
-.table > div > .startTime.domContentLoadedEnd {
-    background: @domContentLoadedEndBg;
-}
-.table > div > .startTime.domContentLoaded {
-    background: @domContentLoadedBg;
-}
-.table > div > .startTime.domInteractive {
-    background: @domInteractiveBg;
-}
-.table > div > .startTime.domCreation {
-    background: @domCreationBg;
-}
-
-
 /**** NgModal popin (have a look inside bower_components) ****/
 .ng-modal {
     position: fixed;

+ 0 - 0
front/src/less/rule.less


+ 265 - 0
front/src/less/timeline.less

@@ -0,0 +1,265 @@
+/* Timeline colors, related to Window Performances */
+@domCreationColor: #FF6600;
+@domCreationBg: #FFE0CC;
+@domContentLoadedColor: #A7E846;
+@domContentLoadedBg: #E0FFD1;
+@domContentLoadedEndColor: #7ECCCC;
+@domContentLoadedEndBg: #D8F0F0;
+@domCompleteColor: #C2A3FF;
+@domCompleteBg: #EDE3FF;
+@domInteractiveColor: #FFE433;
+@domInteractiveBg: #FFFCCC;
+
+
+.timeline {
+    margin: 2em 0 5em;
+}
+.timeline .chart {
+    position: relative;
+    width: 100%;
+    border-bottom: 1px solid #000;
+}
+.timeline .startTime, .timeline .endTime {
+    position: absolute;
+    bottom: 0.5em;
+    font-size: 0.8em;
+}
+.timeline .startTime {
+    left: 0em;
+}
+.timeline .endTime {
+    right: 0em;
+}
+.timeline .chartPoints {
+    display: table;
+    height: 100px;
+    width: 99%;
+    margin: 0 auto;
+}
+.timeline .interval {
+    display: table-cell;
+    position: relative;
+    height: 100px;
+    width: 0.5%;
+}
+.timeline .interval .color {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+}
+.timeline div.interval:hover {
+    background: #9C4274;
+}
+.timeline .interval:hover .color {
+    background: #F04DA7;
+}
+.timeline .domComplete.interval {
+    background: @domCompleteBg;
+}
+.timeline .domComplete .color {
+    background: @domCompleteColor;
+}
+.timeline .domContentLoadedEnd.interval {
+    background: @domContentLoadedEndBg;
+}
+.timeline .domContentLoadedEnd .color {
+    background: @domContentLoadedEndColor;
+}
+.timeline .domContentLoaded.interval {
+    background: @domContentLoadedBg;
+}
+.timeline .domContentLoaded .color {
+    background: @domContentLoadedColor;
+}
+.timeline .domInteractive.interval {
+    background: @domInteractiveBg;
+}
+.timeline .domInteractive .color {
+    background: @domInteractiveColor;
+}
+.timeline .domCreation.interval {
+    background: @domCreationBg;
+}
+.timeline .domCreation .color {
+    background: @domCreationColor;
+}
+.timeline .tooltip.detailsOverlay {
+    position: absolute;
+    display: none;
+    width: auto;
+    padding: 0.5em 1em;
+    top: -1.5em;
+    right: 1em;
+}
+.timeline .interval:hover .tooltip {
+    display: block;
+}
+
+.timeline .legend {
+    display: table;
+    width: 100%;
+    margin-top: 1em;
+}
+.timeline .legend > div {
+    display: table-row;
+}
+.timeline .legend > div > div {
+    position: relative;
+    display: table-cell;
+    margin-top: 1em;
+}
+.timeline .titles {
+    font-weight: bold;
+}
+.timeline .titles > div {
+    padding: 0 1em 0 2em;
+}
+.timeline .tips {
+    font-size: 0.7em;
+}
+.timeline .tips > div {
+    padding: 1em 1em 0 0;
+}
+.timeline .legend .color {
+    display: block;
+    position: absolute;
+    left: 0;
+    height: 1.5em;
+    width: 1.5em;
+    border-radius: 0.2em;
+}
+
+.filters {
+    margin: 1em 0;
+    padding: 0.5em;
+    border: 1px dotted #aaa;
+}
+
+.slowRequestsLimit {
+    width: 3em;
+    font-size: 1em;
+    text-align: right;
+    border: 1px solid #aaa;
+}
+
+input.textFilter {
+    box-shadow: none;
+    font-size: 1em;
+    padding: 0 0.2em;
+    border: 1px solid #aaa;
+    border-radius: none;
+    width: 15em;
+}
+
+.table {
+    display: table;
+    width: 100%;
+    border-spacing: 0.25em;
+}
+
+.table > div {
+    display: table-row;
+}
+
+.table > .headers > div {
+    font-weight: bold;
+    padding: 0.5em 1em;
+}
+
+.table > div > div {
+    padding: 0.1em 1em;
+    background: #f2f2f2;
+    display: table-cell;
+    text-align: left;
+}
+.table > div.jsError > .type, .table > div.jsError > .value {
+    color: #e74c3c;
+    font-weight: bold;
+}
+.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime {
+    background: #EBD8E2;
+}
+.table > div.showingDetails > div {
+    background: #f1c40f;
+}
+
+.table > div > .index {
+    color: #bbb;
+}
+
+.table > div > .type {
+    white-space:nowrap;
+}
+
+.table > div > .value {
+    width: 70%;
+    word-break: break-all;
+}
+
+.table > div > .details {
+    position: relative;
+}
+.table .details .icon-question {
+    color: #f1c40f;
+    cursor: pointer;
+}
+.table .icon-warning {
+    display: inline-block;
+    width: 0.8em;
+}
+
+.detailsOverlay {
+    position: absolute;
+    right: 3em;
+    top: -3em;
+    width: 45em;
+    min-height: 1em;
+    padding: 0 1em 1em;
+    background: #fff;
+    border: 2px solid #f1c40f;
+    border-radius: 0.5em;
+    z-index: 1;
+}
+@media screen and (max-width: 1024px) {
+    .detailsOverlay {
+        width: 25em;
+    }
+}
+.detailsOverlay .closeBtn {
+    position: absolute;
+    top: 0.5em;
+    right: 0.5em;
+    color: #f1c40f;
+    cursor: pointer;
+}
+.detailsOverlay .advice {
+    color: #e74c3c;
+    font-weight: bold;
+}
+.detailsOverlay .trace {
+    word-break: break-all;
+}
+
+.table > div > .duration, .table > div > .startTime {
+    text-align: center;
+    white-space:nowrap;
+}
+.table > div > .startTime.domComplete {
+    background: @domCompleteBg;
+}
+.table > div > .startTime.domContentLoadedEnd {
+    background: @domContentLoadedEndBg;
+}
+.table > div > .startTime.domContentLoaded {
+    background: @domContentLoadedBg;
+}
+.table > div > .startTime.domInteractive {
+    background: @domInteractiveBg;
+}
+.table > div > .startTime.domCreation {
+    background: @domCreationBg;
+}
+.execution .icon-warning {
+    color: #e74c3c;
+    cursor: pointer;
+}

+ 4 - 0
front/src/main.html

@@ -9,6 +9,8 @@
     <link rel="stylesheet" type="text/css" href="/front/css/index.css">
     <link rel="stylesheet" type="text/css" href="/front/css/dashboard.css">
     <link rel="stylesheet" type="text/css" href="/front/css/queue.css">
+    <link rel="stylesheet" type="text/css" href="/front/css/rule.css">
+    <link rel="stylesheet" type="text/css" href="/front/css/timeline.css">
 
     <script src="/bower_components/angular/angular.min.js"></script>
     <script src="/bower_components/angular-route/angular-route.min.js"></script>
@@ -18,6 +20,8 @@
     <script src="/front/js/controllers/aboutCtrl.js"></script>
     <script src="/front/js/controllers/dashboardCtrl.js"></script>
     <script src="/front/js/controllers/queueCtrl.js"></script>
+    <script src="/front/js/controllers/ruleCtrl.js"></script>
+    <script src="/front/js/controllers/timelineCtrl.js"></script>
     <script src="/front/js/models/resultsFactory.js"></script>
     <script src="/front/js/models/runsFactory.js"></script>
     <script src="/front/js/services/menuService.js"></script>

+ 0 - 0
front/src/views/rule.html


+ 147 - 0
front/src/views/timeline.html

@@ -0,0 +1,147 @@
+<div ng-include="'/front/views/resultSubHeader.html'"></div>
+<div class="execution">
+    <h2>Javascript Timeline</h2>
+    <p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p>
+
+    <div class="timeline">
+        <div class="chart">
+            <div class="chartPoints">
+                <div ng-repeat="duration in timeline track by $index"
+                     class="interval"
+                     ng-class="{
+                                domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
+                                domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
+                                                && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
+                                domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
+                                                && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
+                                domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
+                                                && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
+                                domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
+                     }">
+                    <div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
+                    <div class="tooltip detailsOverlay">
+                        <div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
+                    </div>
+                </div>
+            </div>
+            <div class="startTime">0 ms</div>
+            <div class="endTime">{{endTime | number: 0}} ms</div>
+        </div>
+        <div class="legend">
+            <div class="titles">
+                <div class="domCreation"><div class="color"></div>DOM creation</div>
+                <div class="domInteractive"><div class="color"></div>DOM interactive</div>
+                <div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
+                <div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
+                <div class="domComplete"><div class="color"></div>Page is complete</div>
+            </div>
+            <div class="tips">
+                <div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
+                <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
+                <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
+                <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
+                <div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
+            </div>
+        </div>
+    </div>
+
+    <h2>Javascript Profiler</h2>
+    <p>
+        The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
+    </p>
+    <div class="filters">
+        <div>
+            <input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
+            <label for="warningsFilterOn">Show warnings only</label>
+        </div>
+        <div>
+            <input type="checkbox" ng-model="textFilterOn" />
+            Filter by
+            <input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
+        </div>
+    </div>
+    <div class="table">
+
+        <toto data-info="mavariable"></toto>
+        <div class="headers">
+            <div><!-- index --></div>
+            <div>Type</div>
+            <div>Params</div>
+            <div><!-- details --></div>
+            <div>Timestamp</div>
+        </div>
+        <div ng-repeat="node in profilerData"
+             ng-if="(!warningsFilterOn || node.warning || node.error)
+                     && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
+             ng-class="{
+                            showingDetails: node.showDetails,
+                            jsError: node.error,
+                            windowPerformance: node.windowPerformance
+                        }">
+
+            <div class="index">{{$index + 1}}</div>
+            <div class="type">{{node.data.type}}</div>
+
+            <div class="value">
+                {{node.data.callDetails.arguments[0]}}
+                <span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
+                <span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
+                <span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
+            </div>
+            
+            <div class="details">
+                <div ng-class="{
+                            'icon-question': !node.warning && !node.error,
+                            'icon-warning': node.warning || node.error
+                        }"
+                     ng-click="onNodeDetailsClick(node)"
+                     ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
+                
+                <div class="detailsOverlay" ng-if="node.showDetails">
+                    <div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
+
+                    <div ng-if="node.data.callDetails.context.domElement">
+                        <h4>Called on DOM element</h4>
+                        <div>{{node.data.callDetails.context.domElement}}</div>
+                    </div>
+
+                    <div ng-if="node.data.callDetails.context.length === 0">
+                        <h4>Called on 0 jQuery element</h4>
+                        <p class="advice">Useless function call, as the jQuery object is empty.</p>
+                    </div>
+
+                    <div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
+                        <h4>Called on 1 jQuery element</h4>
+                        <div>{{node.data.callDetails.context.firstElementPath}}</div>
+                    </div>
+
+                    <div ng-if="node.data.callDetails.context.length > 1">
+                        <h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
+                        <p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
+                            The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
+                        </p>
+                        <p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
+                    </div>
+
+                    <p class="advice" ng-if="node.data.resultsNumber === 0">
+                        The query returned 0 results. Could it be unused or dead code?
+                    </p>
+
+                    <div ng-if="node.parsedBacktrace">
+                        <h4>Backtrace</h4>
+                        <div class="table">
+                            <div ng-repeat="trace in node.parsedBacktrace track by $index">
+                                <div>{{trace.fnName || '(anonymous)'}}</div>
+                                <div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
+                            </div>
+                            <div ng-if="node.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
+                                <div>can't find any backtrace :/</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
+        </div>
+    </div>
+</div>

+ 0 - 1
lib/rulesChecker.js

@@ -1,4 +1,3 @@
-var Q = require('q');
 var debug = require('debug')('ylt:ruleschecker');
 
 var RulesChecker = function() {

+ 8 - 11
lib/runner.js

@@ -1,9 +1,10 @@
-var Q                   = require('q');
-var debug               = require('debug')('ylt:runner');
+var Q                       = require('q');
+var debug                   = require('debug')('ylt:runner');
 
-var phantomasWrapper    = require('./tools/phantomasWrapper');
-var rulesChecker        = require('./rulesChecker');
-var scoreCalculator     = require('./scoreCalculator');
+var phantomasWrapper        = require('./tools/phantomasWrapper');
+var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
+var rulesChecker            = require('./rulesChecker');
+var scoreCalculator         = require('./scoreCalculator');
 
 
 var Runner = function(params) {
@@ -21,12 +22,8 @@ var Runner = function(params) {
     phantomasWrapper.execute(data).then(function(phantomasResults) {
         data.toolsResults.phantomas = phantomasResults;
 
-        // Get the JS Execution Tree from offenders and put in the main object
-        try {
-            data.javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);    
-        } catch(e) {
-            debug('Could not find nor parse phantomas.offenders.javascriptExecutionTree');
-        }
+        // Treat the JS Execution Tree from offenders
+        data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
 
         // Other tools go here
 

+ 62 - 0
lib/tools/jsExecutionTransformer.js

@@ -0,0 +1,62 @@
+var debug = require('debug')('ylt:jsExecutionTransformer');
+
+var jsExecutionTransformer = function() {
+
+    this.transform = function(data) {
+        var javascriptExecutionTree = {};
+
+        debug('Starting JS execution transformation');
+
+        try {
+            javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
+        
+            if (javascriptExecutionTree.children) {
+                javascriptExecutionTree.children.forEach(function(node) {
+                    
+                    // Mark abnormal things with a warning flag
+                    var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
+                    if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
+                            node.data.resultsNumber === 0 ||
+                            contextLenght === 0) {
+                        node.warning = true;
+                    }
+
+                    // Mark errors with an error flag
+                    if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
+                        node.error = true;
+                    }
+
+                    // Mark a performance flag
+                    if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
+                        node.windowPerformance = true;
+                    }
+
+                    // Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
+                    switch(node.data.type) {
+                        case 'domInteractive':
+                            data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
+                            break;
+                        case 'domContentLoaded':
+                            data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
+                            break;
+                        case 'domContentLoadedEnd':
+                            data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
+                            break;
+                        case 'domComplete':
+                            data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
+                            break;
+                    }
+                });
+            }
+
+            debug('JS execution transformation complete');
+
+        } catch(err) {
+            throw err;
+        }
+
+        return javascriptExecutionTree;
+    };
+};
+
+module.exports = new jsExecutionTransformer();

+ 3 - 0
lib/yellowlabtools.js

@@ -26,6 +26,9 @@ var YellowLabTools = function(url, options) {
         var runner = new Runner(params)
             .then(function(data) {
                 deferred.resolve(data);
+            })
+            .fail(function(err) {
+                deferred.reject(err);
             });
     }