diff --git a/bower.json b/bower.json index 3a28910..9d7da0f 100644 --- a/bower.json +++ b/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" } diff --git a/front/src/css/dashboard.css b/front/src/css/dashboard.css index 9d64baf..5a6147d 100644 --- a/front/src/css/dashboard.css +++ b/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; diff --git a/front/src/css/rule.css b/front/src/css/rule.css new file mode 100644 index 0000000..e69de29 diff --git a/front/src/css/timeline.css b/front/src/css/timeline.css new file mode 100644 index 0000000..c2eb686 --- /dev/null +++ b/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; +} diff --git a/front/src/js/app.js b/front/src/js/app.js index 6d44767..612ad18 100644 --- a/front/src/js/app.js +++ b/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. diff --git a/front/src/js/controllers/dashboardCtrl.js b/front/src/js/controllers/dashboardCtrl.js index 3598b70..f28a4ad 100644 --- a/front/src/js/controllers/dashboardCtrl.js +++ b/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(); }]); \ No newline at end of file diff --git a/front/src/js/controllers/ruleCtrl.js b/front/src/js/controllers/ruleCtrl.js new file mode 100644 index 0000000..7ad7a97 --- /dev/null +++ b/front/src/js/controllers/ruleCtrl.js @@ -0,0 +1,5 @@ +var ruleCtrl = angular.module('ruleCtrl', []); + +ruleCtrl.controller('RuleCtrl', ['$scope', function($scope) { + +}]); \ No newline at end of file diff --git a/front/src/js/controllers/timelineCtrl.js b/front/src/js/controllers/timelineCtrl.js new file mode 100644 index 0000000..8d4b18c --- /dev/null +++ b/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 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(); + +}]); \ No newline at end of file diff --git a/front/src/js/models/resultsFactory.js b/front/src/js/models/resultsFactory.js index fab26d0..33b286e 100644 --- a/front/src/js/models/resultsFactory.js +++ b/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'}} + }); }]); \ No newline at end of file diff --git a/front/src/less/dashboard.less b/front/src/less/dashboard.less index 629df3b..bdaa76b 100644 --- a/front/src/less/dashboard.less +++ b/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; diff --git a/front/src/less/rule.less b/front/src/less/rule.less new file mode 100644 index 0000000..e69de29 diff --git a/front/src/less/timeline.less b/front/src/less/timeline.less new file mode 100644 index 0000000..a15465a --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/front/src/main.html b/front/src/main.html index f062e71..72b719d 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -9,6 +9,8 @@ + + @@ -18,6 +20,8 @@ + + diff --git a/front/src/views/rule.html b/front/src/views/rule.html new file mode 100644 index 0000000..e69de29 diff --git a/front/src/views/timeline.html b/front/src/views/timeline.html new file mode 100644 index 0000000..f3fcb25 --- /dev/null +++ b/front/src/views/timeline.html @@ -0,0 +1,147 @@ +
+
+

Javascript Timeline

+

This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.

+ +
+
+
+
+
+
+
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
+
+
+
+
0 ms
+
{{endTime | number: 0}} ms
+
+
+
+
DOM creation
+
DOM interactive
+
DOM content loaded event
+
Page completion
+
Page is complete
+
+
+
Executing Javascript and DOM queries here is a bad practice and slows down the DOM construction.
+
Some frameworks do things here, but it's not reliable and should be avoided.
+
Also known as "document ready". This is where you should execute top-priority scripts, like binding action buttons or launch a video player.
+
Here you can execute mid-priority tasks. Loading a script with createElement('script') is one way to do so.
+
The page is considered loaded, it's time for low priority things : trackers, social plugins, easter egg...
+
+
+
+ +

Javascript Profiler

+

+ The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads. +

+
+
+ + +
+
+ + Filter by + +
+
+
+ + +
+
+
Type
+
Params
+
+
Timestamp
+
+
+ +
{{$index + 1}}
+
{{node.data.type}}
+ +
+ {{node.data.callDetails.arguments[0]}} + : {{node.data.callDetails.arguments[1]}} + : {{node.data.callDetails.arguments[2]}} + : {{node.data.callDetails.arguments[3]}} +
+ +
+
+ +
+
+ +
+

Called on DOM element

+
{{node.data.callDetails.context.domElement}}
+
+ +
+

Called on 0 jQuery element

+

Useless function call, as the jQuery object is empty.

+
+ +
+

Called on 1 jQuery element

+
{{node.data.callDetails.context.firstElementPath}}
+
+ +
+

Called on {{node.data.callDetails.context.length}} jQuery elements

+

+ 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). +

+

First one is: {{node.data.callDetails.context.firstElementPath}}

+
+ +

+ The query returned 0 results. Could it be unused or dead code? +

+ +
+

Backtrace

+
+
+
{{trace.fnName || '(anonymous)'}}
+ +
+
+
can't find any backtrace :/
+
+
+
+
+
+
{{node.data.timestamp | number: 0}} ms
+
+
+
\ No newline at end of file diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index f6f6d65..1fab084 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -1,4 +1,3 @@ -var Q = require('q'); var debug = require('debug')('ylt:ruleschecker'); var RulesChecker = function() { diff --git a/lib/runner.js b/lib/runner.js index e48a306..0cd4dcb 100644 --- a/lib/runner.js +++ b/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 diff --git a/lib/tools/jsExecutionTransformer.js b/lib/tools/jsExecutionTransformer.js new file mode 100644 index 0000000..c7801e3 --- /dev/null +++ b/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(); \ No newline at end of file diff --git a/lib/yellowlabtools.js b/lib/yellowlabtools.js index a56e49d..bca7a61 100644 --- a/lib/yellowlabtools.js +++ b/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); }); }