From c1bae20f01168ff28a1c0fd1267d172466f94817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 16 Jun 2015 20:33:19 +0200 Subject: [PATCH] File minification --- .../src/js/directives/offendersDirectives.js | 2 +- front/src/views/rule.html | 24 +- lib/metadata/policies.js | 12 +- lib/metadata/scoreProfileGeneric.json | 4 +- lib/tools/weightChecker/fileMinifier.js | 195 +++++ lib/tools/weightChecker/imageOptimizer.js | 7 +- lib/tools/weightChecker/weightChecker.js | 80 +- package.json | 27 +- test/core/fileMinifierTest.js | 214 ++++++ test/core/weightCheckerTest.js | 36 +- test/www/unminified-script.js | 190 +++++ test/www/unminified-stylesheet.css | 702 ++++++++++++++++++ 12 files changed, 1456 insertions(+), 37 deletions(-) create mode 100644 lib/tools/weightChecker/fileMinifier.js create mode 100644 test/core/fileMinifierTest.js create mode 100644 test/www/unminified-script.js create mode 100644 test/www/unminified-stylesheet.css 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 @@ -
+
- ({{offender.size}} kB) + ({{offender.size}} KB)
@@ -230,6 +230,26 @@
+
+

{{rule.value | bytes}} could be saved on

+
+
+
File
+
Current weight
+
Minified
+
Gain
+
+
+
+ +
+
{{file.original | bytes}}
+
{{file.minified | bytes}}
+
-{{file.gain | bytes}}
+
+
+
+

{{rule.value}} offenders

Please open the JS timeline diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index af1fec1..269c8b2 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -872,6 +872,16 @@ var policies = { "hasOffenders": true, "unit": 'bytes' }, + "fileMinification": { + "tool": "weightChecker", + "label": "File minification", + "message": "

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 0) { + break; + } + } + + return lineIndex; + }; + + + $scope.backToDashboard = function() { + $location.path('/result/' + $scope.runId); + }; + + $scope.testAgain = function() { + API.relaunchTest($scope.result); + }; + + loadResults(); + +}]); + +timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) { + return { + restrict: 'A', + link: function (scope, element, attributes) { + // When the user clicks on the timeline, find the right profiler line and scroll to it + element.on('click', function() { + var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick); + var lineElement = angular.element(document.getElementById('line_' + lineIndex)); + + // Animate the background color to "flash" the row + lineElement.addClass('highlight'); + $timeout(function() { + $animate.removeClass(lineElement, 'highlight'); + scope.$digest(); + }, 50); + + + window.scrollTo(0, lineElement[0].offsetTop); + }); + } + }; +}]); \ No newline at end of file diff --git a/test/www/unminified-stylesheet.css b/test/www/unminified-stylesheet.css new file mode 100644 index 0000000..620bf3b --- /dev/null +++ b/test/www/unminified-stylesheet.css @@ -0,0 +1,702 @@ +/* Timeline colors, related to Window Performances */ +.execution { + text-align: center; +} +.selectScript { + padding-bottom: 2em; + font-size: 0.9em; +} +.selectScript select { + max-width: 30em; +} +.selectScript.empty { + font-size: 0.8em; +} +.selectScript.empty select { + width: 10em; +} +.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 .interval .color.clickable { + cursor: pointer; +} +.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 auto; + padding: 0.5em; + min-width: 30em; + width: 30%; + border: 1px dotted #aaa; + text-align: left; +} +.subFilters { + margin-left: 3em; + font-size: 0.9em; +} +.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.highlight > div.startTime { + background-color: #C0F090; +} +.table > div.highlight-remove { + transition: 3s; +} +.table > div.highlight-remove > div.startTime { + transition: background-color 3s ease-in; +} +.table > div > .index { + color: #bbb; + word-break: normal; +} +.table > div > .type { + white-space: nowrap; +} +.table .children { + margin-top: 0.2em; + font-size: 0.8em; + line-height: 1.6em; +} +.table .child { + margin-left: 0.5em; +} +.table .child > .child { + margin-left: 1em; +} +.table .child:before { + content: "↳"; +} +.table .child .childArgs { + display: none; +} +.table .child span { + position: relative; +} +.table .child span:hover { + background: #EBD8E2; +} +.table .child span:hover div { + display: inline-block; +} +.table .child span:hover .childArgs { + display: block; + position: absolute; + padding: 0 1em 0 2em; + left: 100%; + top: 0; + background: #EBD8E2; + line-height: 1.3em; + height: 1.3em; + z-index: 2; +} +.table .showingDetails .child span:hover { + background: inherit; +} +.table .showingDetails .child span:hover .childArgs { + display: none; +} +.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 { + display: none; + 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: 2; +} +@media screen and (max-width: 1024px) { + .detailsOverlay { + width: 25em; + } +} +.showDetails .detailsOverlay { + display: block; +} +.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; +} +.queryWithoutResultsFilterOn > div { + display: none; +} +.queryWithoutResultsFilterOn > div.queryWithoutResults { + display: table-row; +} +.jQueryCallOnEmptyObjectFilterOn > div { + display: none; +} +.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject { + display: table-row; +} +.eventNotDelegatedFilterOn > div { + display: none; +} +.eventNotDelegatedFilterOn > div.eventNotDelegated { + display: table-row; +} +.jsErrorFilterOn > div { + display: none; +} +.jsErrorFilterOn > div.jsError { + display: table-row; +} +.testedUrl { + color: inherit; +} +.summary { + text-align: center; +} +.summary .globalScore { + display: table; + width: 60%; + margin: 3em auto; +} +.summary .globalScore > div { + display: table-cell; + width: 50%; + vertical-align: middle; +} +.summary .globalScore .globalGrade { + margin: 0.5 auto; + width: 2.5em; + height: 2.5em; + line-height: 2.5em; + border-radius: 0.5em; + font-size: 3em; + font-weight: bold; + vertical-align: middle; +} +.summary .globalScore .on100 { + font-size: 1.2em; + font-weight: bold; + margin: 0.5em 0 1em; +} +.summary .globalScore .screenshotWrapper:hover { + opacity: 0.75; +} +.summary .globalScore .screenshotWrapper:hover:after { + position: absolute; + width: 1.25em; + height: 1.25em; + top: 0.7em; + left: 1.55em; + font-size: 3em; + color: #FFF; + background: #000; + border-radius: 0.2em; + text-align: center; + content: "+"; + opacity: 0.85; +} +.summary .globalScore .screenshotWrapper.phone:hover:after { + top: 1.7em; + left: 0.64em; +} +.summary .globalScore .screenshotWrapper.tablet:hover:after { + top: 1.5em; + left: 0.9em; +} +.summary .notations { + display: table; + width: 80%; + margin: 0 10% 1.5em; + border-spacing: 1em; +} +.summary .notations > div { + display: table-row; +} +.summary .notations > div > div { + display: table-cell; + height: 2.5em; + vertical-align: middle; +} +.summary .notations .category { + font-weight: bold; + text-align: center; + width: 20%; +} +.summary .notations .criteria { + font-weight: normal; + width: 75%; +} +.summary .notations .A.categoryScore, +.summary .notations .B.categoryScore, +.summary .notations .C.categoryScore, +.summary .notations .D.categoryScore, +.summary .notations .E.categoryScore, +.summary .notations .F.categoryScore, +.summary .notations .NA.categoryScore { + width: 2.5em; + max-width: 2.5em; + min-width: 2.5em; + font-size: 2em; + text-align: center; + border-radius: 0.5em; + font-weight: bold; +} +.summary .notations .grade .A, +.summary .notations .grade .B, +.summary .notations .grade .C, +.summary .notations .grade .D, +.summary .notations .grade .E, +.summary .notations .grade .F, +.summary .notations .grade .NA { + width: 1em; + height: 1em; + font-size: 1em; + color: transparent; + margin: 0 auto; + border-radius: 0.5em; +} +.summary .notations .criteria .table { + width: 100%; +} +.summary .notations .criteria .table > div:hover > div { + background: #EBD8E2; + cursor: pointer; +} +.summary .notations .criteria .table > div:hover > div.info { + background: #FFF; +} +.summary .notations .criteria .table > div:hover > div.info .icon-question { + color: #EBD8E2; +} +.summary .notations .criteria .grade { + width: 10%; + padding-left: 0.5em; + padding-right: 0.5em; + vertical-align: middle; +} +.summary .notations .criteria .label { + width: 70%; +} +.summary .notations .criteria .result { + width: 18%; + font-weight: bold; + white-space: nowrap; + text-align: center; + vertical-align: middle; +} +.summary .notations .warning .label, +.summary .notations .warning .result, +.summary .notations .icon-warning { + color: #FF1919; +} +.summary .notations .criteria .info { + width: 2%; + text-align: center; + vertical-align: middle; + background: #FFF; + padding-left: 0.1em; + padding-right: 0.1em; +} +.summary .notations .criteria .icon-question { + color: transparent; +} +.summary .fromShare { + margin-bottom: 3em; +} +.summary .apiTip { + font-size: 0.8em; + margin-bottom: 4em; + color: #413; +} +.summary .apiTip a { + color: inherit; +} +.summary .tweet .tweetText { + color: #413; + background: #F2F2F2; + border: none; + width: 25em; + padding: 0.4em; + border-radius: 0.5em; + box-shadow: 0.05em 0.1em 0 0 #999; +} +.summary .tweet .tweetButton, +.summary .tweet .linkedinButton { + color: #413; + background: #F2F2F2; + margin-right: 0; +} +.summary .tweet .tweetButton:hover, +.summary .tweet .linkedinButton:hover { + color: #F2F2F2; + background: #e74c3c; +} +.summary .tweet input { + font-size: 0.9em; +} + + +.promess { + padding: 0em 2em; + margin-bottom: 0.5em; + font-weight: normal; + font-size: 1.2em; +} +.price { + padding: 0em 2em 3em; + margin-top: 0em; + font-size: 0.9em; +} +.url { + width: 50%; +} +.launchBtn { + background: #ffa319; + color: #fff; +} +.launchBtn:focus { + background: #e74c3c; +} +.launchBtn.disabled { + background: #deaca6; +} +.launchBtn.disabled:focus { + color: #ddd; +} +.settings { + width: 50%; + margin: 0 auto; +} +.settings input, +.settings select { + font-size: 1em; +} +.settings input[type=text] { + width: 100%; + min-width: 4em; +} +.device { + margin-top: 3em; +} +.device .item { + display: inline-block; + margin: 1em 0.75em; + width: 5.5em; + height: 5.5em; + color: #FFF; + border: 1px solid #FFF; + padding: 1px; + border-radius: 0.5em; + cursor: pointer; + text-decoration: none; + font-size: 0.8em; +} + +.device .item.active { + color: #ffa319; + border: 2px solid #ffa319; + padding: 0; +} +.device .item:hover { + color: #ffa319; +} +.device .item > div { + margin: 0.2em 0 0.1em; + font-size: 3em; +} +.settingsTooltip { + position: relative; +} +.settingsTooltip span { + font-size: 0.8em; + vertical-align: text-top; +} +.settingsTooltip div { + display: none; + position: absolute; + padding: 0.5em; + width: 25em; + background: #FFF; + color: #000; + font-size: 0.8em; + border-radius: 1em; + border: 2px solid #ffa319; + white-space: normal; + z-index: 2; +} +.settingsTooltip:hover div { + display: block; +} +.showAdvanced { + display: inline-block; + margin-top: 2em; + color: #FFF; + text-decoration: none; + font-size: 0.9em; +} +.showAdvanced:hover { + color: #ffa319; +} +.currentSettings { + font-size: 0.9em; +} +.currentSettings span { + color: #ffa319; +} +.currentSettings span:after { + color: #FFF; + content: ","; +} +.currentSettings span:last-child:after { + content: ""; +} +.advanced { + margin: 1em 0 0; + display: table; + width: 100%; + text-align: left; + border-spacing: 0.75em; +} +.advanced > div { + display: table-row; +} +.advanced > div > div { + display: table-cell; + width: 75%; +} +.advanced > div > div.label { + width: 25%; + white-space: nowrap; +} +.advanced .subTable { + display: table; + border-spacing: 0; + width: 100%; +} +.advanced .subTable > div { + display: table-row; +} +.advanced .subTable > div > div { + display: table-cell; + padding: 0 0 0.75em; +} +.features { + display: table; + width: 50%; + margin: 6em auto 0; + font-size: 0.9em; + color: #413; +} +.features > div { + width: 33.3%; + display: table-cell; + padding: 0 1.5em; +} +input[type=submit], +input.url { + padding: 0 0.5em; + margin: 0.5em; + font-size: 1.2em; + height: 2em; + border: 0 solid; + border-radius: 0.5em; + box-shadow: 0.1em 0.2em 0 0 #5e2846; + outline: none; +} +input[type=submit]:hover { + color: #ddd; +} +input[type=submit].clicked { + color: #ddd; + position: relative; + left: 0.1em; + top: 0.2em; + box-shadow: none; +} \ No newline at end of file