UNPKG

15.4 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
7 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
8 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
9 * Google as part of the polymer project is also subject to an additional IP
10 * rights grant found at http://polymer.github.io/PATENTS.txt
11 */
12var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13 if (k2 === undefined) k2 = k;
14 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
15}) : (function(o, m, k, k2) {
16 if (k2 === undefined) k2 = k;
17 o[k2] = m[k];
18}));
19var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20 Object.defineProperty(o, "default", { enumerable: true, value: v });
21}) : function(o, v) {
22 o["default"] = v;
23});
24var __importStar = (this && this.__importStar) || function (mod) {
25 if (mod && mod.__esModule) return mod;
26 var result = {};
27 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
28 __setModuleDefault(result, mod);
29 return result;
30};
31Object.defineProperty(exports, "__esModule", { value: true });
32exports.Runner = void 0;
33const fsExtra = __importStar(require("fs-extra"));
34const ProgressBar = require("progress");
35const ansi = require("ansi-escape-sequences");
36const json_output_1 = require("./json-output");
37const browser_1 = require("./browser");
38const measure_1 = require("./measure");
39const csv_1 = require("./csv");
40const stats_1 = require("./stats");
41const format_1 = require("./format");
42const github = __importStar(require("./github"));
43const specs_1 = require("./specs");
44const util_1 = require("./util");
45class Runner {
46 constructor(config, servers) {
47 this.browsers = new Map();
48 this.results = new Map();
49 /**
50 * How many times we will load a page and try to collect all measurements
51 * before fully failing.
52 */
53 this.maxAttempts = 3;
54 /**
55 * Maximum milliseconds we will wait for all measurements to be collected per
56 * attempt before reloading and trying a new attempt.
57 */
58 this.attemptTimeout = 10000;
59 /**
60 * How many milliseconds we will wait between each poll for measurements.
61 */
62 this.pollTime = 50;
63 this.hitTimeout = false;
64 this.config = config;
65 this.specs = config.benchmarks;
66 this.servers = servers;
67 this.bar = new ProgressBar('[:bar] :status', {
68 total: this.specs.length * (config.sampleSize + /** warmup */ 1),
69 width: 58,
70 });
71 }
72 async run() {
73 await this.launchBrowsers();
74 if (this.config.githubCheck !== undefined) {
75 this.completeGithubCheck =
76 await github.createCheck(this.config.githubCheck);
77 }
78 console.log('Running benchmarks\n');
79 await this.warmup();
80 await this.takeMinimumSamples();
81 await this.takeAdditionalSamples();
82 await this.closeBrowsers();
83 const results = this.makeResults();
84 await this.outputResults(results);
85 return results;
86 }
87 async launchBrowsers() {
88 for (const { browser } of this.specs) {
89 const sig = browser_1.browserSignature(browser);
90 if (this.browsers.has(sig)) {
91 continue;
92 }
93 this.bar.tick(0, { status: `launching ${browser.name}` });
94 // It's important that we execute each benchmark iteration in a new tab.
95 // At least in Chrome, each tab corresponds to process which shares some
96 // amount of cached V8 state which can cause significant measurement
97 // effects. There might even be additional interaction effects that
98 // would require an entirely new browser to remove, but experience in
99 // Chrome so far shows that new tabs are neccessary and sufficient.
100 const driver = await browser_1.makeDriver(browser);
101 const tabs = await driver.getAllWindowHandles();
102 // We'll always launch new tabs from this initial blank tab.
103 const initialTabHandle = tabs[0];
104 this.browsers.set(sig, { name: browser.name, driver, initialTabHandle });
105 }
106 }
107 async closeBrowsers() {
108 // Close the browsers by closing each of their last remaining tabs.
109 await Promise.all([...this.browsers.values()].map(({ driver }) => driver.close()));
110 }
111 /**
112 * Do one throw-away run per benchmark to warm up our server (especially
113 * when expensive bare module resolution is enabled), and the browser.
114 */
115 async warmup() {
116 const { specs, bar } = this;
117 for (let i = 0; i < specs.length; i++) {
118 const spec = specs[i];
119 bar.tick(0, {
120 status: `warmup ${i + 1}/${specs.length} ${format_1.benchmarkOneLiner(spec)}`,
121 });
122 await this.takeSamples(spec);
123 bar.tick(1);
124 }
125 }
126 recordSamples(spec, newResults) {
127 let specResults = this.results.get(spec);
128 if (specResults === undefined) {
129 specResults = [];
130 this.results.set(spec, specResults);
131 }
132 // This function is called once per page per sample. The first time this
133 // function is called for a page, that result object becomes our "primary"
134 // one. On subsequent calls, we accrete the additional sample data into this
135 // primary one. The other fields are always the same, so we can just ignore
136 // them after the first call.
137 // TODO(aomarks) The other fields (user agent, bytes sent, etc.) only need
138 // to be collected on the first run of each page, so we could do that in the
139 // warmup phase, and then function would only need to take sample data,
140 // since it's a bit confusing how we throw away a bunch of fields after the
141 // first call.
142 for (const newResult of newResults) {
143 const primary = specResults[newResult.measurementIndex];
144 if (primary === undefined) {
145 specResults[newResult.measurementIndex] = newResult;
146 }
147 else {
148 primary.millis.push(...newResult.millis);
149 }
150 }
151 }
152 async takeMinimumSamples() {
153 // Always collect our minimum number of samples.
154 const { config, specs, bar } = this;
155 const numRuns = specs.length * config.sampleSize;
156 let run = 0;
157 for (let sample = 0; sample < config.sampleSize; sample++) {
158 for (const spec of specs) {
159 bar.tick(0, {
160 status: `${++run}/${numRuns} ${format_1.benchmarkOneLiner(spec)}`,
161 });
162 this.recordSamples(spec, await this.takeSamples(spec));
163 if (bar.curr === bar.total - 1) {
164 // Note if we tick with 0 after we've completed, the status is
165 // rendered on the next line for some reason.
166 bar.tick(1, { status: 'done' });
167 }
168 else {
169 bar.tick(1);
170 }
171 }
172 }
173 }
174 async takeAdditionalSamples() {
175 const { config, specs } = this;
176 if (config.timeout <= 0) {
177 return;
178 }
179 console.log();
180 const timeoutMs = config.timeout * 60 * 1000; // minutes -> millis
181 const startMs = Date.now();
182 let run = 0;
183 let sample = 0;
184 let elapsed = 0;
185 while (true) {
186 if (stats_1.horizonsResolved(this.makeResults(), config.horizons)) {
187 console.log();
188 break;
189 }
190 if (elapsed >= timeoutMs) {
191 this.hitTimeout = true;
192 break;
193 }
194 // Run batches of 10 additional samples at a time for more presentable
195 // sample sizes, and to nudge sample sizes up a little.
196 for (let i = 0; i < 10; i++) {
197 sample++;
198 for (const spec of specs) {
199 run++;
200 elapsed = Date.now() - startMs;
201 const remainingSecs = Math.max(0, Math.round((timeoutMs - elapsed) / 1000));
202 const mins = Math.floor(remainingSecs / 60);
203 const secs = remainingSecs % 60;
204 process.stdout.write(`\r${format_1.spinner[run % format_1.spinner.length]} Auto-sample ${sample} ` +
205 `(timeout in ${mins}m${secs}s)` + ansi.erase.inLine(0));
206 this.recordSamples(spec, await this.takeSamples(spec));
207 }
208 }
209 }
210 }
211 async takeSamples(spec) {
212 const { servers, config, browsers } = this;
213 let server;
214 if (spec.url.kind === 'local') {
215 server = servers.get(spec);
216 if (server === undefined) {
217 throw new Error('Internal error: no server for spec');
218 }
219 }
220 const url = specs_1.specUrl(spec, servers, config);
221 const { driver, initialTabHandle } = browsers.get(browser_1.browserSignature(spec.browser));
222 let session;
223 let pendingMeasurements;
224 let measurementResults;
225 // We'll try N attempts per page. Within each attempt, we'll try to collect
226 // all of the measurements by polling. If we hit our per-attempt timeout
227 // before collecting all measurements, we'll move onto the next attempt
228 // where we reload the whole page and start from scratch. If we hit our max
229 // attempts, we'll throw.
230 for (let pageAttempt = 1;; pageAttempt++) {
231 // New attempt. Reset all measurements and results.
232 pendingMeasurements = new Set(spec.measurement);
233 measurementResults = [];
234 await browser_1.openAndSwitchToNewTab(driver, spec.browser);
235 await driver.get(url);
236 for (let waited = 0; pendingMeasurements.size > 0 && waited <= this.attemptTimeout; waited += this.pollTime) {
237 // TODO(aomarks) You don't have to wait in callback mode!
238 await util_1.wait(this.pollTime);
239 for (let measurementIndex = 0; measurementIndex < spec.measurement.length; measurementIndex++) {
240 if (measurementResults[measurementIndex] !== undefined) {
241 // Already collected this measurement on this attempt.
242 continue;
243 }
244 const measurement = spec.measurement[measurementIndex];
245 const result = await measure_1.measure(driver, measurement, server);
246 if (result !== undefined) {
247 measurementResults[measurementIndex] = result;
248 pendingMeasurements.delete(measurement);
249 }
250 }
251 }
252 // Close the active tab (but not the whole browser, since the
253 // initial blank tab is still open).
254 await driver.close();
255 await driver.switchTo().window(initialTabHandle);
256 if (server !== undefined) {
257 session = server.endSession();
258 }
259 if (pendingMeasurements.size === 0 || pageAttempt >= this.maxAttempts) {
260 break;
261 }
262 console.log(`\n\nFailed ${pageAttempt}/${this.maxAttempts} times ` +
263 `to get measurement(s) ${spec.name}` +
264 (spec.measurement.length > 1 ? ` [${[...pendingMeasurements]
265 .map(measure_1.measurementName)
266 .join(', ')}]` :
267 '') +
268 ` in ${spec.browser.name} from ${url}. Retrying.`);
269 }
270 if (pendingMeasurements.size > 0) {
271 console.log();
272 throw new Error(`\n\nFailed ${this.maxAttempts}/${this.maxAttempts} times ` +
273 `to get measurement(s) ${spec.name}` +
274 (spec.measurement.length > 1 ? ` [${[...pendingMeasurements]
275 .map(measure_1.measurementName)
276 .join(', ')}]` :
277 '') +
278 ` in ${spec.browser.name} from ${url}`);
279 }
280 return spec.measurement.map((measurement, measurementIndex) => ({
281 name: spec.measurement.length === 1 ?
282 spec.name :
283 `${spec.name} [${measure_1.measurementName(measurement)}]`,
284 measurement,
285 measurementIndex: measurementIndex,
286 queryString: spec.url.kind === 'local' ? spec.url.queryString : '',
287 version: spec.url.kind === 'local' && spec.url.version !== undefined ?
288 spec.url.version.label :
289 '',
290 millis: [measurementResults[measurementIndex]],
291 bytesSent: session ? session.bytesSent : 0,
292 browser: spec.browser,
293 userAgent: session ? session.userAgent : '',
294 }));
295 }
296 makeResults() {
297 const resultStats = [];
298 for (const results of this.results.values()) {
299 for (let r = 0; r < results.length; r++) {
300 const result = results[r];
301 resultStats.push({ result, stats: stats_1.summaryStats(result.millis) });
302 }
303 }
304 return stats_1.computeDifferences(resultStats);
305 }
306 async outputResults(withDifferences) {
307 const { config, hitTimeout } = this;
308 console.log();
309 const { fixed, unfixed } = format_1.automaticResultTable(withDifferences);
310 console.log(format_1.horizontalTermResultTable(fixed));
311 console.log(format_1.verticalTermResultTable(unfixed));
312 if (hitTimeout === true) {
313 console.log(ansi.format(`[bold red]{NOTE} Hit ${config.timeout} minute auto-sample timeout` +
314 ` trying to resolve horizon(s)`));
315 console.log('Consider a longer --timeout or different --horizon');
316 }
317 if (config.jsonFile) {
318 const json = await json_output_1.jsonOutput(withDifferences);
319 await fsExtra.writeJSON(config.jsonFile, json, { spaces: 2 });
320 }
321 // TOOD(aomarks) Remove this in next major version.
322 if (config.legacyJsonFile) {
323 const json = await json_output_1.legacyJsonOutput(withDifferences.map((s) => s.result));
324 await fsExtra.writeJSON(config.legacyJsonFile, json);
325 }
326 if (config.csvFileStats) {
327 await fsExtra.writeFile(config.csvFileStats, csv_1.formatCsvStats(withDifferences));
328 }
329 if (config.csvFileRaw) {
330 await fsExtra.writeFile(config.csvFileRaw, csv_1.formatCsvRaw(withDifferences));
331 }
332 if (this.completeGithubCheck !== undefined) {
333 const markdown = format_1.horizontalHtmlResultTable(fixed) + '\n' +
334 format_1.verticalHtmlResultTable(unfixed);
335 await this.completeGithubCheck(markdown);
336 }
337 }
338}
339exports.Runner = Runner;
340//# sourceMappingURL=runner.js.map
\No newline at end of file