diff --git a/front/src/js/directives/offendersDirectives.js b/front/src/js/directives/offendersDirectives.js
index 52d9db0..d122393 100644
--- a/front/src/js/directives/offendersDirectives.js
+++ b/front/src/js/directives/offendersDirectives.js
@@ -865,7 +865,7 @@
var kilo = bytes / 1024;
if (kilo < 1) {
- return bytes + ' Bytes';
+ return bytes + ' bytes';
}
if (kilo < 100) {
diff --git a/front/src/views/rule.html b/front/src/views/rule.html
index d9f1a7f..7e3d15a 100644
--- a/front/src/views/rule.html
+++ b/front/src/views/rule.html
@@ -137,9 +137,9 @@
This is the weight that could be saved if all text resources were correctly minified.
The tools in use here are UglifyJS, clean-css and HTMLMinifier.
The gains of minification are generally small, but the impact can be high when these text files are loaded on the critical path.
", + "isOkThreshold": 20480, + "isBadThreshold": 81920, + "isAbnormalThreshold": 153600, + "hasOffenders": true, + "unit": 'bytes' + }, "requests": { "tool": "phantomas", "label": "Total requests number", @@ -888,7 +898,7 @@ var policies = { .map(function(offender) { return offendersHelpers.fileWithSizePattern(offender); }).sort(function(a, b) { - return b.size - a.size; + return (b.file < a.file) ? 1 : (b.file > a.file) ? -1 : 0; }) }; } diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 81c7781..1407255 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -73,7 +73,9 @@ "label": "Page weight", "policies": { "totalWeight": 5, - "imageOptimization": 3 + "imageOptimization": 2, + "fileMinification": 1, + "assetsNotGzipped": 2 } }, "requests": { diff --git a/lib/tools/weightChecker/fileMinifier.js b/lib/tools/weightChecker/fileMinifier.js new file mode 100644 index 0000000..dd5ec2b --- /dev/null +++ b/lib/tools/weightChecker/fileMinifier.js @@ -0,0 +1,195 @@ +var debug = require('debug')('ylt:fileMinifier'); + +var Q = require('q'); +var UglifyJS = require('uglify-js'); +var CleanCSS = require('clean-css'); +var Minimize = require('minimize'); + + +var FileMinifier = function() { + + function minifyFile(entry) { + var deferred = Q.defer(); + + if (!entry.weightCheck || !entry.weightCheck.body) { + // No valid file available + deferred.resolve(entry); + return deferred.promise; + } + + var fileSize = entry.weightCheck.uncompressedSize; + debug('Let\'s try to optimize %s', entry.url); + debug('Current file size is %d', fileSize); + + if (entry.isJS) { + + debug('File is a JS'); + + // Starting softly with a lossless compression + return minifyJs(entry.weightCheck.body) + + .then(function(newFile) { + if (!newFile) { + debug('Optimization didn\'t work'); + return entry; + } + + var newFileSize = newFile.length; + + debug('JS minification complete for %s', entry.url); + + if (gainIsEnough(fileSize, newFileSize)) { + entry.weightCheck.minified = newFileSize; + entry.weightCheck.isMinified = false; + debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); + } + + return entry; + }) + + .fail(function(err) { + return entry; + }); + + } else if (entry.isCSS) { + + debug('File is a CSS'); + + // Starting softly with a lossless compression + return minifyCss(entry.weightCheck.body) + + .then(function(newFile) { + if (!newFile) { + debug('Optimization didn\'t work'); + return entry; + } + + var newFileSize = newFile.length; + + debug('CSS minification complete for %s', entry.url); + + if (gainIsEnough(fileSize, newFileSize)) { + entry.weightCheck.minified = newFileSize; + entry.weightCheck.isMinified = false; + debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); + } + + return entry; + }) + + .fail(function(err) { + return entry; + }); + + } else if (entry.isHTML) { + + debug('File is an HTML'); + + // Starting softly with a lossless compression + return minifyHtml(entry.weightCheck.body) + + .then(function(newFile) { + console.log('KKKKKKKKKKKK'); + if (!newFile) { + debug('Optimization didn\'t work'); + return entry; + } + + var newFileSize = newFile.length; + + debug('HTML minification complete for %s', entry.url); + + if (gainIsEnough(fileSize, newFileSize)) { + entry.weightCheck.minified = newFileSize; + entry.weightCheck.isMinified = false; + debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); + } else { + console.log('OOOO old file size: ' + fileSize); + console.log('OOOO new file size: ' + newFileSize); + console.log(entry.weightCheck); + } + + return entry; + }) + + .fail(function(err) { + console.log('LLLLLLLLLLLLLLL'); + console.log(err); + return entry; + }); + + } else { + debug('File type %s is not an (optimizable) image', entry.contentType); + deferred.resolve(entry); + } + + return deferred.promise; + } + + // The gain is estimated of enough value if it's over 2KB or over 20%, + // but it's ignored if is below 100 bytes + function gainIsEnough(oldWeight, newWeight) { + var gain = oldWeight - newWeight; + var ratio = gain / oldWeight; + return (gain > 2096 || (ratio > 0.2 && gain > 400)); + } + + // Uglify + function minifyJs(body) { + var deferred = Q.defer(); + + try { + var result = UglifyJS.minify(body, {fromString: true}); + deferred.resolve(result.code); + } catch(err) { + deferred.reject(err); + } + + return deferred.promise; + } + + // Clear-css + function minifyCss(body) { + var deferred = Q.defer(); + + try { + var result = new CleanCSS({compatibility: 'ie8'}).minify(body); + deferred.resolve(result.styles); + } catch(err) { + deferred.reject(err); + } + + return deferred.promise; + } + + // HTMLMinifier + function minifyHtml(body) { + var deferred = Q.defer(); + + var minimize = new Minimize({ + empty: true, // KEEP empty attributes + conditionals: true, // KEEP conditional internet explorer comments + spare: true // KEEP redundant attributes + }); + + minimize.parse(body, function (error, data) { + if (error) { + deferred.reject(error); + } else { + deferred.resolve(data); + } + }); + + return deferred.promise; + } + + return { + minifyFile: minifyFile, + minifyJs: minifyJs, + minifyCss: minifyCss, + minifyHtml: minifyHtml, + gainIsEnough: gainIsEnough + }; +}; + +module.exports = new FileMinifier(); \ No newline at end of file diff --git a/lib/tools/weightChecker/imageOptimizer.js b/lib/tools/weightChecker/imageOptimizer.js index 93838f8..453f1b8 100644 --- a/lib/tools/weightChecker/imageOptimizer.js +++ b/lib/tools/weightChecker/imageOptimizer.js @@ -7,18 +7,18 @@ var jpegoptim = require('imagemin-jpegoptim'); var ImageOptimizer = function() { var MAX_JPEG_QUALITY = 85; - var OPTIPNG_COMPRESSION_LEVEL = 2; + var OPTIPNG_COMPRESSION_LEVEL = 1; function optimizeImage(entry) { var deferred = Q.defer(); - if (!entry.weightCheck.body) { + if (!entry.weightCheck || !entry.weightCheck.body) { // No valid file available deferred.resolve(entry); return deferred.promise; } - var fileSize = entry.weightCheck.bodySize; + var fileSize = entry.weightCheck.uncompressedSize; debug('Let\'s try to optimize %s', entry.url); debug('Current file size is %d', fileSize); @@ -227,7 +227,6 @@ var ImageOptimizer = function() { } return { - recompressIfImage: optimizeImage, optimizeImage: optimizeImage, compressJpegLosslessly: compressJpegLosslessly, compressJpegLossly: compressJpegLossly, diff --git a/lib/tools/weightChecker/weightChecker.js b/lib/tools/weightChecker/weightChecker.js index ec50a38..d90e629 100644 --- a/lib/tools/weightChecker/weightChecker.js +++ b/lib/tools/weightChecker/weightChecker.js @@ -13,6 +13,7 @@ var async = require('async'); var request = require('request'); var imageOptimizer = require('./imageOptimizer'); +var fileMinifier = require('./fileMinifier'); var WeightChecker = function() { @@ -23,6 +24,8 @@ var WeightChecker = function() { // This function will re-download every asset and check if it could be optimized function recheckAllFiles(data) { + var startTime = Date.now(); + debug('Redownload started'); var deferred = Q.defer(); var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList); @@ -35,7 +38,9 @@ var WeightChecker = function() { redownloadEntry(entry) - .then(imageOptimizer.recompressIfImage) + .then(imageOptimizer.optimizeImage) + + .then(fileMinifier.minifyFile) .then(function(newEntry) { callback(null, newEntry); @@ -49,11 +54,20 @@ var WeightChecker = function() { // Lanch all redownload functions and wait for completion async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) { + if (err) { debug(err); deferred.reject(err); } else { + debug('All files checked'); + endTime = Date.now(); + debug('Redownload took %d ms', endTime - startTime); + + // Remove unwanted requests (redirections, about:blank) + results = results.filter(function(result) { + return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0); + }); var metrics = {}; var offenders = {}; @@ -67,6 +81,11 @@ var WeightChecker = function() { offenders.imageOptimization = listImageNotOptimized(results); metrics.imageOptimization = offenders.imageOptimization.totalGain; + // File minification + offenders.fileMinification = listFilesNotMinified(results); + metrics.fileMinification = offenders.fileMinification.totalGain; + + data.toolsResults.weightChecker = { metrics: metrics, @@ -144,14 +163,14 @@ var WeightChecker = function() { }; requests.forEach(function(req) { - if (req.weightCheck.bodySize && req.weightCheck.isOptimized === false) { - var gain = req.weightCheck.bodySize - req.weightCheck.optimized; + if (req.weightCheck.uncompressedSize && req.weightCheck.isOptimized === false) { + var gain = req.weightCheck.uncompressedSize - req.weightCheck.optimized; results.totalGain += gain; results.images.push({ url: req.url, - original: req.weightCheck.bodySize, + original: req.weightCheck.uncompressedSize, optimized: req.weightCheck.optimized, lossless: req.weightCheck.lossless, lossy: req.weightCheck.lossy, @@ -164,10 +183,35 @@ var WeightChecker = function() { } + function listFilesNotMinified(requests) { + var results = { + totalGain: 0, + files: [] + }; + + requests.forEach(function(req) { + if (req.weightCheck.uncompressedSize && req.weightCheck.isMinified === false) { + var gain = req.weightCheck.uncompressedSize - req.weightCheck.minified; + + results.totalGain += gain; + + results.files.push({ + url: req.url, + original: req.weightCheck.uncompressedSize, + minified: req.weightCheck.minified, + gain: gain + }); + } + }); + + return results; + } + + function redownloadEntry(entry) { var deferred = Q.defer(); - function onError(message) { + function downloadError(message) { debug('Could not download %s Error: %s', entry.url, message); entry.weightCheck = { message: message @@ -175,18 +219,32 @@ var WeightChecker = function() { deferred.resolve(entry); } + // Not downloaded again but will be counted in totalWeight + function notDownloadableFile(message) { + entry.weightCheck = { + message: message + }; + deferred.resolve(entry); + } + + // Not counted in totalWeight + function unwantedFile(message) { + debug(message); + deferred.resolve(entry); + } + if (entry.method !== 'GET') { - onError('only downloading GET'); + notDownloadableFile('only downloading GET'); return deferred.promise; } if (entry.status !== 200) { - onError('only downloading requests with status code 200'); + unwantedFile('only downloading requests with status code 200'); return deferred.promise; } if (entry.url === 'about:blank') { - onError('not downloading about:blank'); + unwantedFile('not downloading about:blank'); return deferred.promise; } @@ -202,15 +260,14 @@ var WeightChecker = function() { url: entry.url, headers: reqHeaders, timeout: REQUEST_TIMEOUT - //encoding: (entry.contentType === 'image/jpeg' || entry.contentType === 'image/png') ? 'binary' : null }; download(requestOptions, entry.contentType, function(error, result) { if (error) { if (error.code === 'ETIMEDOUT') { - onError('timeout after ' + REQUEST_TIMEOUT + 'ms'); + downloadError('timeout after ' + REQUEST_TIMEOUT + 'ms'); } else { - onError('error while downloading: ' + error.code); + downloadError('error while downloading: ' + error.code); } return; } @@ -332,7 +389,6 @@ var WeightChecker = function() { }); } - return { recheckAllFiles: recheckAllFiles, listRequestWeight: listRequestWeight, diff --git a/package.json b/package.json index 03c1a33..bf30697 100644 --- a/package.json +++ b/package.json @@ -11,21 +11,24 @@ }, "main": "./lib/index.js", "dependencies": { - "async": "~1.0.0", - "body-parser": "~1.12.4", - "compression": "~1.4.4", - "cors": "^2.6.0", - "debug": "~2.2.0", - "express": "~4.12.4", - "imagemin": "~3.2.0", - "imagemin-jpegoptim": "~4.0.0", + "async": "1.0.0", + "body-parser": "1.12.4", + "clean-css": "3.3.0", + "compression": "1.4.4", + "cors": "2.6.0", + "debug": "2.2.0", + "express": "4.12.4", + "imagemin": "3.2.0", + "imagemin-jpegoptim": "4.0.0", "lwip": "0.0.6", - "meow": "^3.1.0", + "meow": "3.1.0", + "minimize": "1.4.1", "phantomas": "1.10.2", "ps-node": "0.0.4", - "q": "~1.4.1", - "rimraf": "~2.3.4", - "temporary": "0.0.8" + "q": "1.4.1", + "rimraf": "2.3.4", + "temporary": "0.0.8", + "uglify-js": "2.4.23" }, "devDependencies": { "chai": "^2.3.0", diff --git a/test/core/fileMinifierTest.js b/test/core/fileMinifierTest.js new file mode 100644 index 0000000..baa92f5 --- /dev/null +++ b/test/core/fileMinifierTest.js @@ -0,0 +1,214 @@ +var should = require('chai').should(); +var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier'); +var fs = require('fs'); +var path = require('path'); + +describe('fileMinifier', function() { + + it('should minify a JS file with minifyJs', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js')); + + var fileSize = fileContent.length; + + fileMinifier.minifyJs(fileContent.toString()).then(function(newFile) { + var newFileSize = newFile.length; + newFileSize.should.be.below(fileSize); + done(); + }).fail(function(err) { + done(err); + }); + }); + + it('should minify a JS file with minifyFile', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/unminified-script.js', + requestHeaders: { + 'User-Agent': 'something', + Referer: 'http://www.google.fr/', + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate' + }, + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + fileMinifier.minifyFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('isMinified').that.equals(false); + newEntry.weightCheck.should.have.a.property('minified').that.is.below(fileSize); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should fail minifying an already minified JS', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery1.8.3.js')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/jquery1.8.3.js', + requestHeaders: { + 'User-Agent': 'something', + Referer: 'http://www.google.fr/', + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate' + }, + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + fileMinifier.minifyFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('isMinified'); + newEntry.weightCheck.should.not.have.a.property('minified'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should fail minifying a JS with syntax errors', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/svg-image.svg', + requestHeaders: { + 'User-Agent': 'something', + Referer: 'http://www.google.fr/', + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate' + }, + status: 200, + isJS: true, + type: 'js', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + fileMinifier.minifyFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.not.have.a.property('isMinified'); + newEntry.weightCheck.should.not.have.a.property('minified'); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should minify a CSS file with clean-css', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css')); + + var fileSize = fileContent.length; + + fileMinifier.minifyCss(fileContent.toString()).then(function(newFile) { + var newFileSize = newFile.length; + newFileSize.should.be.below(fileSize); + done(); + }).fail(function(err) { + done(err); + }); + }); + + it('should minify a CSS file with minifyFile', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css')); + var fileSize = fileContent.length; + + var entry = { + method: 'GET', + url: 'http://localhost:8388/unminified-stylesheet.css', + requestHeaders: { + 'User-Agent': 'something', + Referer: 'http://www.google.fr/', + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate' + }, + status: 200, + isCSS: true, + type: 'css', + contentLength: 999, + weightCheck: { + body: fileContent.toString('utf8'), + totalWeight: fileSize + 200, + headersSize: 200, + bodySize: fileSize, + isCompressed: false, + uncompressedSize: fileSize + } + }; + + fileMinifier.minifyFile(entry) + + .then(function(newEntry) { + newEntry.weightCheck.should.have.a.property('isMinified').that.equals(false); + newEntry.weightCheck.should.have.a.property('minified').that.is.below(fileSize); + + done(); + }) + + .fail(function(err) { + done(err); + }); + }); + + it('should minify an HTML file with ', function(done) { + var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html')); + + var fileSize = fileContent.length; + + fileMinifier.minifyHtml(fileContent.toString()).then(function(newFile) { + var newFileSize = newFile.length; + newFileSize.should.be.below(fileSize); + done(); + }).fail(function(err) { + done(err); + }); + }); + +}); diff --git a/test/core/weightCheckerTest.js b/test/core/weightCheckerTest.js index 16d15b2..61ddf9d 100644 --- a/test/core/weightCheckerTest.js +++ b/test/core/weightCheckerTest.js @@ -57,6 +57,30 @@ describe('weightChecker', function() { type: 'image', contentType: 'image/svg+xml' }, + { + method: 'GET', + url: 'http://localhost:8388/unminified-script.js', + requestHeaders: { + 'User-Agent': 'something', + Referer: 'http://www.google.fr/', + Accept: '*/*' + }, + status: 200, + isJS: true, + type: 'js' + }, + { + method: 'GET', + url: 'http://localhost:8388/unminified-stylesheet.css', + requestHeaders: { + 'User-Agent': 'something', + Referer: 'http://www.google.fr/', + Accept: '*/*' + }, + status: 200, + isCSS: true, + type: 'css' + }, { method: 'GET', url: 'about:blank', @@ -92,14 +116,19 @@ describe('weightChecker', function() { data.toolsResults.weightChecker.offenders.should.have.a.property('totalWeight'); data.toolsResults.weightChecker.offenders.totalWeight.totalWeight.should.be.above(0); data.toolsResults.weightChecker.offenders.totalWeight.byType.html.requests.length.should.equal(1); - data.toolsResults.weightChecker.offenders.totalWeight.byType.js.requests.length.should.equal(1); + data.toolsResults.weightChecker.offenders.totalWeight.byType.js.requests.length.should.equal(2); + data.toolsResults.weightChecker.offenders.totalWeight.byType.css.requests.length.should.equal(1); data.toolsResults.weightChecker.offenders.totalWeight.byType.image.requests.length.should.equal(2); - data.toolsResults.weightChecker.offenders.totalWeight.byType.other.requests.length.should.equal(1); + data.toolsResults.weightChecker.offenders.totalWeight.byType.other.requests.length.should.equal(0); data.toolsResults.weightChecker.offenders.should.have.a.property('imageOptimization'); data.toolsResults.weightChecker.offenders.imageOptimization.totalGain.should.be.above(0); data.toolsResults.weightChecker.offenders.imageOptimization.images.length.should.equal(2); + data.toolsResults.weightChecker.offenders.should.have.a.property('fileMinification'); + data.toolsResults.weightChecker.offenders.fileMinification.totalGain.should.be.above(0); + data.toolsResults.weightChecker.offenders.fileMinification.files.length.should.equal(2); + done(); }) @@ -222,8 +251,7 @@ describe('weightChecker', function() { weightChecker.redownloadEntry(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('message').that.equals('only downloading requests with status code 200'); - + newEntry.should.not.have.a.property('weightCheck'); done(); }) diff --git a/test/www/unminified-script.js b/test/www/unminified-script.js new file mode 100644 index 0000000..ff44234 --- /dev/null +++ b/test/www/unminified-script.js @@ -0,0 +1,190 @@ +var timelineCtrl = angular.module('timelineCtrl', []); + +timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) { + $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, exclude: 'toolsResults'}, function(result) { + $rootScope.loadedResult = result; + $scope.result = result; + render(); + }); + } else { + $scope.result = $rootScope.loadedResult; + render(); + } + } + + function render() { + initFilters(); + initScriptFiltering(); + initExecutionTree(); + initTimeline(); + $timeout(initProfiler, 100); + } + + function initFilters() { + var hash = $location.hash(); + var filter = null; + + if (hash.indexOf('filter=') === 0) { + filter = hash.substr(7); + } + + $scope.warningsFilterOn = (filter !== null); + $scope.warningsFilters = { + queryWithoutResults: (filter === null || filter === 'queryWithoutResults'), + jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'), + eventNotDelegated: (filter === null || filter === 'eventNotDelegated'), + jsError: (filter === null || filter === 'jsError') + }; + } + + function initScriptFiltering() { + var offenders = $scope.result.rules.jsCount.offendersObj.list; + $scope.scripts = []; + + offenders.forEach(function(script) { + var filePath = script.file; + + if (filePath.length > 100) { + filePath = filePath.substr(0, 98) + '...'; + } + + var scriptObj = { + fullPath: script.file, + shortPath: filePath + }; + + $scope.scripts.push(scriptObj); + }); + } + + function initExecutionTree() { + var originalExecutions = $scope.result.javascriptExecutionTree.children || []; + + // Detect the last event of all (before filtering) and read time + var lastEvent = originalExecutions[originalExecutions.length - 1]; + $scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0); + + // Filter + $scope.executionTree = []; + originalExecutions.forEach(function(node) { + + // Filter by script (if enabled) + if ($scope.selectedScript) { + if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) { + return; + } + if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") { + return; + } + } + + $scope.executionTree.push(node); + }); + } + + function initTimeline() { + + // Split the timeline into 200 intervals + var numberOfIntervals = 199; + $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