From e1c45f32cecd196b5ad960e6b6b18473cce016c5 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Thu, 10 Aug 2017 12:22:50 -0700 Subject: [PATCH 1/4] Add node perf tester based on Chrome Devtools protocol --- .gitignore | 1 + node-perf-tester.js | 93 ++++++++++++++++++++++++++++ options.json | 4 ++ package.json | 11 ++++ yarn.lock | 144 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 253 insertions(+) create mode 100644 node-perf-tester.js create mode 100644 options.json create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 8d4ae25..1e501e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bower_components +node_modules \ No newline at end of file diff --git a/node-perf-tester.js b/node-perf-tester.js new file mode 100644 index 0000000..03ce432 --- /dev/null +++ b/node-perf-tester.js @@ -0,0 +1,93 @@ +const path = require('path'); + +const CDP = require('chrome-remote-interface'); + +const argv = require('minimist')(process.argv.slice(2)); + +const NUMBER_OF_RUNS = argv.runs || 100; + +(async function() { + let port = argv.port; + let chrome; + + if (!argv.port) { + const chromeLauncher = require('chrome-launcher'); + chrome = await chromeLauncher.launch({ + chromeFlags: ['--headless', '--disable-gpu', '--remote-debugging-address=0.0.0.0'], + port: 0 + }); + port = chrome.port; + } + + const tab = await CDP.New({port}); + const client = await CDP({tab, port}); + + const {Page, Network, Runtime} = client; + + const ONE_MB = 1024 * 1024 / 8; + const throttling = { + FAST_3G: { + downloadThroughput: 1.6 * ONE_MB * .9, + uploadThroughput: .75 * ONE_MB * .9 + }, + SLOW_3G: { + downloadThroughput: .5 * ONE_MB * .8, + uploadThroughput: .5 * ONE_MB * .8 + } + } + + await Promise.all([ + Page.enable(), + Network.enable(), + port && argv.throttling && Network.emulateNetworkConditions( + Object.assign({}, throttling[argv.throttling], { + offline: false, + latency: 10 + }) + ), + Network.clearBrowserCache(), + Network.setCacheDisabled({cacheDisabled: true}), + Network.setBypassServiceWorker({bypass: true}), + ]); + + let loadEventPromise; + + Page.loadEventFired(() => { + loadEventPromise(); + }); + + const options = require(path.join(process.cwd(), argv.targets)); + + const perfTimings = {}; + for (const [type] of options) { + perfTimings[type] = []; + } + + process.on('exit', async() => { + for (const [type, timings] of Object.entries(perfTimings)) { + const average = timings.reduce((a, b) => a + b) / timings.length; + console.log(`Average gain for ${type} in ${timings.length} runs is ${average}`); + } + + await CDP.Close({port, id: tab.id}); + await client.close(); + if (!argv.port) { + await chrome.kill(); + } + }); + + for (let i = 0; i < NUMBER_OF_RUNS; i++) { + for (const [type, url] of options) { + requestType = type; + + Page.navigate({url}); + + await new Promise(resolve => { + loadEventPromise = resolve; + }); + const {result: {value: perfTiming}} = await Runtime.evaluate({expression: 'window.perfTiming'}); + perfTimings[type].push(perfTiming); + } + process.stdout.write(`${i + 1}/${NUMBER_OF_RUNS}\r`); + } +})() \ No newline at end of file diff --git a/options.json b/options.json new file mode 100644 index 0000000..2c1f787 --- /dev/null +++ b/options.json @@ -0,0 +1,4 @@ +[ + ["original", "http://localhost:8081/youtube/"], + ["effects", "http://localhost:8081/youtube/index-effects.html"] +] \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0f38055 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "perf-tester", + "version": "0.0.1", + "main": "node-perf-tester.js", + "license": "BSD-3-Clause", + "dependencies": { + "chrome-launcher": "^0.4.0", + "chrome-remote-interface": "^0.24.3", + "minimist": "^1.2.0" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..591d7d7 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,144 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/core-js@^0.9.41": + version "0.9.42" + resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-0.9.42.tgz#dd6da92cd7d5ab5ca0b4477524537c3e633b6bce" + +"@types/mkdirp@^0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.3.29.tgz#7f2ad7ec55f914482fc9b1ec4bb1ae6028d46066" + +"@types/node@6.0.66": + version "6.0.66" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.66.tgz#5680b74a6135d33d4c00447e7c3dc691a4601625" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +chrome-launcher@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.4.0.tgz#805e4a90d3d16c93655216aeba1fe880922ecc1f" + dependencies: + "@types/core-js" "^0.9.41" + "@types/mkdirp" "^0.3.29" + "@types/node" "6.0.66" + lighthouse-logger "^1.0.0" + mkdirp "0.5.1" + rimraf "^2.6.1" + +chrome-remote-interface@^0.24.3: + version "0.24.3" + resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.24.3.tgz#ee38252fd43bea435dd1dbb74f4b90d179186bc9" + dependencies: + commander "2.1.x" + ws "2.0.x" + +commander@2.1.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +debug@^2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@^7.0.5: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +lighthouse-logger@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.0.0.tgz#c6abdfbbbf0b4a541ab33864802cbad8944bcc8c" + dependencies: + debug "^2.6.8" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +ultron@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +ws@2.0.x: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-2.0.3.tgz#532fd499c3f7d7d720e543f1f807106cfc57d9cb" + dependencies: + ultron "~1.1.0" From 34d401ce78aadca6e4d62d3304fe011ab7d95956 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Thu, 10 Aug 2017 12:29:47 -0700 Subject: [PATCH 2/4] Fix missing window.perfTiming and correct line-endings --- .gitattributes | 1 + perf.js | 135 +++++++++++++++++++++++++------------------------ 2 files changed, 70 insertions(+), 66 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/perf.js b/perf.js index e1bae03..3aad6c4 100644 --- a/perf.js +++ b/perf.js @@ -1,66 +1,69 @@ -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt - * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt - */ -// x-browser compat. -(function() { - let wcr = false; - - addEventListener('WebComponentsReady', function() { - wcr = true; - }); - - console.perf = function() { - if (window.HTMLImports && !HTMLImports.useNative && !wcr) { - let fn = console._perf.bind(console); - HTMLImports.whenReady(fn); - } else { - console._perf(); - } - }; - - console._perf = function() { - if (window.gc) { - for (let i=0; i<20; i++) { - gc(); - } - } - if (console.time) { - console.time('perf'); - } - console.perf.time = performance.now(); - }; - - console.perfEnd = function(info) { - if (window.WebComponents) { - if (!wcr) { - addEventListener('WebComponentsReady', function() { - console._perfEnd(info); - }); - } else { - console._perfEnd(info); - } - } else { - console._perfEnd(info); - } - }; - - console._perfEnd = function(info) { - // force layout - document.body.offsetWidth; - let time = performance.now() - console.perf.time; - if (console.time) { - console.timeEnd('perf'); - } - document.title = time.toFixed(1) + 'ms: ' + document.title; - if (window.top !== window) { - window.top.postMessage({time: time + 'ms', info: info}, '*'); - } - }; - -})(); +/** + * @license + * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at + * http:polymer.github.io/LICENSE.txt The complete set of authors may be found + * at http:polymer.github.io/AUTHORS.txt The complete set of contributors may be + * found at http:polymer.github.io/CONTRIBUTORS.txt Code distributed by Google + * as part of the polymer project is also subject to an additional IP rights + * grant found at http:polymer.github.io/PATENTS.txt + */ +// x-browser compat. +(function() { +let wcr = false; + +addEventListener('WebComponentsReady', function() { + wcr = true; +}); + +console.perf = function() { + if (window.HTMLImports && !HTMLImports.useNative && !wcr) { + let fn = console._perf.bind(console); + HTMLImports.whenReady(fn); + } else { + console._perf(); + } +}; + +console._perf = function() { + if (window.gc) { + for (let i = 0; i < 20; i++) { + gc(); + } + } + if (console.time) { + console.time('perf'); + } + console.perf.time = performance.now(); +}; + +console.perfEnd = function(info) { + if (window.WebComponents) { + if (!wcr) { + addEventListener('WebComponentsReady', function() { + console._perfEnd(info); + }); + } else { + console._perfEnd(info); + } + } else { + console._perfEnd(info); + } +}; + +console._perfEnd = function(info, options = {}) { + if (!options.skipForceLayout) { + // force layout + document.body.offsetWidth; + } + window.perfTiming = performance.now() - console.perf.time; + if (console.time) { + console.timeEnd('perf'); + } + document.title = window.perfTiming.toFixed(1) + 'ms: ' + document.title; + if (window.top !== window) { + window.top.postMessage({time: window.perfTiming + 'ms', info}, '*'); + } +}; + +})(); From 4560df92d5b0e23ce909c4629b1f21b87cbc0d42 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Tue, 5 Sep 2017 12:07:22 -0700 Subject: [PATCH 3/4] Address reviewer feedback --- README.md | 22 +++++- node-perf-tester.js | 162 +++++++++++++++++++++++--------------------- options.json | 5 +- 3 files changed, 106 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 504abb3..c76f72b 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,24 @@ An element to help performance test other elements. The perf-tester element accepts an array of `tests` which are html files containing perf tests to run. A simple "runner.html" as is shown in `/demo` can be created to display output. -Each test file should load `perf.js` and call `console.perf()` to start the test and `console.perfEnd()` to finish it. \ No newline at end of file +Each test file should load `perf.js` and call `console.perf()` to start the test and `console.perfEnd()` to finish it. + +### Node script + +Run the node-script with + + node node-perf-tester.js + +The available options are: + + --runs Number of runs to measure on + --targets The file location of a JSON-structured file with the following format: + + [ + ["", ""] + ] + + where type is the name of the type you are testing and the url the location of the target under test + + --port The port number to connect the protocol to. Use this when you are connecting to a separate device. If this argument is not provided, chrome-headless is used. + --throttling one of [FAST_3G, SLOW_3G] If not running in Chrome headless, you can enable throttling with one of the two options. diff --git a/node-perf-tester.js b/node-perf-tester.js index 03ce432..f67d69e 100644 --- a/node-perf-tester.js +++ b/node-perf-tester.js @@ -7,87 +7,93 @@ const argv = require('minimist')(process.argv.slice(2)); const NUMBER_OF_RUNS = argv.runs || 100; (async function() { - let port = argv.port; - let chrome; - - if (!argv.port) { - const chromeLauncher = require('chrome-launcher'); - chrome = await chromeLauncher.launch({ - chromeFlags: ['--headless', '--disable-gpu', '--remote-debugging-address=0.0.0.0'], - port: 0 - }); - port = chrome.port; + let port = argv.port; + let chrome; + + if (!argv.port) { + const chromeLauncher = require('chrome-launcher'); + chrome = await chromeLauncher.launch({ + chromeFlags: + ['--headless', '--disable-gpu', '--remote-debugging-address=0.0.0.0'], + port: 0 + }); + port = chrome.port; + } + + const tab = await CDP.New({port}); + const client = await CDP({tab, port}); + + const {Page, Network, Runtime} = client; + + const ONE_MB = 1024 * 1024 / 8; + const throttling = { + FAST_3G: { + downloadThroughput: 1.6 * ONE_MB * .9, + uploadThroughput: .75 * ONE_MB * .9 + }, + SLOW_3G: { + downloadThroughput: .5 * ONE_MB * .8, + uploadThroughput: .5 * ONE_MB * .8 } - - const tab = await CDP.New({port}); - const client = await CDP({tab, port}); - - const {Page, Network, Runtime} = client; - - const ONE_MB = 1024 * 1024 / 8; - const throttling = { - FAST_3G: { - downloadThroughput: 1.6 * ONE_MB * .9, - uploadThroughput: .75 * ONE_MB * .9 - }, - SLOW_3G: { - downloadThroughput: .5 * ONE_MB * .8, - uploadThroughput: .5 * ONE_MB * .8 - } + } + + await Promise.all([ + Page.enable(), + Network.enable(), + port && argv.throttling && + Network.emulateNetworkConditions(Object.assign( + {}, throttling[argv.throttling], + {offline: false, latency: 10})), + Network.clearBrowserCache(), + Network.setCacheDisabled({cacheDisabled: true}), + Network.setBypassServiceWorker({bypass: true}), + ]); + + let loadEventPromise; + + Page.loadEventFired(() => { + loadEventPromise(); + }); + + const options = require(path.join(process.cwd(), argv.targets)); + + const perfTimings = {}; + for (const [type] of options) { + perfTimings[type] = []; + } + + process.on('exit', async () => { + for (const [type, timings] of Object.entries(perfTimings)) { + const average = timings.reduce((a, b) => a + b) / timings.length; + console.log( + `Average gain for ${type} in ${timings.length} runs is ${average}`); } - await Promise.all([ - Page.enable(), - Network.enable(), - port && argv.throttling && Network.emulateNetworkConditions( - Object.assign({}, throttling[argv.throttling], { - offline: false, - latency: 10 - }) - ), - Network.clearBrowserCache(), - Network.setCacheDisabled({cacheDisabled: true}), - Network.setBypassServiceWorker({bypass: true}), - ]); - - let loadEventPromise; - - Page.loadEventFired(() => { - loadEventPromise(); - }); - - const options = require(path.join(process.cwd(), argv.targets)); - - const perfTimings = {}; - for (const [type] of options) { - perfTimings[type] = []; + await CDP.Close({port, id: tab.id}); + await client.close(); + if (!argv.port) { + await chrome.kill(); } - - process.on('exit', async() => { - for (const [type, timings] of Object.entries(perfTimings)) { - const average = timings.reduce((a, b) => a + b) / timings.length; - console.log(`Average gain for ${type} in ${timings.length} runs is ${average}`); - } - - await CDP.Close({port, id: tab.id}); - await client.close(); - if (!argv.port) { - await chrome.kill(); - } - }); - - for (let i = 0; i < NUMBER_OF_RUNS; i++) { - for (const [type, url] of options) { - requestType = type; - - Page.navigate({url}); - - await new Promise(resolve => { - loadEventPromise = resolve; - }); - const {result: {value: perfTiming}} = await Runtime.evaluate({expression: 'window.perfTiming'}); - perfTimings[type].push(perfTiming); - } - process.stdout.write(`${i + 1}/${NUMBER_OF_RUNS}\r`); + }); + + for (let i = 0; i < NUMBER_OF_RUNS; i++) { + for (const [type, url] of options) { + requestType = type; + + Page.navigate({url}); + + await new Promise(resolve => { + loadEventPromise = resolve; + }); + const {result: {value: perfTiming}} = + await Runtime.evaluate({expression: 'window.perfTiming'}); + // const {result: {value: perfTiming}} = await + // Runtime.evaluate({expression: 'window.performance.timing.loadEventEnd- + // window.performance.timing.navigationStart'}); + perfTimings[type].push(perfTiming); } + process.stdout.write(`${i + 1}/${NUMBER_OF_RUNS}\r`); + } + + process.exit(0); })() \ No newline at end of file diff --git a/options.json b/options.json index 2c1f787..0637a08 100644 --- a/options.json +++ b/options.json @@ -1,4 +1 @@ -[ - ["original", "http://localhost:8081/youtube/"], - ["effects", "http://localhost:8081/youtube/index-effects.html"] -] \ No newline at end of file +[] \ No newline at end of file From c76a52089d37e07277710bd9b8802c16c80b9137 Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Tue, 5 Sep 2017 12:10:33 -0700 Subject: [PATCH 4/4] Fix formatting --- node-perf-tester.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/node-perf-tester.js b/node-perf-tester.js index f67d69e..afe13df 100644 --- a/node-perf-tester.js +++ b/node-perf-tester.js @@ -35,19 +35,18 @@ const NUMBER_OF_RUNS = argv.runs || 100; downloadThroughput: .5 * ONE_MB * .8, uploadThroughput: .5 * ONE_MB * .8 } - } - - await Promise.all([ - Page.enable(), - Network.enable(), - port && argv.throttling && - Network.emulateNetworkConditions(Object.assign( - {}, throttling[argv.throttling], - {offline: false, latency: 10})), - Network.clearBrowserCache(), - Network.setCacheDisabled({cacheDisabled: true}), - Network.setBypassServiceWorker({bypass: true}), - ]); + }; + + await Promise.all([ + Page.enable(), + Network.enable(), + port && argv.throttling && + Network.emulateNetworkConditions(Object.assign( + {}, throttling[argv.throttling], {offline: false, latency: 10})), + Network.clearBrowserCache(), + Network.setCacheDisabled({cacheDisabled: true}), + Network.setBypassServiceWorker({bypass: true}), + ]); let loadEventPromise;