UNPKG

@angular/benchpress

Version:

Benchpress - a framework for e2e performance tests

1 lines 121 kB
{"version":3,"file":"benchpress.mjs","sources":["../../../../../../packages/benchpress/src/common_options.ts","../../../../../../packages/benchpress/src/measure_values.ts","../../../../../../packages/benchpress/src/metric.ts","../../../../../../packages/benchpress/src/metric/multi_metric.ts","../../../../../../packages/benchpress/src/web_driver_extension.ts","../../../../../../packages/benchpress/src/metric/perflog_metric.ts","../../../../../../packages/benchpress/src/web_driver_adapter.ts","../../../../../../packages/benchpress/src/metric/user_metric.ts","../../../../../../packages/benchpress/src/reporter.ts","../../../../../../packages/benchpress/src/validator.ts","../../../../../../packages/benchpress/src/sample_description.ts","../../../../../../packages/benchpress/src/statistic.ts","../../../../../../packages/benchpress/src/reporter/util.ts","../../../../../../packages/benchpress/src/reporter/console_reporter.ts","../../../../../../packages/benchpress/src/reporter/json_file_reporter.ts","../../../../../../packages/benchpress/src/reporter/multi_reporter.ts","../../../../../../packages/benchpress/src/sampler.ts","../../../../../../packages/benchpress/src/validator/regression_slope_validator.ts","../../../../../../packages/benchpress/src/validator/size_validator.ts","../../../../../../packages/benchpress/src/webdriver/chrome_driver_extension.ts","../../../../../../packages/benchpress/src/webdriver/firefox_driver_extension.ts","../../../../../../packages/benchpress/src/webdriver/ios_driver_extension.ts","../../../../../../packages/benchpress/src/runner.ts","../../../../../../packages/benchpress/src/webdriver/selenium_webdriver_adapter.ts","../../../../../../packages/benchpress/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {InjectionToken} from '@angular/core';\nimport * as fs from 'fs';\n\nexport class Options {\n static SAMPLE_ID = new InjectionToken('Options.sampleId');\n static DEFAULT_DESCRIPTION = new InjectionToken('Options.defaultDescription');\n static SAMPLE_DESCRIPTION = new InjectionToken('Options.sampleDescription');\n static FORCE_GC = new InjectionToken('Options.forceGc');\n static NO_PREPARE = () => true;\n static PREPARE = new InjectionToken('Options.prepare');\n static EXECUTE = new InjectionToken('Options.execute');\n static CAPABILITIES = new InjectionToken('Options.capabilities');\n static USER_AGENT = new InjectionToken('Options.userAgent');\n static MICRO_METRICS = new InjectionToken('Options.microMetrics');\n static USER_METRICS = new InjectionToken('Options.userMetrics');\n static NOW = new InjectionToken('Options.now');\n static WRITE_FILE = new InjectionToken('Options.writeFile');\n static RECEIVED_DATA = new InjectionToken('Options.receivedData');\n static REQUEST_COUNT = new InjectionToken('Options.requestCount');\n static CAPTURE_FRAMES = new InjectionToken('Options.frameCapture');\n static RAW_PERFLOG_PATH = new InjectionToken('Options.rawPerflogPath');\n static DEFAULT_PROVIDERS = [\n {provide: Options.DEFAULT_DESCRIPTION, useValue: {}},\n {provide: Options.SAMPLE_DESCRIPTION, useValue: {}},\n {provide: Options.FORCE_GC, useValue: false},\n {provide: Options.PREPARE, useValue: Options.NO_PREPARE},\n {provide: Options.MICRO_METRICS, useValue: {}}, {provide: Options.USER_METRICS, useValue: {}},\n {provide: Options.NOW, useValue: () => new Date()},\n {provide: Options.RECEIVED_DATA, useValue: false},\n {provide: Options.REQUEST_COUNT, useValue: false},\n {provide: Options.CAPTURE_FRAMES, useValue: false},\n {provide: Options.WRITE_FILE, useValue: writeFile},\n {provide: Options.RAW_PERFLOG_PATH, useValue: null}\n ];\n}\n\nfunction writeFile(filename: string, content: string): Promise<any> {\n return new Promise<void>(function(resolve, reject) {\n fs.writeFile(filename, content, (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nexport class MeasureValues {\n constructor(\n public runIndex: number, public timeStamp: Date, public values: {[key: string]: any}) {}\n\n toJson() {\n return {\n 'timeStamp': this.timeStamp.toJSON(),\n 'runIndex': this.runIndex,\n 'values': this.values,\n };\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\n\n/**\n * A metric is measures values\n */\nexport abstract class Metric {\n /**\n * Starts measuring\n */\n beginMeasure(): Promise<any> {\n throw new Error('NYI');\n }\n\n /**\n * Ends measuring and reports the data\n * since the begin call.\n * @param restart: Whether to restart right after this.\n */\n endMeasure(restart: boolean): Promise<{[key: string]: any}> {\n throw new Error('NYI');\n }\n\n /**\n * Describes the metrics provided by this metric implementation.\n * (e.g. units, ...)\n */\n describe(): {[key: string]: string} {\n throw new Error('NYI');\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {InjectionToken, Injector} from '@angular/core';\n\nimport {Metric} from '../metric';\n\nexport class MultiMetric extends Metric {\n static provideWith(childTokens: any[]): any[] {\n return [\n {\n provide: _CHILDREN,\n useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),\n deps: [Injector]\n },\n {\n provide: MultiMetric,\n useFactory: (children: Metric[]) => new MultiMetric(children),\n deps: [_CHILDREN]\n }\n ];\n }\n\n constructor(private _metrics: Metric[]) {\n super();\n }\n\n /**\n * Starts measuring\n */\n override beginMeasure(): Promise<any> {\n return Promise.all(this._metrics.map(metric => metric.beginMeasure()));\n }\n\n /**\n * Ends measuring and reports the data\n * since the begin call.\n * @param restart: Whether to restart right after this.\n */\n override endMeasure(restart: boolean): Promise<{[key: string]: any}> {\n return Promise.all(this._metrics.map(metric => metric.endMeasure(restart)))\n .then(values => mergeStringMaps(<any>values));\n }\n\n /**\n * Describes the metrics provided by this metric implementation.\n * (e.g. units, ...)\n */\n override describe(): {[key: string]: any} {\n return mergeStringMaps(this._metrics.map((metric) => metric.describe()));\n }\n}\n\nfunction mergeStringMaps(maps: {[key: string]: string}[]): {[key: string]: string} {\n const result: {[key: string]: string} = {};\n maps.forEach(map => {\n Object.keys(map).forEach(prop => {\n result[prop] = map[prop];\n });\n });\n return result;\n}\n\nconst _CHILDREN = new InjectionToken('MultiMetric.children');\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {InjectionToken, Injector} from '@angular/core';\n\nimport {Options} from './common_options';\n\nexport type PerfLogEvent = {\n [key: string]: any\n}&{\n ph?: 'X' | 'B' | 'E' | 'I',\n ts?: number,\n dur?: number,\n name?: string,\n pid?: string,\n args?: {\n encodedDataLength?: number,\n usedHeapSize?: number,\n majorGc?: boolean,\n url?: string,\n method?: string\n }\n};\n\n/**\n * A WebDriverExtension implements extended commands of the webdriver protocol\n * for a given browser, independent of the WebDriverAdapter.\n * Needs one implementation for every supported Browser.\n */\nexport abstract class WebDriverExtension {\n static provideFirstSupported(childTokens: any[]): any[] {\n const res = [\n {\n provide: _CHILDREN,\n useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),\n deps: [Injector]\n },\n {\n provide: WebDriverExtension,\n useFactory: (children: WebDriverExtension[], capabilities: {[key: string]: any}) => {\n let delegate: WebDriverExtension = undefined!;\n children.forEach(extension => {\n if (extension.supports(capabilities)) {\n delegate = extension;\n }\n });\n if (!delegate) {\n throw new Error('Could not find a delegate for given capabilities!');\n }\n return delegate;\n },\n deps: [_CHILDREN, Options.CAPABILITIES]\n }\n ];\n return res;\n }\n\n gc(): Promise<any> {\n throw new Error('NYI');\n }\n\n timeBegin(name: string): Promise<any> {\n throw new Error('NYI');\n }\n\n timeEnd(name: string, restartName: string|null): Promise<any> {\n throw new Error('NYI');\n }\n\n /**\n * Format:\n * - cat: category of the event\n * - name: event name: 'script', 'gc', 'render', ...\n * - ph: phase: 'B' (begin), 'E' (end), 'X' (Complete event), 'I' (Instant event)\n * - ts: timestamp in ms, e.g. 12345\n * - pid: process id\n * - args: arguments, e.g. {heapSize: 1234}\n *\n * Based on [Chrome Trace Event\n *Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit)\n **/\n readPerfLog(): Promise<PerfLogEvent[]> {\n throw new Error('NYI');\n }\n\n perfLogFeatures(): PerfLogFeatures {\n throw new Error('NYI');\n }\n\n supports(capabilities: {[key: string]: any}): boolean {\n return true;\n }\n}\n\nexport class PerfLogFeatures {\n render: boolean;\n gc: boolean;\n frameCapture: boolean;\n userTiming: boolean;\n\n constructor(\n {render = false, gc = false, frameCapture = false, userTiming = false}:\n {render?: boolean, gc?: boolean, frameCapture?: boolean, userTiming?: boolean} = {}) {\n this.render = render;\n this.gc = gc;\n this.frameCapture = frameCapture;\n this.userTiming = userTiming;\n }\n}\n\nconst _CHILDREN = new InjectionToken('WebDriverExtension.children');\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, InjectionToken} from '@angular/core';\n\nimport {Options} from '../common_options';\nimport {Metric} from '../metric';\nimport {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';\n\n\n/**\n * A metric that reads out the performance log\n */\n@Injectable()\nexport class PerflogMetric extends Metric {\n static SET_TIMEOUT = new InjectionToken('PerflogMetric.setTimeout');\n static IGNORE_NAVIGATION = new InjectionToken('PerflogMetric.ignoreNavigation');\n static PROVIDERS = [\n {\n provide: PerflogMetric,\n deps:\n [\n WebDriverExtension, PerflogMetric.SET_TIMEOUT, Options.MICRO_METRICS, Options.FORCE_GC,\n Options.CAPTURE_FRAMES, Options.RECEIVED_DATA, Options.REQUEST_COUNT,\n PerflogMetric.IGNORE_NAVIGATION\n ]\n },\n {\n provide: PerflogMetric.SET_TIMEOUT,\n useValue: (fn: Function, millis: number) => <any>setTimeout(fn, millis)\n },\n {provide: PerflogMetric.IGNORE_NAVIGATION, useValue: false}\n ];\n\n private _remainingEvents: PerfLogEvent[];\n private _measureCount: number;\n private _perfLogFeatures: PerfLogFeatures;\n\n /**\n * @param driverExtension\n * @param setTimeout\n * @param microMetrics Name and description of metrics provided via console.time / console.timeEnd\n * @param ignoreNavigation If true, don't measure from navigationStart events. These events are\n * usually triggered by a page load, but can also be triggered when adding iframes to the DOM.\n **/\n constructor(\n private _driverExtension: WebDriverExtension,\n @Inject(PerflogMetric.SET_TIMEOUT) private _setTimeout: Function,\n @Inject(Options.MICRO_METRICS) private _microMetrics: {[key: string]: string},\n @Inject(Options.FORCE_GC) private _forceGc: boolean,\n @Inject(Options.CAPTURE_FRAMES) private _captureFrames: boolean,\n @Inject(Options.RECEIVED_DATA) private _receivedData: boolean,\n @Inject(Options.REQUEST_COUNT) private _requestCount: boolean,\n @Inject(PerflogMetric.IGNORE_NAVIGATION) private _ignoreNavigation: boolean) {\n super();\n\n this._remainingEvents = [];\n this._measureCount = 0;\n this._perfLogFeatures = _driverExtension.perfLogFeatures();\n if (!this._perfLogFeatures.userTiming) {\n // User timing is needed for navigationStart.\n this._receivedData = false;\n this._requestCount = false;\n }\n }\n\n override describe(): {[key: string]: string} {\n const res: {[key: string]: any} = {\n 'scriptTime': 'script execution time in ms, including gc and render',\n 'pureScriptTime': 'script execution time in ms, without gc nor render'\n };\n if (this._perfLogFeatures.render) {\n res['renderTime'] = 'render time in ms';\n }\n if (this._perfLogFeatures.gc) {\n res['gcTime'] = 'gc time in ms';\n res['gcAmount'] = 'gc amount in kbytes';\n res['majorGcTime'] = 'time of major gcs in ms';\n if (this._forceGc) {\n res['forcedGcTime'] = 'forced gc time in ms';\n res['forcedGcAmount'] = 'forced gc amount in kbytes';\n }\n }\n if (this._receivedData) {\n res['receivedData'] = 'encoded bytes received since navigationStart';\n }\n if (this._requestCount) {\n res['requestCount'] = 'count of requests sent since navigationStart';\n }\n if (this._captureFrames) {\n if (!this._perfLogFeatures.frameCapture) {\n const warningMsg = 'WARNING: Metric requested, but not supported by driver';\n // using dot syntax for metric name to keep them grouped together in console reporter\n res['frameTime.mean'] = warningMsg;\n res['frameTime.worst'] = warningMsg;\n res['frameTime.best'] = warningMsg;\n res['frameTime.smooth'] = warningMsg;\n } else {\n res['frameTime.mean'] = 'mean frame time in ms (target: 16.6ms for 60fps)';\n res['frameTime.worst'] = 'worst frame time in ms';\n res['frameTime.best'] = 'best frame time in ms';\n res['frameTime.smooth'] = 'percentage of frames that hit 60fps';\n }\n }\n for (const name in this._microMetrics) {\n res[name] = this._microMetrics[name];\n }\n return res;\n }\n\n override beginMeasure(): Promise<any> {\n let resultPromise = Promise.resolve(null);\n if (this._forceGc) {\n resultPromise = resultPromise.then((_) => this._driverExtension.gc());\n }\n return resultPromise.then((_) => this._beginMeasure());\n }\n\n override endMeasure(restart: boolean): Promise<{[key: string]: number}> {\n if (this._forceGc) {\n return this._endPlainMeasureAndMeasureForceGc(restart);\n } else {\n return this._endMeasure(restart);\n }\n }\n\n /** @internal */\n private _endPlainMeasureAndMeasureForceGc(restartMeasure: boolean) {\n return this._endMeasure(true).then((measureValues) => {\n // disable frame capture for measurements during forced gc\n const originalFrameCaptureValue = this._captureFrames;\n this._captureFrames = false;\n return this._driverExtension.gc()\n .then((_) => this._endMeasure(restartMeasure))\n .then((forceGcMeasureValues) => {\n this._captureFrames = originalFrameCaptureValue;\n measureValues['forcedGcTime'] = forceGcMeasureValues['gcTime'];\n measureValues['forcedGcAmount'] = forceGcMeasureValues['gcAmount'];\n return measureValues;\n });\n });\n }\n\n private _beginMeasure(): Promise<any> {\n return this._driverExtension.timeBegin(this._markName(this._measureCount++));\n }\n\n private _endMeasure(restart: boolean): Promise<{[key: string]: number}> {\n const markName = this._markName(this._measureCount - 1);\n const nextMarkName = restart ? this._markName(this._measureCount++) : null;\n return this._driverExtension.timeEnd(markName, nextMarkName)\n .then((_: any) => this._readUntilEndMark(markName));\n }\n\n private _readUntilEndMark(\n markName: string, loopCount: number = 0, startEvent: PerfLogEvent|null = null) {\n if (loopCount > _MAX_RETRY_COUNT) {\n throw new Error(`Tried too often to get the ending mark: ${loopCount}`);\n }\n return this._driverExtension.readPerfLog().then((events) => {\n this._addEvents(events);\n const result = this._aggregateEvents(this._remainingEvents, markName);\n if (result) {\n this._remainingEvents = events;\n return result;\n }\n let resolve: (result: any) => void;\n const promise = new Promise<{[key: string]: number}>(res => {\n resolve = res;\n });\n this._setTimeout(() => resolve(this._readUntilEndMark(markName, loopCount + 1)), 100);\n return promise;\n });\n }\n\n private _addEvents(events: PerfLogEvent[]) {\n let needSort = false;\n events.forEach(event => {\n if (event['ph'] === 'X') {\n needSort = true;\n const startEvent: PerfLogEvent = {};\n const endEvent: PerfLogEvent = {};\n for (const prop in event) {\n startEvent[prop] = event[prop];\n endEvent[prop] = event[prop];\n }\n startEvent['ph'] = 'B';\n endEvent['ph'] = 'E';\n endEvent['ts'] = startEvent['ts']! + startEvent['dur']!;\n this._remainingEvents.push(startEvent);\n this._remainingEvents.push(endEvent);\n } else {\n this._remainingEvents.push(event);\n }\n });\n if (needSort) {\n // Need to sort because of the ph==='X' events\n this._remainingEvents.sort((a, b) => {\n const diff = a['ts']! - b['ts']!;\n return diff > 0 ? 1 : diff < 0 ? -1 : 0;\n });\n }\n }\n\n private _aggregateEvents(events: PerfLogEvent[], markName: string): {[key: string]: number}|null {\n const result: {[key: string]: number} = {'scriptTime': 0, 'pureScriptTime': 0};\n if (this._perfLogFeatures.gc) {\n result['gcTime'] = 0;\n result['majorGcTime'] = 0;\n result['gcAmount'] = 0;\n }\n if (this._perfLogFeatures.render) {\n result['renderTime'] = 0;\n }\n if (this._captureFrames) {\n result['frameTime.mean'] = 0;\n result['frameTime.best'] = 0;\n result['frameTime.worst'] = 0;\n result['frameTime.smooth'] = 0;\n }\n for (const name in this._microMetrics) {\n result[name] = 0;\n }\n if (this._receivedData) {\n result['receivedData'] = 0;\n }\n if (this._requestCount) {\n result['requestCount'] = 0;\n }\n\n let markStartEvent: PerfLogEvent = null!;\n let markEndEvent: PerfLogEvent = null!;\n events.forEach((event) => {\n const ph = event['ph'];\n const name = event['name'];\n\n // Here we are determining if this is the event signaling the start or end of our performance\n // testing (this is triggered by us calling #timeBegin and #timeEnd).\n //\n // Previously, this was done by checking that the event name matched our mark name and that\n // the phase was either \"B\" or \"E\" (\"begin\" or \"end\"). However, since Chrome v90 this is\n // showing up as \"-bpstart\" and \"-bpend\" (\"benchpress start/end\"), which is what one would\n // actually expect since that is the mark name used in ChromeDriverExtension - see the\n // #timeBegin and #timeEnd implementations in chrome_driver_extension.ts. For\n // backwards-compatibility with Chrome v89 (and older), we do both checks: the phase-based\n // one (\"B\" or \"E\") and event name-based (the \"-bp(start/end)\" suffix).\n const isStartEvent = (ph === 'B' && name === markName) || name === markName + '-bpstart';\n const isEndEvent = (ph === 'E' && name === markName) || name === markName + '-bpend';\n if (isStartEvent) {\n markStartEvent = event;\n } else if (ph === 'I' && name === 'navigationStart' && !this._ignoreNavigation) {\n // if a benchmark measures reload of a page, use the last\n // navigationStart as begin event\n markStartEvent = event;\n } else if (isEndEvent) {\n markEndEvent = event;\n }\n });\n if (!markStartEvent || !markEndEvent) {\n // not all events have been received, no further processing for now\n return null;\n }\n if (markStartEvent.pid !== markEndEvent.pid) {\n result['invalid'] = 1;\n }\n\n let gcTimeInScript = 0;\n let renderTimeInScript = 0;\n\n const frameTimestamps: number[] = [];\n const frameTimes: number[] = [];\n let frameCaptureStartEvent: PerfLogEvent|null = null;\n let frameCaptureEndEvent: PerfLogEvent|null = null;\n\n const intervalStarts: {[key: string]: PerfLogEvent} = {};\n const intervalStartCount: {[key: string]: number} = {};\n\n let inMeasureRange = false;\n events.forEach((event) => {\n const ph = event['ph'];\n let name = event['name']!;\n let microIterations = 1;\n const microIterationsMatch = name.match(_MICRO_ITERATIONS_REGEX);\n if (microIterationsMatch) {\n name = microIterationsMatch[1];\n microIterations = parseInt(microIterationsMatch[2], 10);\n }\n if (event === markStartEvent) {\n inMeasureRange = true;\n } else if (event === markEndEvent) {\n inMeasureRange = false;\n }\n if (!inMeasureRange || event['pid'] !== markStartEvent['pid']) {\n return;\n }\n\n if (this._requestCount && name === 'sendRequest') {\n result['requestCount'] += 1;\n } else if (this._receivedData && name === 'receivedData' && ph === 'I') {\n result['receivedData'] += event['args']!['encodedDataLength']!;\n }\n if (ph === 'B' && name === _MARK_NAME_FRAME_CAPTURE) {\n if (frameCaptureStartEvent) {\n throw new Error('can capture frames only once per benchmark run');\n }\n if (!this._captureFrames) {\n throw new Error(\n 'found start event for frame capture, but frame capture was not requested in benchpress');\n }\n frameCaptureStartEvent = event;\n } else if (ph === 'E' && name === _MARK_NAME_FRAME_CAPTURE) {\n if (!frameCaptureStartEvent) {\n throw new Error('missing start event for frame capture');\n }\n frameCaptureEndEvent = event;\n }\n\n if (ph === 'I' && frameCaptureStartEvent && !frameCaptureEndEvent && name === 'frame') {\n frameTimestamps.push(event['ts']!);\n if (frameTimestamps.length >= 2) {\n frameTimes.push(\n frameTimestamps[frameTimestamps.length - 1] -\n frameTimestamps[frameTimestamps.length - 2]);\n }\n }\n\n if (ph === 'B') {\n if (!intervalStarts[name]) {\n intervalStartCount[name] = 1;\n intervalStarts[name] = event;\n } else {\n intervalStartCount[name]++;\n }\n } else if ((ph === 'E') && intervalStarts[name]) {\n intervalStartCount[name]--;\n if (intervalStartCount[name] === 0) {\n const startEvent = intervalStarts[name];\n const duration = (event['ts']! - startEvent['ts']!);\n intervalStarts[name] = null!;\n if (name === 'gc') {\n result['gcTime'] += duration;\n const amount =\n (startEvent['args']!['usedHeapSize']! - event['args']!['usedHeapSize']!) / 1000;\n result['gcAmount'] += amount;\n const majorGc = event['args']!['majorGc'];\n if (majorGc && majorGc) {\n result['majorGcTime'] += duration;\n }\n if (intervalStarts['script']) {\n gcTimeInScript += duration;\n }\n } else if (name === 'render') {\n result['renderTime'] += duration;\n if (intervalStarts['script']) {\n renderTimeInScript += duration;\n }\n } else if (name === 'script') {\n result['scriptTime'] += duration;\n } else if (this._microMetrics[name]) {\n (<any>result)[name] += duration / microIterations;\n }\n }\n }\n });\n\n if (frameCaptureStartEvent && !frameCaptureEndEvent) {\n throw new Error('missing end event for frame capture');\n }\n if (this._captureFrames && !frameCaptureStartEvent) {\n throw new Error('frame capture requested in benchpress, but no start event was found');\n }\n if (frameTimes.length > 0) {\n this._addFrameMetrics(result, frameTimes);\n }\n result['pureScriptTime'] = result['scriptTime'] - gcTimeInScript - renderTimeInScript;\n return result;\n }\n\n private _addFrameMetrics(result: {[key: string]: number}, frameTimes: any[]) {\n result['frameTime.mean'] = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;\n const firstFrame = frameTimes[0];\n result['frameTime.worst'] = frameTimes.reduce((a, b) => a > b ? a : b, firstFrame);\n result['frameTime.best'] = frameTimes.reduce((a, b) => a < b ? a : b, firstFrame);\n result['frameTime.smooth'] =\n frameTimes.filter(t => t < _FRAME_TIME_SMOOTH_THRESHOLD).length / frameTimes.length;\n }\n\n private _markName(index: number) {\n return `${_MARK_NAME_PREFIX}${index}`;\n }\n}\n\nconst _MICRO_ITERATIONS_REGEX = /(.+)\\*(\\d+)$/;\n\nconst _MAX_RETRY_COUNT = 20;\nconst _MARK_NAME_PREFIX = 'benchpress';\n\nconst _MARK_NAME_FRAME_CAPTURE = 'frameCapture';\n// using 17ms as a somewhat looser threshold, instead of 16.6666ms\nconst _FRAME_TIME_SMOOTH_THRESHOLD = 17;\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\n\n\n/**\n * A WebDriverAdapter bridges API differences between different WebDriver clients,\n * e.g. JS vs Dart Async vs Dart Sync webdriver.\n * Needs one implementation for every supported WebDriver client.\n */\nexport abstract class WebDriverAdapter {\n waitFor(callback: Function): Promise<any> {\n throw new Error('NYI');\n }\n executeScript(script: string): Promise<any> {\n throw new Error('NYI');\n }\n executeAsyncScript(script: string): Promise<any> {\n throw new Error('NYI');\n }\n capabilities(): Promise<{[key: string]: any}> {\n throw new Error('NYI');\n }\n logs(type: string): Promise<any[]> {\n throw new Error('NYI');\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, StaticProvider} from '@angular/core';\n\nimport {Options} from '../common_options';\nimport {Metric} from '../metric';\nimport {WebDriverAdapter} from '../web_driver_adapter';\n\n@Injectable()\nexport class UserMetric extends Metric {\n static PROVIDERS =\n <StaticProvider[]>[{provide: UserMetric, deps: [Options.USER_METRICS, WebDriverAdapter]}];\n\n constructor(\n @Inject(Options.USER_METRICS) private _userMetrics: {[key: string]: string},\n private _wdAdapter: WebDriverAdapter) {\n super();\n }\n\n /**\n * Starts measuring\n */\n override beginMeasure(): Promise<any> {\n return Promise.resolve(true);\n }\n\n /**\n * Ends measuring.\n */\n override endMeasure(restart: boolean): Promise<{[key: string]: any}> {\n let resolve: (result: any) => void;\n let reject: (error: any) => void;\n const promise = new Promise<{[key: string]: any;}>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n const adapter = this._wdAdapter;\n const names = Object.keys(this._userMetrics);\n\n function getAndClearValues() {\n Promise.all(names.map(name => adapter.executeScript(`return window.${name}`)))\n .then((values: any[]) => {\n if (values.every(v => typeof v === 'number')) {\n Promise.all(names.map(name => adapter.executeScript(`delete window.${name}`)))\n .then((_: any[]) => {\n const map: {[k: string]: any} = {};\n for (let i = 0, n = names.length; i < n; i++) {\n map[names[i]] = values[i];\n }\n resolve(map);\n }, reject);\n } else {\n <any>setTimeout(getAndClearValues, 100);\n }\n }, reject);\n }\n getAndClearValues();\n return promise;\n }\n\n /**\n * Describes the metrics provided by this metric implementation.\n * (e.g. units, ...)\n */\n override describe(): {[key: string]: any} {\n return this._userMetrics;\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {MeasureValues} from './measure_values';\n\n/**\n * A reporter reports measure values and the valid sample.\n */\nexport abstract class Reporter {\n reportMeasureValues(values: MeasureValues): Promise<any> {\n throw new Error('NYI');\n }\n\n reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]): Promise<any> {\n throw new Error('NYI');\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {MeasureValues} from './measure_values';\n\n/**\n * A Validator calculates a valid sample out of the complete sample.\n * A valid sample is a sample that represents the population that should be observed\n * in the correct way.\n */\nexport abstract class Validator {\n /**\n * Calculates a valid sample out of the complete sample\n */\n validate(completeSample: MeasureValues[]): MeasureValues[]|null {\n throw new Error('NYI');\n }\n\n /**\n * Returns a Map that describes the properties of the validator\n * (e.g. sample size, ...)\n */\n describe(): {[key: string]: any} {\n throw new Error('NYI');\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {InjectionToken} from '@angular/core';\n\nimport {Options} from './common_options';\nimport {Metric} from './metric';\nimport {Validator} from './validator';\n\n\n/**\n * SampleDescription merges all available descriptions about a sample\n */\nexport class SampleDescription {\n static PROVIDERS = [{\n provide: SampleDescription,\n useFactory:\n (metric: Metric, id: string, forceGc: boolean, userAgent: string, validator: Validator,\n defaultDesc: {[key: string]: string}, userDesc: {[key: string]: string}) =>\n new SampleDescription(\n id,\n [\n {'forceGc': forceGc, 'userAgent': userAgent}, validator.describe(), defaultDesc,\n userDesc\n ],\n metric.describe()),\n deps:\n [\n Metric, Options.SAMPLE_ID, Options.FORCE_GC, Options.USER_AGENT, Validator,\n Options.DEFAULT_DESCRIPTION, Options.SAMPLE_DESCRIPTION\n ]\n }];\n description: {[key: string]: any};\n\n constructor(\n public id: string, descriptions: Array<{[key: string]: any}>,\n public metrics: {[key: string]: any}) {\n this.description = {};\n descriptions.forEach(description => {\n Object.keys(description).forEach(prop => {\n this.description[prop] = description[prop];\n });\n });\n }\n\n toJson() {\n return {'id': this.id, 'description': this.description, 'metrics': this.metrics};\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nexport class Statistic {\n static calculateCoefficientOfVariation(sample: number[], mean: number) {\n return Statistic.calculateStandardDeviation(sample, mean) / mean * 100;\n }\n\n static calculateMean(samples: number[]) {\n let total = 0;\n // TODO: use reduce\n samples.forEach(x => total += x);\n return total / samples.length;\n }\n\n static calculateStandardDeviation(samples: number[], mean: number) {\n let deviation = 0;\n // TODO: use reduce\n samples.forEach(x => deviation += Math.pow(x - mean, 2));\n deviation = deviation / (samples.length);\n deviation = Math.sqrt(deviation);\n return deviation;\n }\n\n static calculateRegressionSlope(\n xValues: number[], xMean: number, yValues: number[], yMean: number) {\n // See https://en.wikipedia.org/wiki/Simple_linear_regression\n let dividendSum = 0;\n let divisorSum = 0;\n for (let i = 0; i < xValues.length; i++) {\n dividendSum += (xValues[i] - xMean) * (yValues[i] - yMean);\n divisorSum += Math.pow(xValues[i] - xMean, 2);\n }\n return dividendSum / divisorSum;\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {MeasureValues} from '../measure_values';\nimport {Statistic} from '../statistic';\n\nexport function formatNum(n: number) {\n return n.toFixed(2);\n}\n\nexport function sortedProps(obj: {[key: string]: any}) {\n return Object.keys(obj).sort();\n}\n\nexport function formatStats(validSamples: MeasureValues[], metricName: string): string {\n const samples = validSamples.map(measureValues => measureValues.values[metricName]);\n const mean = Statistic.calculateMean(samples);\n const cv = Statistic.calculateCoefficientOfVariation(samples, mean);\n const formattedMean = formatNum(mean);\n // Note: Don't use the unicode character for +- as it might cause\n // hickups for consoles...\n return isNaN(cv) ? formattedMean : `${formattedMean}+-${Math.floor(cv)}%`;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, InjectionToken} from '@angular/core';\nimport {MeasureValues} from '../measure_values';\nimport {Reporter} from '../reporter';\nimport {SampleDescription} from '../sample_description';\n\nimport {formatNum, formatStats, sortedProps} from './util';\n\n\n/**\n * A reporter for the console\n */\n@Injectable()\nexport class ConsoleReporter extends Reporter {\n static PRINT = new InjectionToken('ConsoleReporter.print');\n static COLUMN_WIDTH = new InjectionToken('ConsoleReporter.columnWidth');\n static PROVIDERS = [\n {\n provide: ConsoleReporter,\n deps: [ConsoleReporter.COLUMN_WIDTH, SampleDescription, ConsoleReporter.PRINT]\n },\n {provide: ConsoleReporter.COLUMN_WIDTH, useValue: 18}, {\n provide: ConsoleReporter.PRINT,\n useValue:\n function(v: any) {\n // tslint:disable-next-line:no-console\n console.log(v);\n }\n }\n ];\n\n private static _lpad(value: string, columnWidth: number, fill = ' ') {\n let result = '';\n for (let i = 0; i < columnWidth - value.length; i++) {\n result += fill;\n }\n return result + value;\n }\n\n private _metricNames: string[];\n\n constructor(\n @Inject(ConsoleReporter.COLUMN_WIDTH) private _columnWidth: number,\n sampleDescription: SampleDescription,\n @Inject(ConsoleReporter.PRINT) private _print: Function) {\n super();\n this._metricNames = sortedProps(sampleDescription.metrics);\n this._printDescription(sampleDescription);\n }\n\n private _printDescription(sampleDescription: SampleDescription) {\n this._print(`BENCHMARK ${sampleDescription.id}`);\n this._print('Description:');\n const props = sortedProps(sampleDescription.description);\n props.forEach((prop) => {\n this._print(`- ${prop}: ${sampleDescription.description[prop]}`);\n });\n this._print('Metrics:');\n this._metricNames.forEach((metricName) => {\n this._print(`- ${metricName}: ${sampleDescription.metrics[metricName]}`);\n });\n this._print('');\n this._printStringRow(this._metricNames);\n this._printStringRow(this._metricNames.map((_) => ''), '-');\n }\n\n override reportMeasureValues(measureValues: MeasureValues): Promise<any> {\n const formattedValues = this._metricNames.map(metricName => {\n const value = measureValues.values[metricName];\n return formatNum(value);\n });\n this._printStringRow(formattedValues);\n return Promise.resolve(null);\n }\n\n override reportSample(completeSample: MeasureValues[], validSamples: MeasureValues[]):\n Promise<any> {\n this._printStringRow(this._metricNames.map((_) => ''), '=');\n this._printStringRow(\n this._metricNames.map(metricName => formatStats(validSamples, metricName)));\n return Promise.resolve(null);\n }\n\n private _printStringRow(parts: any[], fill = ' ') {\n this._print(\n parts.map(part => ConsoleReporter._lpad(part, this._columnWidth, fill)).join(' | '));\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, InjectionToken} from '@angular/core';\n\nimport {Options} from '../common_options';\nimport {MeasureValues} from '../measure_values';\nimport {Reporter} from '../reporter';\nimport {SampleDescription} from '../sample_description';\n\nimport {formatStats, sortedProps} from './util';\n\n\n/**\n * A reporter that writes results into a json file.\n */\n@Injectable()\nexport class JsonFileReporter extends Reporter {\n static PATH = new InjectionToken('JsonFileReporter.path');\n static PROVIDERS = [\n {\n provide: JsonFileReporter,\n deps: [SampleDescription, JsonFileReporter.PATH, Options.WRITE_FILE, Options.NOW]\n },\n {provide: JsonFileReporter.PATH, useValue: '.'}\n ];\n\n constructor(\n private _description: SampleDescription, @Inject(JsonFileReporter.PATH) private _path: string,\n @Inject(Options.WRITE_FILE) private _writeFile: Function,\n @Inject(Options.NOW) private _now: Function) {\n super();\n }\n\n override reportMeasureValues(measureValues: MeasureValues): Promise<any> {\n return Promise.resolve(null);\n }\n\n override reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]):\n Promise<any> {\n const stats: {[key: string]: string} = {};\n sortedProps(this._description.metrics).forEach((metricName) => {\n stats[metricName] = formatStats(validSample, metricName);\n });\n const content = JSON.stringify(\n {\n 'description': this._description,\n 'stats': stats,\n 'completeSample': completeSample,\n 'validSample': validSample,\n },\n null, 2);\n const filePath = `${this._path}/${this._description.id}_${this._now().getTime()}.json`;\n return this._writeFile(filePath, content);\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {InjectionToken, Injector} from '@angular/core';\n\nimport {MeasureValues} from '../measure_values';\nimport {Reporter} from '../reporter';\n\nexport class MultiReporter extends Reporter {\n static provideWith(childTokens: any[]): any[] {\n return [\n {\n provide: _CHILDREN,\n useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),\n deps: [Injector],\n },\n {\n provide: MultiReporter,\n useFactory: (children: Reporter[]) => new MultiReporter(children),\n deps: [_CHILDREN]\n }\n ];\n }\n\n constructor(private _reporters: Reporter[]) {\n super();\n }\n\n override reportMeasureValues(values: MeasureValues): Promise<any[]> {\n return Promise.all(this._reporters.map(reporter => reporter.reportMeasureValues(values)));\n }\n\n override reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]):\n Promise<any[]> {\n return Promise.all(\n this._reporters.map(reporter => reporter.reportSample(completeSample, validSample)));\n }\n}\n\nconst _CHILDREN = new InjectionToken('MultiReporter.children');\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, StaticProvider} from '@angular/core';\n\nimport {Options} from './common_options';\nimport {MeasureValues} from './measure_values';\nimport {Metric} from './metric';\nimport {Reporter} from './reporter';\nimport {Validator} from './validator';\nimport {WebDriverAdapter} from './web_driver_adapter';\n\n\n/**\n * The Sampler owns the sample loop:\n * 1. calls the prepare/execute callbacks,\n * 2. gets data from the metric\n * 3. asks the validator for a valid sample\n * 4. reports the new data to the reporter\n * 5. loop until there is a valid sample\n */\n@Injectable()\nexport class Sampler {\n static PROVIDERS = <StaticProvider[]>[{\n provide: Sampler,\n deps:\n [\n WebDriverAdapter, Metric, Reporter, Validator, Options.PREPARE, Options.EXECUTE,\n Options.NOW\n ]\n }];\n constructor(\n private _driver: WebDriverAdapter, private _metric: Metric, private _reporter: Reporter,\n private _validator: Validator, @Inject(Options.PREPARE) private _prepare: Function,\n @Inject(Options.EXECUTE) private _execute: Function,\n @Inject(Options.NOW) private _now: Function) {}\n\n sample(): Promise<SampleState> {\n const loop = (lastState: SampleState): Promise<SampleState> => {\n return this._iterate(lastState).then((newState) => {\n if (newState.validSample != null) {\n return newState;\n } else {\n return loop(newState);\n }\n });\n };\n return loop(new SampleState([], null));\n }\n\n private _iterate(lastState: SampleState): Promise<SampleState> {\n let resultPromise: Promise<SampleState|null>;\n if (this._prepare !== Options.NO_PREPARE) {\n resultPromise = this._driver.waitFor(this._prepare);\n } else {\n resultPromise = Promise.resolve(null);\n }\n if (this._prepare !== Options.NO_PREPARE || lastState.completeSample.length === 0) {\n resultPromise = resultPromise.then((_) => this._metric.beginMeasure());\n }\n return resultPromise.then((_) => this._driver.waitFor(this._execute))\n .then((_) => this._metric.endMeasure(this._prepare === Options.NO_PREPARE))\n .then((measureValues) => {\n if (!!measureValues['invalid']) {\n return lastState;\n }\n return this._report(lastState, measureValues);\n });\n }\n\n private _report(state: SampleState, metricValues: {[key: string]: any}): Promise<SampleState> {\n const measureValues = new MeasureValues(state.completeSample.length, this._now(), metricValues);\n const completeSample = state.completeSample.concat([measureValues]);\n const validSample = this._validator.validate(completeSample);\n let resultPromise = this._reporter.reportMeasureValues(measureValues);\n if (validSample != null) {\n resultPromise =\n resultPromise.then((_) => this._reporter.reportSample(completeSample, validSample));\n }\n return resultPromise.then((_) => new SampleState(completeSample, validSample));\n }\n}\n\nexport class SampleState {\n constructor(public completeSample: MeasureValues[], public validSample: MeasureValues[]|null) {}\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, InjectionToken} from '@angular/core';\n\nimport {MeasureValues} from '../measure_values';\nimport {Statistic} from '../statistic';\nimport {Validator} from '../validator';\n\n/**\n * A validator that checks the regression slope of a specific metric.\n * Waits for the regression slope to be >=0.\n */\n@Injectable()\nexport class RegressionSlopeValidator extends Validator {\n static SAMPLE_SIZE = new InjectionToken('RegressionSlopeValidator.sampleSize');\n static METRIC = new InjectionToken('RegressionSlopeValidator.metric');\n static PROVIDERS = [\n {\n provide: RegressionSlopeValidator,\n deps: [RegressionSlopeValidator.SAMPLE_SIZE, RegressionSlopeValidator.METRIC]\n },\n {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: 10},\n {provide: RegressionSlopeValidator.METRIC, useValue: 'scriptTime'}\n ];\n\n constructor(\n @Inject(RegressionSlopeValidator.SAMPLE_SIZE) private _sampleSize: number,\n @Inject(RegressionSlopeValidator.METRIC) private _metric: string) {\n super();\n }\n\n override describe(): {[key: string]: any} {\n return {'sampleSize': this._sampleSize, 'regressionSlopeMetric': this._metric};\n }\n\n override validate(completeSample: MeasureValues[]): MeasureValues[]|null {\n if (completeSample.length >= this._sampleSize) {\n const latestSample =\n completeSample.slice(completeSample.length - this._sampleSize, completeSample.length);\n const xValues: number[] = [];\n const yValues: number[] = [];\n for (let i = 0; i < latestSample.length; i++) {\n // For now, we only use the array index as x value.\n // TODO(tbosch): think about whether we should use time here instead\n xValues.push(i);\n yValues.push(latestSample[i].values[this._metric]);\n }\n const regressionSlope = Statistic.calculateRegressionSlope(\n xValues, Statistic.calculateMean(xValues), yValues, Statistic.calculateMean(yValues));\n return regressionSlope >= 0 ? latestSample : null;\n } else {\n return null;\n }\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, InjectionToken} from '@angular/core';\n\nimport {MeasureValues} from '../measure_values';\nimport {Validator} from '../validator';\n\n/**\n * A validator that waits for the sample to have a certain size.\n */\n@Injectable()\nexport class SizeValidator extends Validator {\n static SAMPLE_SIZE = new InjectionToken('SizeValidator.sampleSize');\n static PROVIDERS = [\n {provide: SizeValidator, deps: [SizeValidator.SAMPLE_SIZE]},\n {provide: SizeValidator.SAMPLE_SIZE, useValue: 10}\n ];\n\n constructor(@Inject(SizeValidator.SAMPLE_SIZE) private _sampleSize: number) {\n super();\n }\n\n override describe(): {[key: string]: any} {\n return {'sampleSize': this._sampleSize};\n }\n\n override validate(completeSample: MeasureValues[]): MeasureValues[]|null {\n if (completeSample.length >= this._sampleSize) {\n return completeSample.slice(completeSample.length - this._sampleSize, completeSample.length);\n } else {\n return null;\n }\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport {Inject, Injectable, StaticProvider} from '@angular/core';\nimport * as fs from 'fs';\n\nimport {Options} from '../common_options';\nimport {WebDriverAdapter} from '../web_driver_adapter';\nimport {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';\n\n/**\n * Set the following 'traceCategories' to collect metrics in Chrome:\n * 'v8,blink.console,disabled-by-default-devtools.timeline,devtools.timeline,blink.user_timing'\n *\n * In order to collect the frame rate related metrics, add 'benchmark'\n * to the list above.\n */\n@Injectable()\nexport class ChromeDriverExtension extends WebDriverExtension {\n static PROVIDERS = <StaticProvider>[{\n provide: ChromeDriverExtension,\n deps: [WebDriverAdapter, Options.USER_AGENT, Options.RAW_PERFLOG_PATH]\n }];\n\n private _majorChromeVersion: number;\n private _firstRun = true;\n private _rawPerflogPath: string|null;\n\n constructor(\n private driver: WebDriverAdapter, @Inject(Options.USER_AGENT) userAgent: string,\n @Inject(Options.RAW_PERFLOG_PATH) rawPerflogPath: string|null) {\n super();\n this._majorChromeVersion = this._parseChromeVersion(userAgent);\n this._rawPerflogPath = rawPerflo