refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
53
packages/benchpress/src/common_options.ts
Normal file
53
packages/benchpress/src/common_options.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken} from '@angular/core';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export class Options {
|
||||
static SAMPLE_ID = new InjectionToken('Options.sampleId');
|
||||
static DEFAULT_DESCRIPTION = new InjectionToken('Options.defaultDescription');
|
||||
static SAMPLE_DESCRIPTION = new InjectionToken('Options.sampleDescription');
|
||||
static FORCE_GC = new InjectionToken('Options.forceGc');
|
||||
static NO_PREPARE = () => true;
|
||||
static PREPARE = new InjectionToken('Options.prepare');
|
||||
static EXECUTE = new InjectionToken('Options.execute');
|
||||
static CAPABILITIES = new InjectionToken('Options.capabilities');
|
||||
static USER_AGENT = new InjectionToken('Options.userAgent');
|
||||
static MICRO_METRICS = new InjectionToken('Options.microMetrics');
|
||||
static USER_METRICS = new InjectionToken('Options.userMetrics');
|
||||
static NOW = new InjectionToken('Options.now');
|
||||
static WRITE_FILE = new InjectionToken('Options.writeFile');
|
||||
static RECEIVED_DATA = new InjectionToken('Options.receivedData');
|
||||
static REQUEST_COUNT = new InjectionToken('Options.requestCount');
|
||||
static CAPTURE_FRAMES = new InjectionToken('Options.frameCapture');
|
||||
static DEFAULT_PROVIDERS = [
|
||||
{provide: Options.DEFAULT_DESCRIPTION, useValue: {}},
|
||||
{provide: Options.SAMPLE_DESCRIPTION, useValue: {}},
|
||||
{provide: Options.FORCE_GC, useValue: false},
|
||||
{provide: Options.PREPARE, useValue: Options.NO_PREPARE},
|
||||
{provide: Options.MICRO_METRICS, useValue: {}}, {provide: Options.USER_METRICS, useValue: {}},
|
||||
{provide: Options.NOW, useValue: () => new Date()},
|
||||
{provide: Options.RECEIVED_DATA, useValue: false},
|
||||
{provide: Options.REQUEST_COUNT, useValue: false},
|
||||
{provide: Options.CAPTURE_FRAMES, useValue: false},
|
||||
{provide: Options.WRITE_FILE, useValue: writeFile}
|
||||
];
|
||||
}
|
||||
|
||||
function writeFile(filename: string, content: string): Promise<any> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.writeFile(filename, content, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
2
packages/benchpress/src/firefox_extension/.gitignore
vendored
Normal file
2
packages/benchpress/src/firefox_extension/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.xpi
|
||||
addon-sdk*
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
declare var exportFunction: any;
|
||||
declare var unsafeWindow: any;
|
||||
|
||||
exportFunction(function() {
|
||||
const curTime = unsafeWindow.performance.now();
|
||||
(<any>self).port.emit('startProfiler', curTime);
|
||||
}, unsafeWindow, {defineAs: 'startProfiler'});
|
||||
|
||||
exportFunction(function() {
|
||||
(<any>self).port.emit('stopProfiler');
|
||||
}, unsafeWindow, {defineAs: 'stopProfiler'});
|
||||
|
||||
exportFunction(function(cb: Function) {
|
||||
(<any>self).port.once('perfProfile', cb);
|
||||
(<any>self).port.emit('getProfile');
|
||||
}, unsafeWindow, {defineAs: 'getProfile'});
|
||||
|
||||
exportFunction(function() {
|
||||
(<any>self).port.emit('forceGC');
|
||||
}, unsafeWindow, {defineAs: 'forceGC'});
|
||||
|
||||
exportFunction(function(name: string) {
|
||||
const curTime = unsafeWindow.performance.now();
|
||||
(<any>self).port.emit('markStart', name, curTime);
|
||||
}, unsafeWindow, {defineAs: 'markStart'});
|
||||
|
||||
exportFunction(function(name: string) {
|
||||
const curTime = unsafeWindow.performance.now();
|
||||
(<any>self).port.emit('markEnd', name, curTime);
|
||||
}, unsafeWindow, {defineAs: 'markEnd'});
|
78
packages/benchpress/src/firefox_extension/lib/main.ts
Normal file
78
packages/benchpress/src/firefox_extension/lib/main.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const {Cc, Ci, Cu} = require('chrome');
|
||||
const os = Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
|
||||
const ParserUtil = require('./parser_util');
|
||||
|
||||
class Profiler {
|
||||
private _profiler: any;
|
||||
private _markerEvents: any[];
|
||||
private _profilerStartTime: number;
|
||||
|
||||
constructor() { this._profiler = Cc['@mozilla.org/tools/profiler;1'].getService(Ci.nsIProfiler); }
|
||||
|
||||
start(entries: any, interval: any, features: any, timeStarted: any) {
|
||||
this._profiler.StartProfiler(entries, interval, features, features.length);
|
||||
this._profilerStartTime = timeStarted;
|
||||
this._markerEvents = [];
|
||||
}
|
||||
|
||||
stop() { this._profiler.StopProfiler(); }
|
||||
|
||||
getProfilePerfEvents() {
|
||||
const profileData = this._profiler.getProfileData();
|
||||
let perfEvents = ParserUtil.convertPerfProfileToEvents(profileData);
|
||||
perfEvents = this._mergeMarkerEvents(perfEvents);
|
||||
perfEvents.sort(function(event1: any, event2: any) {
|
||||
return event1.ts - event2.ts;
|
||||
}); // Sort by ts
|
||||
return perfEvents;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _mergeMarkerEvents(perfEvents: any[]): any[] {
|
||||
this._markerEvents.forEach(function(markerEvent) { perfEvents.push(markerEvent); });
|
||||
return perfEvents;
|
||||
}
|
||||
|
||||
addStartEvent(name: string, timeStarted: number) {
|
||||
this._markerEvents.push({ph: 'B', ts: timeStarted - this._profilerStartTime, name: name});
|
||||
}
|
||||
|
||||
addEndEvent(name: string, timeEnded: number) {
|
||||
this._markerEvents.push({ph: 'E', ts: timeEnded - this._profilerStartTime, name: name});
|
||||
}
|
||||
}
|
||||
|
||||
function forceGC() {
|
||||
Cu.forceGC();
|
||||
os.notifyObservers(null, 'child-gc-request', null);
|
||||
};
|
||||
|
||||
const mod = require('sdk/page-mod');
|
||||
const data = require('sdk/self').data;
|
||||
const profiler = new Profiler();
|
||||
mod.PageMod({
|
||||
include: ['*'],
|
||||
contentScriptFile: data.url('installed_script.js'),
|
||||
onAttach: (worker: any) => {
|
||||
worker.port.on(
|
||||
'startProfiler',
|
||||
(timeStarted: any) => profiler.start(
|
||||
/* = profiler memory */ 3000000, 0.1, ['leaf', 'js', 'stackwalk', 'gc'], timeStarted));
|
||||
worker.port.on('stopProfiler', () => profiler.stop());
|
||||
worker.port.on(
|
||||
'getProfile', () => worker.port.emit('perfProfile', profiler.getProfilePerfEvents()));
|
||||
worker.port.on('forceGC', forceGC);
|
||||
worker.port.on(
|
||||
'markStart', (name: string, timeStarted: any) => profiler.addStartEvent(name, timeStarted));
|
||||
worker.port.on(
|
||||
'markEnd', (name: string, timeEnded: any) => profiler.addEndEvent(name, timeEnded));
|
||||
}
|
||||
});
|
92
packages/benchpress/src/firefox_extension/lib/parser_util.ts
Normal file
92
packages/benchpress/src/firefox_extension/lib/parser_util.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} perfProfile The perf profile JSON object.
|
||||
* @return {Object[]} An array of recognized events that are captured
|
||||
* within the perf profile.
|
||||
*/
|
||||
export function convertPerfProfileToEvents(perfProfile: any): any[] {
|
||||
const inProgressEvents = new Map(); // map from event name to start time
|
||||
const finishedEvents: {[key: string]: any}[] = []; // Event[] finished events
|
||||
const addFinishedEvent = function(eventName: string, startTime: number, endTime: number) {
|
||||
const categorizedEventName = categorizeEvent(eventName);
|
||||
let args: {[key: string]: any} = undefined;
|
||||
if (categorizedEventName == 'gc') {
|
||||
// TODO: We cannot measure heap size at the moment
|
||||
args = {usedHeapSize: 0};
|
||||
}
|
||||
if (startTime == endTime) {
|
||||
// Finished instantly
|
||||
finishedEvents.push({ph: 'X', ts: startTime, name: categorizedEventName, args: args});
|
||||
} else {
|
||||
// Has duration
|
||||
finishedEvents.push({ph: 'B', ts: startTime, name: categorizedEventName, args: args});
|
||||
finishedEvents.push({ph: 'E', ts: endTime, name: categorizedEventName, args: args});
|
||||
}
|
||||
};
|
||||
|
||||
const samples = perfProfile.threads[0].samples;
|
||||
// In perf profile, firefox samples all the frames in set time intervals. Here
|
||||
// we go through all the samples and construct the start and end time for each
|
||||
// event.
|
||||
for (let i = 0; i < samples.length; ++i) {
|
||||
const sample = samples[i];
|
||||
const sampleTime = sample.time;
|
||||
|
||||
// Add all the frames into a set so it's easier/faster to find the set
|
||||
// differences
|
||||
const sampleFrames = new Set();
|
||||
sample.frames.forEach(function(frame: {[key: string]: any}) {
|
||||
sampleFrames.add(frame['location']);
|
||||
});
|
||||
|
||||
// If an event is in the inProgressEvents map, but not in the current sample,
|
||||
// then it must have just finished. We add this event to the finishedEvents
|
||||
// array and remove it from the inProgressEvents map.
|
||||
const previousSampleTime = (i == 0 ? /* not used */ -1 : samples[i - 1].time);
|
||||
inProgressEvents.forEach(function(startTime, eventName) {
|
||||
if (!(sampleFrames.has(eventName))) {
|
||||
addFinishedEvent(eventName, startTime, previousSampleTime);
|
||||
inProgressEvents.delete(eventName);
|
||||
}
|
||||
});
|
||||
|
||||
// If an event is in the current sample, but not in the inProgressEvents map,
|
||||
// then it must have just started. We add this event to the inProgressEvents
|
||||
// map.
|
||||
sampleFrames.forEach(function(eventName) {
|
||||
if (!(inProgressEvents.has(eventName))) {
|
||||
inProgressEvents.set(eventName, sampleTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If anything is still in progress, we need to included it as a finished event
|
||||
// since recording ended.
|
||||
const lastSampleTime = samples[samples.length - 1].time;
|
||||
inProgressEvents.forEach(function(startTime, eventName) {
|
||||
addFinishedEvent(eventName, startTime, lastSampleTime);
|
||||
});
|
||||
|
||||
// Remove all the unknown categories.
|
||||
return finishedEvents.filter(function(event) { return event['name'] != 'unknown'; });
|
||||
}
|
||||
|
||||
// TODO: this is most likely not exhaustive.
|
||||
export function categorizeEvent(eventName: string): string {
|
||||
if (eventName.indexOf('PresShell::Paint') > -1) {
|
||||
return 'render';
|
||||
} else if (eventName.indexOf('FirefoxDriver.prototype.executeScript') > -1) {
|
||||
return 'script';
|
||||
} else if (eventName.indexOf('forceGC') > -1) {
|
||||
return 'gc';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
51
packages/benchpress/src/firefox_extension/lib/test_helper.ts
Normal file
51
packages/benchpress/src/firefox_extension/lib/test_helper.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const q = require('q');
|
||||
const FirefoxProfile = require('firefox-profile');
|
||||
const jpm = require('jpm/lib/xpi');
|
||||
const pathUtil = require('path');
|
||||
|
||||
const PERF_ADDON_PACKAGE_JSON_DIR = '..';
|
||||
|
||||
exports.getAbsolutePath = function(path: string) {
|
||||
const normalizedPath = pathUtil.normalize(path);
|
||||
if (pathUtil.resolve(normalizedPath) == normalizedPath) {
|
||||
// Already absolute path
|
||||
return normalizedPath;
|
||||
} else {
|
||||
return pathUtil.join(__dirname, normalizedPath);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getFirefoxProfile = function(extensionPath: string) {
|
||||
const deferred = q.defer();
|
||||
|
||||
const firefoxProfile = new FirefoxProfile();
|
||||
firefoxProfile.addExtensions([extensionPath], () => {
|
||||
firefoxProfile.encoded((encodedProfile: any) => {
|
||||
const multiCapabilities = [{browserName: 'firefox', firefox_profile: encodedProfile}];
|
||||
deferred.resolve(multiCapabilities);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
exports.getFirefoxProfileWithExtension = function() {
|
||||
const absPackageJsonDir = pathUtil.join(__dirname, PERF_ADDON_PACKAGE_JSON_DIR);
|
||||
const packageJson = require(pathUtil.join(absPackageJsonDir, 'package.json'));
|
||||
|
||||
const savedCwd = process.cwd();
|
||||
process.chdir(absPackageJsonDir);
|
||||
|
||||
return jpm(packageJson).then((xpiPath: string) => {
|
||||
process.chdir(savedCwd);
|
||||
return exports.getFirefoxProfile(xpiPath);
|
||||
});
|
||||
};
|
1
packages/benchpress/src/firefox_extension/package.json
Normal file
1
packages/benchpress/src/firefox_extension/package.json
Normal file
@ -0,0 +1 @@
|
||||
{ "version" : "0.0.1", "main" : "lib/main.js", "name" : "ffperf-addon" }
|
20
packages/benchpress/src/measure_values.ts
Normal file
20
packages/benchpress/src/measure_values.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export class MeasureValues {
|
||||
constructor(
|
||||
public runIndex: number, public timeStamp: Date, public values: {[key: string]: any}) {}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'timeStamp': this.timeStamp.toJSON(),
|
||||
'runIndex': this.runIndex,
|
||||
'values': this.values,
|
||||
};
|
||||
}
|
||||
}
|
31
packages/benchpress/src/metric.ts
Normal file
31
packages/benchpress/src/metric.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* A metric is measures values
|
||||
*/
|
||||
export abstract class Metric {
|
||||
/**
|
||||
* Starts measuring
|
||||
*/
|
||||
beginMeasure(): Promise<any> { throw new Error('NYI'); }
|
||||
|
||||
/**
|
||||
* Ends measuring and reports the data
|
||||
* since the begin call.
|
||||
* @param restart: Whether to restart right after this.
|
||||
*/
|
||||
endMeasure(restart: boolean): Promise<{[key: string]: any}> { throw new Error('NYI'); }
|
||||
|
||||
/**
|
||||
* Describes the metrics provided by this metric implementation.
|
||||
* (e.g. units, ...)
|
||||
*/
|
||||
describe(): {[key: string]: string} { throw new Error('NYI'); }
|
||||
}
|
63
packages/benchpress/src/metric/multi_metric.ts
Normal file
63
packages/benchpress/src/metric/multi_metric.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken, Injector} from '@angular/core';
|
||||
|
||||
import {Metric} from '../metric';
|
||||
|
||||
export class MultiMetric extends Metric {
|
||||
static provideWith(childTokens: any[]): any[] {
|
||||
return [
|
||||
{
|
||||
provide: _CHILDREN,
|
||||
useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),
|
||||
deps: [Injector]
|
||||
},
|
||||
{
|
||||
provide: MultiMetric,
|
||||
useFactory: (children: Metric[]) => new MultiMetric(children),
|
||||
deps: [_CHILDREN]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
constructor(private _metrics: Metric[]) { super(); }
|
||||
|
||||
/**
|
||||
* Starts measuring
|
||||
*/
|
||||
beginMeasure(): Promise<any> {
|
||||
return Promise.all(this._metrics.map(metric => metric.beginMeasure()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends measuring and reports the data
|
||||
* since the begin call.
|
||||
* @param restart: Whether to restart right after this.
|
||||
*/
|
||||
endMeasure(restart: boolean): Promise<{[key: string]: any}> {
|
||||
return Promise.all(this._metrics.map(metric => metric.endMeasure(restart)))
|
||||
.then(values => mergeStringMaps(<any>values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the metrics provided by this metric implementation.
|
||||
* (e.g. units, ...)
|
||||
*/
|
||||
describe(): {[key: string]: any} {
|
||||
return mergeStringMaps(this._metrics.map((metric) => metric.describe()));
|
||||
}
|
||||
}
|
||||
|
||||
function mergeStringMaps(maps: {[key: string]: string}[]): {[key: string]: string} {
|
||||
const result: {[key: string]: string} = {};
|
||||
maps.forEach(map => { Object.keys(map).forEach(prop => { result[prop] = map[prop]; }); });
|
||||
return result;
|
||||
}
|
||||
|
||||
const _CHILDREN = new InjectionToken('MultiMetric.children');
|
371
packages/benchpress/src/metric/perflog_metric.ts
Normal file
371
packages/benchpress/src/metric/perflog_metric.ts
Normal file
@ -0,0 +1,371 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
||||
|
||||
import {Options} from '../common_options';
|
||||
import {Metric} from '../metric';
|
||||
import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
|
||||
|
||||
|
||||
/**
|
||||
* A metric that reads out the performance log
|
||||
*/
|
||||
@Injectable()
|
||||
export class PerflogMetric extends Metric {
|
||||
static SET_TIMEOUT = new InjectionToken('PerflogMetric.setTimeout');
|
||||
static PROVIDERS = [
|
||||
PerflogMetric, {
|
||||
provide: PerflogMetric.SET_TIMEOUT,
|
||||
useValue: (fn: Function, millis: number) => <any>setTimeout(fn, millis)
|
||||
}
|
||||
];
|
||||
|
||||
private _remainingEvents: PerfLogEvent[];
|
||||
private _measureCount: number;
|
||||
private _perfLogFeatures: PerfLogFeatures;
|
||||
|
||||
/**
|
||||
* @param driverExtension
|
||||
* @param setTimeout
|
||||
* @param microMetrics Name and description of metrics provided via console.time / console.timeEnd
|
||||
**/
|
||||
constructor(
|
||||
private _driverExtension: WebDriverExtension,
|
||||
@Inject(PerflogMetric.SET_TIMEOUT) private _setTimeout: Function,
|
||||
@Inject(Options.MICRO_METRICS) private _microMetrics: {[key: string]: string},
|
||||
@Inject(Options.FORCE_GC) private _forceGc: boolean,
|
||||
@Inject(Options.CAPTURE_FRAMES) private _captureFrames: boolean,
|
||||
@Inject(Options.RECEIVED_DATA) private _receivedData: boolean,
|
||||
@Inject(Options.REQUEST_COUNT) private _requestCount: boolean) {
|
||||
super();
|
||||
|
||||
this._remainingEvents = [];
|
||||
this._measureCount = 0;
|
||||
this._perfLogFeatures = _driverExtension.perfLogFeatures();
|
||||
if (!this._perfLogFeatures.userTiming) {
|
||||
// User timing is needed for navigationStart.
|
||||
this._receivedData = false;
|
||||
this._requestCount = false;
|
||||
}
|
||||
}
|
||||
|
||||
describe(): {[key: string]: string} {
|
||||
const res: {[key: string]: any} = {
|
||||
'scriptTime': 'script execution time in ms, including gc and render',
|
||||
'pureScriptTime': 'script execution time in ms, without gc nor render'
|
||||
};
|
||||
if (this._perfLogFeatures.render) {
|
||||
res['renderTime'] = 'render time in ms';
|
||||
}
|
||||
if (this._perfLogFeatures.gc) {
|
||||
res['gcTime'] = 'gc time in ms';
|
||||
res['gcAmount'] = 'gc amount in kbytes';
|
||||
res['majorGcTime'] = 'time of major gcs in ms';
|
||||
if (this._forceGc) {
|
||||
res['forcedGcTime'] = 'forced gc time in ms';
|
||||
res['forcedGcAmount'] = 'forced gc amount in kbytes';
|
||||
}
|
||||
}
|
||||
if (this._receivedData) {
|
||||
res['receivedData'] = 'encoded bytes received since navigationStart';
|
||||
}
|
||||
if (this._requestCount) {
|
||||
res['requestCount'] = 'count of requests sent since navigationStart';
|
||||
}
|
||||
if (this._captureFrames) {
|
||||
if (!this._perfLogFeatures.frameCapture) {
|
||||
const warningMsg = 'WARNING: Metric requested, but not supported by driver';
|
||||
// using dot syntax for metric name to keep them grouped together in console reporter
|
||||
res['frameTime.mean'] = warningMsg;
|
||||
res['frameTime.worst'] = warningMsg;
|
||||
res['frameTime.best'] = warningMsg;
|
||||
res['frameTime.smooth'] = warningMsg;
|
||||
} else {
|
||||
res['frameTime.mean'] = 'mean frame time in ms (target: 16.6ms for 60fps)';
|
||||
res['frameTime.worst'] = 'worst frame time in ms';
|
||||
res['frameTime.best'] = 'best frame time in ms';
|
||||
res['frameTime.smooth'] = 'percentage of frames that hit 60fps';
|
||||
}
|
||||
}
|
||||
for (const name in this._microMetrics) {
|
||||
res[name] = this._microMetrics[name];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
beginMeasure(): Promise<any> {
|
||||
let resultPromise = Promise.resolve(null);
|
||||
if (this._forceGc) {
|
||||
resultPromise = resultPromise.then((_) => this._driverExtension.gc());
|
||||
}
|
||||
return resultPromise.then((_) => this._beginMeasure());
|
||||
}
|
||||
|
||||
endMeasure(restart: boolean): Promise<{[key: string]: number}> {
|
||||
if (this._forceGc) {
|
||||
return this._endPlainMeasureAndMeasureForceGc(restart);
|
||||
} else {
|
||||
return this._endMeasure(restart);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _endPlainMeasureAndMeasureForceGc(restartMeasure: boolean) {
|
||||
return this._endMeasure(true).then((measureValues) => {
|
||||
// disable frame capture for measurements during forced gc
|
||||
const originalFrameCaptureValue = this._captureFrames;
|
||||
this._captureFrames = false;
|
||||
return this._driverExtension.gc()
|
||||
.then((_) => this._endMeasure(restartMeasure))
|
||||
.then((forceGcMeasureValues) => {
|
||||
this._captureFrames = originalFrameCaptureValue;
|
||||
measureValues['forcedGcTime'] = forceGcMeasureValues['gcTime'];
|
||||
measureValues['forcedGcAmount'] = forceGcMeasureValues['gcAmount'];
|
||||
return measureValues;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _beginMeasure(): Promise<any> {
|
||||
return this._driverExtension.timeBegin(this._markName(this._measureCount++));
|
||||
}
|
||||
|
||||
private _endMeasure(restart: boolean): Promise<{[key: string]: number}> {
|
||||
const markName = this._markName(this._measureCount - 1);
|
||||
const nextMarkName = restart ? this._markName(this._measureCount++) : null;
|
||||
return this._driverExtension.timeEnd(markName, nextMarkName)
|
||||
.then((_) => this._readUntilEndMark(markName));
|
||||
}
|
||||
|
||||
private _readUntilEndMark(
|
||||
markName: string, loopCount: number = 0, startEvent: PerfLogEvent = null) {
|
||||
if (loopCount > _MAX_RETRY_COUNT) {
|
||||
throw new Error(`Tried too often to get the ending mark: ${loopCount}`);
|
||||
}
|
||||
return this._driverExtension.readPerfLog().then((events) => {
|
||||
this._addEvents(events);
|
||||
const result = this._aggregateEvents(this._remainingEvents, markName);
|
||||
if (result) {
|
||||
this._remainingEvents = events;
|
||||
return result;
|
||||
}
|
||||
let resolve: (result: any) => void;
|
||||
const promise = new Promise(res => { resolve = res; });
|
||||
this._setTimeout(() => resolve(this._readUntilEndMark(markName, loopCount + 1)), 100);
|
||||
return promise;
|
||||
});
|
||||
}
|
||||
|
||||
private _addEvents(events: PerfLogEvent[]) {
|
||||
let needSort = false;
|
||||
events.forEach(event => {
|
||||
if (event['ph'] === 'X') {
|
||||
needSort = true;
|
||||
const startEvent: PerfLogEvent = {};
|
||||
const endEvent: PerfLogEvent = {};
|
||||
for (const prop in event) {
|
||||
startEvent[prop] = event[prop];
|
||||
endEvent[prop] = event[prop];
|
||||
}
|
||||
startEvent['ph'] = 'B';
|
||||
endEvent['ph'] = 'E';
|
||||
endEvent['ts'] = startEvent['ts'] + startEvent['dur'];
|
||||
this._remainingEvents.push(startEvent);
|
||||
this._remainingEvents.push(endEvent);
|
||||
} else {
|
||||
this._remainingEvents.push(event);
|
||||
}
|
||||
});
|
||||
if (needSort) {
|
||||
// Need to sort because of the ph==='X' events
|
||||
this._remainingEvents.sort((a, b) => {
|
||||
const diff = a['ts'] - b['ts'];
|
||||
return diff > 0 ? 1 : diff < 0 ? -1 : 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _aggregateEvents(events: PerfLogEvent[], markName: string): {[key: string]: number} {
|
||||
const result: {[key: string]: number} = {'scriptTime': 0, 'pureScriptTime': 0};
|
||||
if (this._perfLogFeatures.gc) {
|
||||
result['gcTime'] = 0;
|
||||
result['majorGcTime'] = 0;
|
||||
result['gcAmount'] = 0;
|
||||
}
|
||||
if (this._perfLogFeatures.render) {
|
||||
result['renderTime'] = 0;
|
||||
}
|
||||
if (this._captureFrames) {
|
||||
result['frameTime.mean'] = 0;
|
||||
result['frameTime.best'] = 0;
|
||||
result['frameTime.worst'] = 0;
|
||||
result['frameTime.smooth'] = 0;
|
||||
}
|
||||
for (const name in this._microMetrics) {
|
||||
result[name] = 0;
|
||||
}
|
||||
if (this._receivedData) {
|
||||
result['receivedData'] = 0;
|
||||
}
|
||||
if (this._requestCount) {
|
||||
result['requestCount'] = 0;
|
||||
}
|
||||
|
||||
let markStartEvent: PerfLogEvent = null;
|
||||
let markEndEvent: PerfLogEvent = null;
|
||||
events.forEach((event) => {
|
||||
const ph = event['ph'];
|
||||
const name = event['name'];
|
||||
if (ph === 'B' && name === markName) {
|
||||
markStartEvent = event;
|
||||
} else if (ph === 'I' && name === 'navigationStart') {
|
||||
// if a benchmark measures reload of a page, use the last
|
||||
// navigationStart as begin event
|
||||
markStartEvent = event;
|
||||
} else if (ph === 'E' && name === markName) {
|
||||
markEndEvent = event;
|
||||
}
|
||||
});
|
||||
if (!markStartEvent || !markEndEvent) {
|
||||
// not all events have been received, no further processing for now
|
||||
return null;
|
||||
}
|
||||
|
||||
let gcTimeInScript = 0;
|
||||
let renderTimeInScript = 0;
|
||||
|
||||
const frameTimestamps: number[] = [];
|
||||
const frameTimes: number[] = [];
|
||||
let frameCaptureStartEvent: PerfLogEvent = null;
|
||||
let frameCaptureEndEvent: PerfLogEvent = null;
|
||||
|
||||
const intervalStarts: {[key: string]: PerfLogEvent} = {};
|
||||
const intervalStartCount: {[key: string]: number} = {};
|
||||
|
||||
let inMeasureRange = false;
|
||||
events.forEach((event) => {
|
||||
const ph = event['ph'];
|
||||
let name = event['name'];
|
||||
let microIterations = 1;
|
||||
const microIterationsMatch = name.match(_MICRO_ITERATIONS_REGEX);
|
||||
if (microIterationsMatch) {
|
||||
name = microIterationsMatch[1];
|
||||
microIterations = parseInt(microIterationsMatch[2], 10);
|
||||
}
|
||||
if (event === markStartEvent) {
|
||||
inMeasureRange = true;
|
||||
} else if (event === markEndEvent) {
|
||||
inMeasureRange = false;
|
||||
}
|
||||
if (!inMeasureRange || event['pid'] !== markStartEvent['pid']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._requestCount && name === 'sendRequest') {
|
||||
result['requestCount'] += 1;
|
||||
} else if (this._receivedData && name === 'receivedData' && ph === 'I') {
|
||||
result['receivedData'] += event['args']['encodedDataLength'];
|
||||
}
|
||||
if (ph === 'B' && name === _MARK_NAME_FRAME_CAPUTRE) {
|
||||
if (frameCaptureStartEvent) {
|
||||
throw new Error('can capture frames only once per benchmark run');
|
||||
}
|
||||
if (!this._captureFrames) {
|
||||
throw new Error(
|
||||
'found start event for frame capture, but frame capture was not requested in benchpress');
|
||||
}
|
||||
frameCaptureStartEvent = event;
|
||||
} else if (ph === 'E' && name === _MARK_NAME_FRAME_CAPUTRE) {
|
||||
if (!frameCaptureStartEvent) {
|
||||
throw new Error('missing start event for frame capture');
|
||||
}
|
||||
frameCaptureEndEvent = event;
|
||||
}
|
||||
|
||||
if (ph === 'I' && frameCaptureStartEvent && !frameCaptureEndEvent && name === 'frame') {
|
||||
frameTimestamps.push(event['ts']);
|
||||
if (frameTimestamps.length >= 2) {
|
||||
frameTimes.push(
|
||||
frameTimestamps[frameTimestamps.length - 1] -
|
||||
frameTimestamps[frameTimestamps.length - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (ph === 'B') {
|
||||
if (!intervalStarts[name]) {
|
||||
intervalStartCount[name] = 1;
|
||||
intervalStarts[name] = event;
|
||||
} else {
|
||||
intervalStartCount[name]++;
|
||||
}
|
||||
} else if ((ph === 'E') && intervalStarts[name]) {
|
||||
intervalStartCount[name]--;
|
||||
if (intervalStartCount[name] === 0) {
|
||||
const startEvent = intervalStarts[name];
|
||||
const duration = (event['ts'] - startEvent['ts']);
|
||||
intervalStarts[name] = null;
|
||||
if (name === 'gc') {
|
||||
result['gcTime'] += duration;
|
||||
const amount =
|
||||
(startEvent['args']['usedHeapSize'] - event['args']['usedHeapSize']) / 1000;
|
||||
result['gcAmount'] += amount;
|
||||
const majorGc = event['args']['majorGc'];
|
||||
if (majorGc && majorGc) {
|
||||
result['majorGcTime'] += duration;
|
||||
}
|
||||
if (intervalStarts['script']) {
|
||||
gcTimeInScript += duration;
|
||||
}
|
||||
} else if (name === 'render') {
|
||||
result['renderTime'] += duration;
|
||||
if (intervalStarts['script']) {
|
||||
renderTimeInScript += duration;
|
||||
}
|
||||
} else if (name === 'script') {
|
||||
result['scriptTime'] += duration;
|
||||
} else if (this._microMetrics[name]) {
|
||||
(<any>result)[name] += duration / microIterations;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (frameCaptureStartEvent && !frameCaptureEndEvent) {
|
||||
throw new Error('missing end event for frame capture');
|
||||
}
|
||||
if (this._captureFrames && !frameCaptureStartEvent) {
|
||||
throw new Error('frame capture requested in benchpress, but no start event was found');
|
||||
}
|
||||
if (frameTimes.length > 0) {
|
||||
this._addFrameMetrics(result, frameTimes);
|
||||
}
|
||||
result['pureScriptTime'] = result['scriptTime'] - gcTimeInScript - renderTimeInScript;
|
||||
return result;
|
||||
}
|
||||
|
||||
private _addFrameMetrics(result: {[key: string]: number}, frameTimes: any[]) {
|
||||
result['frameTime.mean'] = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
|
||||
const firstFrame = frameTimes[0];
|
||||
result['frameTime.worst'] = frameTimes.reduce((a, b) => a > b ? a : b, firstFrame);
|
||||
result['frameTime.best'] = frameTimes.reduce((a, b) => a < b ? a : b, firstFrame);
|
||||
result['frameTime.smooth'] =
|
||||
frameTimes.filter(t => t < _FRAME_TIME_SMOOTH_THRESHOLD).length / frameTimes.length;
|
||||
}
|
||||
|
||||
private _markName(index: number) { return `${_MARK_NAME_PREFIX}${index}`; }
|
||||
}
|
||||
|
||||
const _MICRO_ITERATIONS_REGEX = /(.+)\*(\d+)$/;
|
||||
|
||||
const _MAX_RETRY_COUNT = 20;
|
||||
const _MARK_NAME_PREFIX = 'benchpress';
|
||||
|
||||
const _MARK_NAME_FRAME_CAPUTRE = 'frameCapture';
|
||||
// using 17ms as a somewhat looser threshold, instead of 16.6666ms
|
||||
const _FRAME_TIME_SMOOTH_THRESHOLD = 17;
|
69
packages/benchpress/src/metric/user_metric.ts
Normal file
69
packages/benchpress/src/metric/user_metric.ts
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
|
||||
import {Options} from '../common_options';
|
||||
import {Metric} from '../metric';
|
||||
import {WebDriverAdapter} from '../web_driver_adapter';
|
||||
|
||||
@Injectable()
|
||||
export class UserMetric extends Metric {
|
||||
static PROVIDERS = [UserMetric];
|
||||
|
||||
constructor(
|
||||
@Inject(Options.USER_METRICS) private _userMetrics: {[key: string]: string},
|
||||
private _wdAdapter: WebDriverAdapter) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts measuring
|
||||
*/
|
||||
beginMeasure(): Promise<any> { return Promise.resolve(true); }
|
||||
|
||||
/**
|
||||
* Ends measuring.
|
||||
*/
|
||||
endMeasure(restart: boolean): Promise<{[key: string]: any}> {
|
||||
let resolve: (result: any) => void;
|
||||
let reject: (error: any) => void;
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
const adapter = this._wdAdapter;
|
||||
const names = Object.keys(this._userMetrics);
|
||||
|
||||
function getAndClearValues() {
|
||||
Promise.all(names.map(name => adapter.executeScript(`return window.${name}`)))
|
||||
.then((values: any[]) => {
|
||||
if (values.every(v => typeof v === 'number')) {
|
||||
Promise.all(names.map(name => adapter.executeScript(`delete window.${name}`)))
|
||||
.then((_: any[]) => {
|
||||
const map: {[k: string]: any} = {};
|
||||
for (let i = 0, n = names.length; i < n; i++) {
|
||||
map[names[i]] = values[i];
|
||||
}
|
||||
resolve(map);
|
||||
}, reject);
|
||||
} else {
|
||||
<any>setTimeout(getAndClearValues, 100);
|
||||
}
|
||||
}, reject);
|
||||
}
|
||||
getAndClearValues();
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the metrics provided by this metric implementation.
|
||||
* (e.g. units, ...)
|
||||
*/
|
||||
describe(): {[key: string]: any} { return this._userMetrics; }
|
||||
}
|
20
packages/benchpress/src/reporter.ts
Normal file
20
packages/benchpress/src/reporter.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MeasureValues} from './measure_values';
|
||||
|
||||
/**
|
||||
* A reporter reports measure values and the valid sample.
|
||||
*/
|
||||
export abstract class Reporter {
|
||||
reportMeasureValues(values: MeasureValues): Promise<any> { throw new Error('NYI'); }
|
||||
|
||||
reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]): Promise<any> {
|
||||
throw new Error('NYI');
|
||||
}
|
||||
}
|
87
packages/benchpress/src/reporter/console_reporter.ts
Normal file
87
packages/benchpress/src/reporter/console_reporter.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
||||
import {MeasureValues} from '../measure_values';
|
||||
import {Reporter} from '../reporter';
|
||||
import {SampleDescription} from '../sample_description';
|
||||
|
||||
import {formatNum, formatStats, sortedProps} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* A reporter for the console
|
||||
*/
|
||||
@Injectable()
|
||||
export class ConsoleReporter extends Reporter {
|
||||
static PRINT = new InjectionToken('ConsoleReporter.print');
|
||||
static COLUMN_WIDTH = new InjectionToken('ConsoleReporter.columnWidth');
|
||||
static PROVIDERS = [
|
||||
ConsoleReporter, {provide: ConsoleReporter.COLUMN_WIDTH, useValue: 18}, {
|
||||
provide: ConsoleReporter.PRINT,
|
||||
useValue: function(v: any) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(v);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
private static _lpad(value: string, columnWidth: number, fill = ' ') {
|
||||
let result = '';
|
||||
for (let i = 0; i < columnWidth - value.length; i++) {
|
||||
result += fill;
|
||||
}
|
||||
return result + value;
|
||||
}
|
||||
|
||||
private _metricNames: string[];
|
||||
|
||||
constructor(
|
||||
@Inject(ConsoleReporter.COLUMN_WIDTH) private _columnWidth: number,
|
||||
sampleDescription: SampleDescription,
|
||||
@Inject(ConsoleReporter.PRINT) private _print: Function) {
|
||||
super();
|
||||
this._metricNames = sortedProps(sampleDescription.metrics);
|
||||
this._printDescription(sampleDescription);
|
||||
}
|
||||
|
||||
private _printDescription(sampleDescription: SampleDescription) {
|
||||
this._print(`BENCHMARK ${sampleDescription.id}`);
|
||||
this._print('Description:');
|
||||
const props = sortedProps(sampleDescription.description);
|
||||
props.forEach((prop) => { this._print(`- ${prop}: ${sampleDescription.description[prop]}`); });
|
||||
this._print('Metrics:');
|
||||
this._metricNames.forEach((metricName) => {
|
||||
this._print(`- ${metricName}: ${sampleDescription.metrics[metricName]}`);
|
||||
});
|
||||
this._print('');
|
||||
this._printStringRow(this._metricNames);
|
||||
this._printStringRow(this._metricNames.map((_) => ''), '-');
|
||||
}
|
||||
|
||||
reportMeasureValues(measureValues: MeasureValues): Promise<any> {
|
||||
const formattedValues = this._metricNames.map(metricName => {
|
||||
const value = measureValues.values[metricName];
|
||||
return formatNum(value);
|
||||
});
|
||||
this._printStringRow(formattedValues);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
reportSample(completeSample: MeasureValues[], validSamples: MeasureValues[]): Promise<any> {
|
||||
this._printStringRow(this._metricNames.map((_) => ''), '=');
|
||||
this._printStringRow(
|
||||
this._metricNames.map(metricName => formatStats(validSamples, metricName)));
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
private _printStringRow(parts: any[], fill = ' ') {
|
||||
this._print(
|
||||
parts.map(part => ConsoleReporter._lpad(part, this._columnWidth, fill)).join(' | '));
|
||||
}
|
||||
}
|
52
packages/benchpress/src/reporter/json_file_reporter.ts
Normal file
52
packages/benchpress/src/reporter/json_file_reporter.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
||||
|
||||
import {Options} from '../common_options';
|
||||
import {MeasureValues} from '../measure_values';
|
||||
import {Reporter} from '../reporter';
|
||||
import {SampleDescription} from '../sample_description';
|
||||
|
||||
import {formatStats, sortedProps} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* A reporter that writes results into a json file.
|
||||
*/
|
||||
@Injectable()
|
||||
export class JsonFileReporter extends Reporter {
|
||||
static PATH = new InjectionToken('JsonFileReporter.path');
|
||||
static PROVIDERS = [JsonFileReporter, {provide: JsonFileReporter.PATH, useValue: '.'}];
|
||||
|
||||
constructor(
|
||||
private _description: SampleDescription, @Inject(JsonFileReporter.PATH) private _path: string,
|
||||
@Inject(Options.WRITE_FILE) private _writeFile: Function,
|
||||
@Inject(Options.NOW) private _now: Function) {
|
||||
super();
|
||||
}
|
||||
|
||||
reportMeasureValues(measureValues: MeasureValues): Promise<any> { return Promise.resolve(null); }
|
||||
|
||||
reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]): Promise<any> {
|
||||
const stats: {[key: string]: string} = {};
|
||||
sortedProps(this._description.metrics).forEach((metricName) => {
|
||||
stats[metricName] = formatStats(validSample, metricName);
|
||||
});
|
||||
const content = JSON.stringify(
|
||||
{
|
||||
'description': this._description,
|
||||
'stats': stats,
|
||||
'completeSample': completeSample,
|
||||
'validSample': validSample,
|
||||
},
|
||||
null, 2);
|
||||
const filePath = `${this._path}/${this._description.id}_${this._now().getTime()}.json`;
|
||||
return this._writeFile(filePath, content);
|
||||
}
|
||||
}
|
42
packages/benchpress/src/reporter/multi_reporter.ts
Normal file
42
packages/benchpress/src/reporter/multi_reporter.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken, Injector} from '@angular/core';
|
||||
|
||||
import {MeasureValues} from '../measure_values';
|
||||
import {Reporter} from '../reporter';
|
||||
|
||||
export class MultiReporter extends Reporter {
|
||||
static provideWith(childTokens: any[]): any[] {
|
||||
return [
|
||||
{
|
||||
provide: _CHILDREN,
|
||||
useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),
|
||||
deps: [Injector],
|
||||
},
|
||||
{
|
||||
provide: MultiReporter,
|
||||
useFactory: (children: Reporter[]) => new MultiReporter(children),
|
||||
deps: [_CHILDREN]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
constructor(private _reporters: Reporter[]) { super(); }
|
||||
|
||||
reportMeasureValues(values: MeasureValues): Promise<any[]> {
|
||||
return Promise.all(this._reporters.map(reporter => reporter.reportMeasureValues(values)));
|
||||
}
|
||||
|
||||
reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]): Promise<any[]> {
|
||||
return Promise.all(
|
||||
this._reporters.map(reporter => reporter.reportSample(completeSample, validSample)));
|
||||
}
|
||||
}
|
||||
|
||||
const _CHILDREN = new InjectionToken('MultiReporter.children');
|
28
packages/benchpress/src/reporter/util.ts
Normal file
28
packages/benchpress/src/reporter/util.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MeasureValues} from '../measure_values';
|
||||
import {Statistic} from '../statistic';
|
||||
|
||||
export function formatNum(n: number) {
|
||||
return n.toFixed(2);
|
||||
}
|
||||
|
||||
export function sortedProps(obj: {[key: string]: any}) {
|
||||
return Object.keys(obj).sort();
|
||||
}
|
||||
|
||||
export function formatStats(validSamples: MeasureValues[], metricName: string): string {
|
||||
const samples = validSamples.map(measureValues => measureValues.values[metricName]);
|
||||
const mean = Statistic.calculateMean(samples);
|
||||
const cv = Statistic.calculateCoefficientOfVariation(samples, mean);
|
||||
const formattedMean = formatNum(mean);
|
||||
// Note: Don't use the unicode character for +- as it might cause
|
||||
// hickups for consoles...
|
||||
return isNaN(cv) ? formattedMean : `${formattedMean}+-${Math.floor(cv)}%`;
|
||||
}
|
109
packages/benchpress/src/runner.ts
Normal file
109
packages/benchpress/src/runner.ts
Normal file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Provider, ReflectiveInjector} from '@angular/core';
|
||||
|
||||
import {Options} from './common_options';
|
||||
import {Metric} from './metric';
|
||||
import {MultiMetric} from './metric/multi_metric';
|
||||
import {PerflogMetric} from './metric/perflog_metric';
|
||||
import {UserMetric} from './metric/user_metric';
|
||||
import {Reporter} from './reporter';
|
||||
import {ConsoleReporter} from './reporter/console_reporter';
|
||||
import {MultiReporter} from './reporter/multi_reporter';
|
||||
import {SampleDescription} from './sample_description';
|
||||
import {SampleState, Sampler} from './sampler';
|
||||
import {Validator} from './validator';
|
||||
import {RegressionSlopeValidator} from './validator/regression_slope_validator';
|
||||
import {SizeValidator} from './validator/size_validator';
|
||||
import {WebDriverAdapter} from './web_driver_adapter';
|
||||
import {WebDriverExtension} from './web_driver_extension';
|
||||
import {ChromeDriverExtension} from './webdriver/chrome_driver_extension';
|
||||
import {FirefoxDriverExtension} from './webdriver/firefox_driver_extension';
|
||||
import {IOsDriverExtension} from './webdriver/ios_driver_extension';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The Runner is the main entry point for executing a sample run.
|
||||
* It provides defaults, creates the injector and calls the sampler.
|
||||
*/
|
||||
export class Runner {
|
||||
constructor(private _defaultProviders: Provider[] = []) {}
|
||||
|
||||
sample({id, execute, prepare, microMetrics, providers, userMetrics}: {
|
||||
id: string,
|
||||
execute?: Function,
|
||||
prepare?: Function,
|
||||
microMetrics?: {[key: string]: string},
|
||||
providers?: Provider[],
|
||||
userMetrics?: {[key: string]: string}
|
||||
}): Promise<SampleState> {
|
||||
const sampleProviders: Provider[] = [
|
||||
_DEFAULT_PROVIDERS, this._defaultProviders, {provide: Options.SAMPLE_ID, useValue: id},
|
||||
{provide: Options.EXECUTE, useValue: execute}
|
||||
];
|
||||
if (prepare != null) {
|
||||
sampleProviders.push({provide: Options.PREPARE, useValue: prepare});
|
||||
}
|
||||
if (microMetrics != null) {
|
||||
sampleProviders.push({provide: Options.MICRO_METRICS, useValue: microMetrics});
|
||||
}
|
||||
if (userMetrics != null) {
|
||||
sampleProviders.push({provide: Options.USER_METRICS, useValue: userMetrics});
|
||||
}
|
||||
if (providers != null) {
|
||||
sampleProviders.push(providers);
|
||||
}
|
||||
|
||||
const inj = ReflectiveInjector.resolveAndCreate(sampleProviders);
|
||||
const adapter: WebDriverAdapter = inj.get(WebDriverAdapter);
|
||||
|
||||
return Promise
|
||||
.all([adapter.capabilities(), adapter.executeScript('return window.navigator.userAgent;')])
|
||||
.then((args) => {
|
||||
const capabilities = args[0];
|
||||
const userAgent = args[1];
|
||||
|
||||
// This might still create instances twice. We are creating a new injector with all the
|
||||
// providers.
|
||||
// Only WebDriverAdapter is reused.
|
||||
// TODO vsavkin consider changing it when toAsyncFactory is added back or when child
|
||||
// injectors are handled better.
|
||||
const injector = ReflectiveInjector.resolveAndCreate([
|
||||
sampleProviders, {provide: Options.CAPABILITIES, useValue: capabilities},
|
||||
{provide: Options.USER_AGENT, useValue: userAgent},
|
||||
{provide: WebDriverAdapter, useValue: adapter}
|
||||
]);
|
||||
|
||||
const sampler = injector.get(Sampler);
|
||||
return sampler.sample();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const _DEFAULT_PROVIDERS = [
|
||||
Options.DEFAULT_PROVIDERS,
|
||||
Sampler.PROVIDERS,
|
||||
ConsoleReporter.PROVIDERS,
|
||||
RegressionSlopeValidator.PROVIDERS,
|
||||
SizeValidator.PROVIDERS,
|
||||
ChromeDriverExtension.PROVIDERS,
|
||||
FirefoxDriverExtension.PROVIDERS,
|
||||
IOsDriverExtension.PROVIDERS,
|
||||
PerflogMetric.PROVIDERS,
|
||||
UserMetric.PROVIDERS,
|
||||
SampleDescription.PROVIDERS,
|
||||
MultiReporter.provideWith([ConsoleReporter]),
|
||||
MultiMetric.provideWith([PerflogMetric, UserMetric]),
|
||||
{provide: Reporter, useExisting: MultiReporter},
|
||||
{provide: Validator, useExisting: RegressionSlopeValidator},
|
||||
WebDriverExtension.provideFirstSupported(
|
||||
[ChromeDriverExtension, FirefoxDriverExtension, IOsDriverExtension]),
|
||||
{provide: Metric, useExisting: MultiMetric},
|
||||
];
|
49
packages/benchpress/src/sample_description.ts
Normal file
49
packages/benchpress/src/sample_description.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken} from '@angular/core';
|
||||
|
||||
import {Options} from './common_options';
|
||||
import {Metric} from './metric';
|
||||
import {Validator} from './validator';
|
||||
|
||||
|
||||
/**
|
||||
* SampleDescription merges all available descriptions about a sample
|
||||
*/
|
||||
export class SampleDescription {
|
||||
static PROVIDERS = [{
|
||||
provide: SampleDescription,
|
||||
useFactory:
|
||||
(metric: Metric, id: string, forceGc: boolean, userAgent: string, validator: Validator,
|
||||
defaultDesc: {[key: string]: string}, userDesc: {[key: string]: string}) =>
|
||||
new SampleDescription(
|
||||
id,
|
||||
[
|
||||
{'forceGc': forceGc, 'userAgent': userAgent}, validator.describe(), defaultDesc,
|
||||
userDesc
|
||||
],
|
||||
metric.describe()),
|
||||
deps: [
|
||||
Metric, Options.SAMPLE_ID, Options.FORCE_GC, Options.USER_AGENT, Validator,
|
||||
Options.DEFAULT_DESCRIPTION, Options.SAMPLE_DESCRIPTION
|
||||
]
|
||||
}];
|
||||
description: {[key: string]: any};
|
||||
|
||||
constructor(
|
||||
public id: string, descriptions: Array<{[key: string]: any}>,
|
||||
public metrics: {[key: string]: any}) {
|
||||
this.description = {};
|
||||
descriptions.forEach(description => {
|
||||
Object.keys(description).forEach(prop => { this.description[prop] = description[prop]; });
|
||||
});
|
||||
}
|
||||
|
||||
toJson() { return {'id': this.id, 'description': this.description, 'metrics': this.metrics}; }
|
||||
}
|
80
packages/benchpress/src/sampler.ts
Normal file
80
packages/benchpress/src/sampler.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
|
||||
import {Options} from './common_options';
|
||||
import {MeasureValues} from './measure_values';
|
||||
import {Metric} from './metric';
|
||||
import {Reporter} from './reporter';
|
||||
import {Validator} from './validator';
|
||||
import {WebDriverAdapter} from './web_driver_adapter';
|
||||
|
||||
|
||||
/**
|
||||
* The Sampler owns the sample loop:
|
||||
* 1. calls the prepare/execute callbacks,
|
||||
* 2. gets data from the metric
|
||||
* 3. asks the validator for a valid sample
|
||||
* 4. reports the new data to the reporter
|
||||
* 5. loop until there is a valid sample
|
||||
*/
|
||||
@Injectable()
|
||||
export class Sampler {
|
||||
static PROVIDERS = [Sampler];
|
||||
|
||||
constructor(
|
||||
private _driver: WebDriverAdapter, private _metric: Metric, private _reporter: Reporter,
|
||||
private _validator: Validator, @Inject(Options.PREPARE) private _prepare: Function,
|
||||
@Inject(Options.EXECUTE) private _execute: Function,
|
||||
@Inject(Options.NOW) private _now: Function) {}
|
||||
|
||||
sample(): Promise<SampleState> {
|
||||
const loop = (lastState: SampleState): Promise<SampleState> => {
|
||||
return this._iterate(lastState).then((newState) => {
|
||||
if (newState.validSample != null) {
|
||||
return newState;
|
||||
} else {
|
||||
return loop(newState);
|
||||
}
|
||||
});
|
||||
};
|
||||
return loop(new SampleState([], null));
|
||||
}
|
||||
|
||||
private _iterate(lastState: SampleState): Promise<SampleState> {
|
||||
let resultPromise: Promise<SampleState>;
|
||||
if (this._prepare !== Options.NO_PREPARE) {
|
||||
resultPromise = this._driver.waitFor(this._prepare);
|
||||
} else {
|
||||
resultPromise = Promise.resolve(null);
|
||||
}
|
||||
if (this._prepare !== Options.NO_PREPARE || lastState.completeSample.length === 0) {
|
||||
resultPromise = resultPromise.then((_) => this._metric.beginMeasure());
|
||||
}
|
||||
return resultPromise.then((_) => this._driver.waitFor(this._execute))
|
||||
.then((_) => this._metric.endMeasure(this._prepare === Options.NO_PREPARE))
|
||||
.then((measureValues) => this._report(lastState, measureValues));
|
||||
}
|
||||
|
||||
private _report(state: SampleState, metricValues: {[key: string]: any}): Promise<SampleState> {
|
||||
const measureValues = new MeasureValues(state.completeSample.length, this._now(), metricValues);
|
||||
const completeSample = state.completeSample.concat([measureValues]);
|
||||
const validSample = this._validator.validate(completeSample);
|
||||
let resultPromise = this._reporter.reportMeasureValues(measureValues);
|
||||
if (validSample != null) {
|
||||
resultPromise =
|
||||
resultPromise.then((_) => this._reporter.reportSample(completeSample, validSample));
|
||||
}
|
||||
return resultPromise.then((_) => new SampleState(completeSample, validSample));
|
||||
}
|
||||
}
|
||||
|
||||
export class SampleState {
|
||||
constructor(public completeSample: MeasureValues[], public validSample: MeasureValues[]) {}
|
||||
}
|
41
packages/benchpress/src/statistic.ts
Normal file
41
packages/benchpress/src/statistic.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export class Statistic {
|
||||
static calculateCoefficientOfVariation(sample: number[], mean: number) {
|
||||
return Statistic.calculateStandardDeviation(sample, mean) / mean * 100;
|
||||
}
|
||||
|
||||
static calculateMean(samples: number[]) {
|
||||
let total = 0;
|
||||
// TODO: use reduce
|
||||
samples.forEach(x => total += x);
|
||||
return total / samples.length;
|
||||
}
|
||||
|
||||
static calculateStandardDeviation(samples: number[], mean: number) {
|
||||
let deviation = 0;
|
||||
// TODO: use reduce
|
||||
samples.forEach(x => deviation += Math.pow(x - mean, 2));
|
||||
deviation = deviation / (samples.length);
|
||||
deviation = Math.sqrt(deviation);
|
||||
return deviation;
|
||||
}
|
||||
|
||||
static calculateRegressionSlope(
|
||||
xValues: number[], xMean: number, yValues: number[], yMean: number) {
|
||||
// See http://en.wikipedia.org/wiki/Simple_linear_regression
|
||||
let dividendSum = 0;
|
||||
let divisorSum = 0;
|
||||
for (let i = 0; i < xValues.length; i++) {
|
||||
dividendSum += (xValues[i] - xMean) * (yValues[i] - yMean);
|
||||
divisorSum += Math.pow(xValues[i] - xMean, 2);
|
||||
}
|
||||
return dividendSum / divisorSum;
|
||||
}
|
||||
}
|
27
packages/benchpress/src/validator.ts
Normal file
27
packages/benchpress/src/validator.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MeasureValues} from './measure_values';
|
||||
|
||||
/**
|
||||
* A Validator calculates a valid sample out of the complete sample.
|
||||
* A valid sample is a sample that represents the population that should be observed
|
||||
* in the correct way.
|
||||
*/
|
||||
export abstract class Validator {
|
||||
/**
|
||||
* Calculates a valid sample out of the complete sample
|
||||
*/
|
||||
validate(completeSample: MeasureValues[]): MeasureValues[] { throw new Error('NYI'); }
|
||||
|
||||
/**
|
||||
* Returns a Map that describes the properties of the validator
|
||||
* (e.g. sample size, ...)
|
||||
*/
|
||||
describe(): {[key: string]: any} { throw new Error('NYI'); }
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
||||
|
||||
import {MeasureValues} from '../measure_values';
|
||||
import {Statistic} from '../statistic';
|
||||
import {Validator} from '../validator';
|
||||
|
||||
/**
|
||||
* A validator that checks the regression slope of a specific metric.
|
||||
* Waits for the regression slope to be >=0.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RegressionSlopeValidator extends Validator {
|
||||
static SAMPLE_SIZE = new InjectionToken('RegressionSlopeValidator.sampleSize');
|
||||
static METRIC = new InjectionToken('RegressionSlopeValidator.metric');
|
||||
static PROVIDERS = [
|
||||
RegressionSlopeValidator, {provide: RegressionSlopeValidator.SAMPLE_SIZE, useValue: 10},
|
||||
{provide: RegressionSlopeValidator.METRIC, useValue: 'scriptTime'}
|
||||
];
|
||||
|
||||
constructor(
|
||||
@Inject(RegressionSlopeValidator.SAMPLE_SIZE) private _sampleSize: number,
|
||||
@Inject(RegressionSlopeValidator.METRIC) private _metric: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
describe(): {[key: string]: any} {
|
||||
return {'sampleSize': this._sampleSize, 'regressionSlopeMetric': this._metric};
|
||||
}
|
||||
|
||||
validate(completeSample: MeasureValues[]): MeasureValues[] {
|
||||
if (completeSample.length >= this._sampleSize) {
|
||||
const latestSample =
|
||||
completeSample.slice(completeSample.length - this._sampleSize, completeSample.length);
|
||||
const xValues: number[] = [];
|
||||
const yValues: number[] = [];
|
||||
for (let i = 0; i < latestSample.length; i++) {
|
||||
// For now, we only use the array index as x value.
|
||||
// TODO(tbosch): think about whether we should use time here instead
|
||||
xValues.push(i);
|
||||
yValues.push(latestSample[i].values[this._metric]);
|
||||
}
|
||||
const regressionSlope = Statistic.calculateRegressionSlope(
|
||||
xValues, Statistic.calculateMean(xValues), yValues, Statistic.calculateMean(yValues));
|
||||
return regressionSlope >= 0 ? latestSample : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
33
packages/benchpress/src/validator/size_validator.ts
Normal file
33
packages/benchpress/src/validator/size_validator.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, InjectionToken} from '@angular/core';
|
||||
|
||||
import {MeasureValues} from '../measure_values';
|
||||
import {Validator} from '../validator';
|
||||
|
||||
/**
|
||||
* A validator that waits for the sample to have a certain size.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SizeValidator extends Validator {
|
||||
static SAMPLE_SIZE = new InjectionToken('SizeValidator.sampleSize');
|
||||
static PROVIDERS = [SizeValidator, {provide: SizeValidator.SAMPLE_SIZE, useValue: 10}];
|
||||
|
||||
constructor(@Inject(SizeValidator.SAMPLE_SIZE) private _sampleSize: number) { super(); }
|
||||
|
||||
describe(): {[key: string]: any} { return {'sampleSize': this._sampleSize}; }
|
||||
|
||||
validate(completeSample: MeasureValues[]): MeasureValues[] {
|
||||
if (completeSample.length >= this._sampleSize) {
|
||||
return completeSample.slice(completeSample.length - this._sampleSize, completeSample.length);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
22
packages/benchpress/src/web_driver_adapter.ts
Normal file
22
packages/benchpress/src/web_driver_adapter.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A WebDriverAdapter bridges API differences between different WebDriver clients,
|
||||
* e.g. JS vs Dart Async vs Dart Sync webdriver.
|
||||
* Needs one implementation for every supported WebDriver client.
|
||||
*/
|
||||
export abstract class WebDriverAdapter {
|
||||
waitFor(callback: Function): Promise<any> { throw new Error('NYI'); }
|
||||
executeScript(script: string): Promise<any> { throw new Error('NYI'); }
|
||||
executeAsyncScript(script: string): Promise<any> { throw new Error('NYI'); }
|
||||
capabilities(): Promise<{[key: string]: any}> { throw new Error('NYI'); }
|
||||
logs(type: string): Promise<any[]> { throw new Error('NYI'); }
|
||||
}
|
104
packages/benchpress/src/web_driver_extension.ts
Normal file
104
packages/benchpress/src/web_driver_extension.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InjectionToken, Injector} from '@angular/core';
|
||||
|
||||
import {Options} from './common_options';
|
||||
|
||||
export type PerfLogEvent = {
|
||||
[key: string]: any
|
||||
} & {
|
||||
ph?: 'X' | 'B' | 'E' | 'I',
|
||||
ts?: number,
|
||||
dur?: number,
|
||||
name?: string,
|
||||
pid?: string,
|
||||
args?: {
|
||||
encodedDataLength?: number,
|
||||
usedHeapSize?: number,
|
||||
majorGc?: boolean,
|
||||
url?: string,
|
||||
method?: string
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A WebDriverExtension implements extended commands of the webdriver protocol
|
||||
* for a given browser, independent of the WebDriverAdapter.
|
||||
* Needs one implementation for every supported Browser.
|
||||
*/
|
||||
export abstract class WebDriverExtension {
|
||||
static provideFirstSupported(childTokens: any[]): any[] {
|
||||
const res = [
|
||||
{
|
||||
provide: _CHILDREN,
|
||||
useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),
|
||||
deps: [Injector]
|
||||
},
|
||||
{
|
||||
provide: WebDriverExtension,
|
||||
useFactory: (children: WebDriverExtension[], capabilities: {[key: string]: any}) => {
|
||||
let delegate: WebDriverExtension;
|
||||
children.forEach(extension => {
|
||||
if (extension.supports(capabilities)) {
|
||||
delegate = extension;
|
||||
}
|
||||
});
|
||||
if (!delegate) {
|
||||
throw new Error('Could not find a delegate for given capabilities!');
|
||||
}
|
||||
return delegate;
|
||||
},
|
||||
deps: [_CHILDREN, Options.CAPABILITIES]
|
||||
}
|
||||
];
|
||||
return res;
|
||||
}
|
||||
|
||||
gc(): Promise<any> { throw new Error('NYI'); }
|
||||
|
||||
timeBegin(name: string): Promise<any> { throw new Error('NYI'); }
|
||||
|
||||
timeEnd(name: string, restartName: string): Promise<any> { throw new Error('NYI'); }
|
||||
|
||||
/**
|
||||
* Format:
|
||||
* - cat: category of the event
|
||||
* - name: event name: 'script', 'gc', 'render', ...
|
||||
* - ph: phase: 'B' (begin), 'E' (end), 'X' (Complete event), 'I' (Instant event)
|
||||
* - ts: timestamp in ms, e.g. 12345
|
||||
* - pid: process id
|
||||
* - args: arguments, e.g. {heapSize: 1234}
|
||||
*
|
||||
* Based on [Chrome Trace Event
|
||||
*Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit)
|
||||
**/
|
||||
readPerfLog(): Promise<PerfLogEvent[]> { throw new Error('NYI'); }
|
||||
|
||||
perfLogFeatures(): PerfLogFeatures { throw new Error('NYI'); }
|
||||
|
||||
supports(capabilities: {[key: string]: any}): boolean { return true; }
|
||||
}
|
||||
|
||||
export class PerfLogFeatures {
|
||||
render: boolean;
|
||||
gc: boolean;
|
||||
frameCapture: boolean;
|
||||
userTiming: boolean;
|
||||
|
||||
constructor(
|
||||
{render = false, gc = false, frameCapture = false, userTiming = false}:
|
||||
{render?: boolean, gc?: boolean, frameCapture?: boolean, userTiming?: boolean} = {}) {
|
||||
this.render = render;
|
||||
this.gc = gc;
|
||||
this.frameCapture = frameCapture;
|
||||
this.userTiming = userTiming;
|
||||
}
|
||||
}
|
||||
|
||||
const _CHILDREN = new InjectionToken('WebDriverExtension.children');
|
208
packages/benchpress/src/webdriver/chrome_driver_extension.ts
Normal file
208
packages/benchpress/src/webdriver/chrome_driver_extension.ts
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
|
||||
import {Options} from '../common_options';
|
||||
import {WebDriverAdapter} from '../web_driver_adapter';
|
||||
import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
|
||||
|
||||
/**
|
||||
* Set the following 'traceCategories' to collect metrics in Chrome:
|
||||
* 'v8,blink.console,disabled-by-default-devtools.timeline,devtools.timeline,blink.user_timing'
|
||||
*
|
||||
* In order to collect the frame rate related metrics, add 'benchmark'
|
||||
* to the list above.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ChromeDriverExtension extends WebDriverExtension {
|
||||
static PROVIDERS = [ChromeDriverExtension];
|
||||
|
||||
private _majorChromeVersion: number;
|
||||
|
||||
constructor(private _driver: WebDriverAdapter, @Inject(Options.USER_AGENT) userAgent: string) {
|
||||
super();
|
||||
this._majorChromeVersion = this._parseChromeVersion(userAgent);
|
||||
}
|
||||
|
||||
private _parseChromeVersion(userAgent: string): number {
|
||||
if (!userAgent) {
|
||||
return -1;
|
||||
}
|
||||
let v = userAgent.split(/Chrom(e|ium)\//g)[2];
|
||||
if (!v) {
|
||||
return -1;
|
||||
}
|
||||
v = v.split('.')[0];
|
||||
if (!v) {
|
||||
return -1;
|
||||
}
|
||||
return parseInt(v, 10);
|
||||
}
|
||||
|
||||
gc() { return this._driver.executeScript('window.gc()'); }
|
||||
|
||||
timeBegin(name: string): Promise<any> {
|
||||
return this._driver.executeScript(`console.time('${name}');`);
|
||||
}
|
||||
|
||||
timeEnd(name: string, restartName: string = null): Promise<any> {
|
||||
let script = `console.timeEnd('${name}');`;
|
||||
if (restartName) {
|
||||
script += `console.time('${restartName}');`;
|
||||
}
|
||||
return this._driver.executeScript(script);
|
||||
}
|
||||
|
||||
// See [Chrome Trace Event
|
||||
// Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit)
|
||||
readPerfLog(): Promise<PerfLogEvent[]> {
|
||||
// TODO(tbosch): Chromedriver bug https://code.google.com/p/chromedriver/issues/detail?id=1098
|
||||
// Need to execute at least one command so that the browser logs can be read out!
|
||||
return this._driver.executeScript('1+1')
|
||||
.then((_) => this._driver.logs('performance'))
|
||||
.then((entries) => {
|
||||
const events: PerfLogEvent[] = [];
|
||||
entries.forEach((entry: any) => {
|
||||
const message = JSON.parse(entry['message'])['message'];
|
||||
if (message['method'] === 'Tracing.dataCollected') {
|
||||
events.push(message['params']);
|
||||
}
|
||||
if (message['method'] === 'Tracing.bufferUsage') {
|
||||
throw new Error('The DevTools trace buffer filled during the test!');
|
||||
}
|
||||
});
|
||||
return this._convertPerfRecordsToEvents(events);
|
||||
});
|
||||
}
|
||||
|
||||
private _convertPerfRecordsToEvents(
|
||||
chromeEvents: Array<{[key: string]: any}>, normalizedEvents: PerfLogEvent[] = null) {
|
||||
if (!normalizedEvents) {
|
||||
normalizedEvents = [];
|
||||
}
|
||||
chromeEvents.forEach((event) => {
|
||||
const categories = this._parseCategories(event['cat']);
|
||||
const normalizedEvent = this._convertEvent(event, categories);
|
||||
if (normalizedEvent != null) normalizedEvents.push(normalizedEvent);
|
||||
});
|
||||
return normalizedEvents;
|
||||
}
|
||||
|
||||
private _convertEvent(event: {[key: string]: any}, categories: string[]) {
|
||||
const name = event['name'];
|
||||
const args = event['args'];
|
||||
if (this._isEvent(categories, name, ['blink.console'])) {
|
||||
return normalizeEvent(event, {'name': name});
|
||||
} else if (this._isEvent(
|
||||
categories, name, ['benchmark'],
|
||||
'BenchmarkInstrumentation::ImplThreadRenderingStats')) {
|
||||
// TODO(goderbauer): Instead of BenchmarkInstrumentation::ImplThreadRenderingStats the
|
||||
// following events should be used (if available) for more accurate measurments:
|
||||
// 1st choice: vsync_before - ground truth on Android
|
||||
// 2nd choice: BenchmarkInstrumentation::DisplayRenderingStats - available on systems with
|
||||
// new surfaces framework (not broadly enabled yet)
|
||||
// 3rd choice: BenchmarkInstrumentation::ImplThreadRenderingStats - fallback event that is
|
||||
// always available if something is rendered
|
||||
const frameCount = event['args']['data']['frame_count'];
|
||||
if (frameCount > 1) {
|
||||
throw new Error('multi-frame render stats not supported');
|
||||
}
|
||||
if (frameCount == 1) {
|
||||
return normalizeEvent(event, {'name': 'frame'});
|
||||
}
|
||||
} else if (
|
||||
this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'], 'Rasterize') ||
|
||||
this._isEvent(
|
||||
categories, name, ['disabled-by-default-devtools.timeline'], 'CompositeLayers')) {
|
||||
return normalizeEvent(event, {'name': 'render'});
|
||||
} else if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'MajorGC')) {
|
||||
const normArgs = {
|
||||
'majorGc': true,
|
||||
'usedHeapSize': args['usedHeapSizeAfter'] !== undefined ? args['usedHeapSizeAfter'] :
|
||||
args['usedHeapSizeBefore']
|
||||
};
|
||||
return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
|
||||
} else if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'MinorGC')) {
|
||||
const normArgs = {
|
||||
'majorGc': false,
|
||||
'usedHeapSize': args['usedHeapSizeAfter'] !== undefined ? args['usedHeapSizeAfter'] :
|
||||
args['usedHeapSizeBefore']
|
||||
};
|
||||
return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
|
||||
} else if (
|
||||
this._isEvent(categories, name, ['devtools.timeline'], 'FunctionCall') &&
|
||||
(!args || !args['data'] ||
|
||||
(args['data']['scriptName'] !== 'InjectedScript' && args['data']['scriptName'] !== ''))) {
|
||||
return normalizeEvent(event, {'name': 'script'});
|
||||
} else if (this._isEvent(categories, name, ['devtools.timeline'], 'EvaluateScript')) {
|
||||
return normalizeEvent(event, {'name': 'script'});
|
||||
} else if (this._isEvent(
|
||||
categories, name, ['devtools.timeline', 'blink'], 'UpdateLayoutTree')) {
|
||||
return normalizeEvent(event, {'name': 'render'});
|
||||
} else if (
|
||||
this._isEvent(categories, name, ['devtools.timeline'], 'UpdateLayerTree') ||
|
||||
this._isEvent(categories, name, ['devtools.timeline'], 'Layout') ||
|
||||
this._isEvent(categories, name, ['devtools.timeline'], 'Paint')) {
|
||||
return normalizeEvent(event, {'name': 'render'});
|
||||
} else if (this._isEvent(categories, name, ['devtools.timeline'], 'ResourceReceivedData')) {
|
||||
const normArgs = {'encodedDataLength': args['data']['encodedDataLength']};
|
||||
return normalizeEvent(event, {'name': 'receivedData', 'args': normArgs});
|
||||
} else if (this._isEvent(categories, name, ['devtools.timeline'], 'ResourceSendRequest')) {
|
||||
const data = args['data'];
|
||||
const normArgs = {'url': data['url'], 'method': data['requestMethod']};
|
||||
return normalizeEvent(event, {'name': 'sendRequest', 'args': normArgs});
|
||||
} else if (this._isEvent(categories, name, ['blink.user_timing'], 'navigationStart')) {
|
||||
return normalizeEvent(event, {'name': 'navigationStart'});
|
||||
}
|
||||
return null; // nothing useful in this event
|
||||
}
|
||||
|
||||
private _parseCategories(categories: string): string[] { return categories.split(','); }
|
||||
|
||||
private _isEvent(
|
||||
eventCategories: string[], eventName: string, expectedCategories: string[],
|
||||
expectedName: string = null): boolean {
|
||||
const hasCategories = expectedCategories.reduce(
|
||||
(value, cat) => value && eventCategories.indexOf(cat) !== -1, true);
|
||||
return !expectedName ? hasCategories : hasCategories && eventName === expectedName;
|
||||
}
|
||||
|
||||
perfLogFeatures(): PerfLogFeatures {
|
||||
return new PerfLogFeatures({render: true, gc: true, frameCapture: true, userTiming: true});
|
||||
}
|
||||
|
||||
supports(capabilities: {[key: string]: any}): boolean {
|
||||
return this._majorChromeVersion >= 44 && capabilities['browserName'].toLowerCase() === 'chrome';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeEvent(chromeEvent: {[key: string]: any}, data: PerfLogEvent): PerfLogEvent {
|
||||
let ph = chromeEvent['ph'].toUpperCase();
|
||||
if (ph === 'S') {
|
||||
ph = 'B';
|
||||
} else if (ph === 'F') {
|
||||
ph = 'E';
|
||||
} else if (ph === 'R') {
|
||||
// mark events from navigation timing
|
||||
ph = 'I';
|
||||
}
|
||||
const result: {[key: string]: any} =
|
||||
{'pid': chromeEvent['pid'], 'ph': ph, 'cat': 'timeline', 'ts': chromeEvent['ts'] / 1000};
|
||||
if (ph === 'X') {
|
||||
let dur = chromeEvent['dur'];
|
||||
if (dur === undefined) {
|
||||
dur = chromeEvent['tdur'];
|
||||
}
|
||||
result['dur'] = !dur ? 0.0 : dur / 1000;
|
||||
}
|
||||
for (const prop in data) {
|
||||
result[prop] = data[prop];
|
||||
}
|
||||
return result;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
import {WebDriverAdapter} from '../web_driver_adapter';
|
||||
import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
|
||||
|
||||
@Injectable()
|
||||
export class FirefoxDriverExtension extends WebDriverExtension {
|
||||
static PROVIDERS = [FirefoxDriverExtension];
|
||||
|
||||
private _profilerStarted: boolean;
|
||||
|
||||
constructor(private _driver: WebDriverAdapter) {
|
||||
super();
|
||||
this._profilerStarted = false;
|
||||
}
|
||||
|
||||
gc() { return this._driver.executeScript('window.forceGC()'); }
|
||||
|
||||
timeBegin(name: string): Promise<any> {
|
||||
if (!this._profilerStarted) {
|
||||
this._profilerStarted = true;
|
||||
this._driver.executeScript('window.startProfiler();');
|
||||
}
|
||||
return this._driver.executeScript('window.markStart("' + name + '");');
|
||||
}
|
||||
|
||||
timeEnd(name: string, restartName: string = null): Promise<any> {
|
||||
let script = 'window.markEnd("' + name + '");';
|
||||
if (restartName != null) {
|
||||
script += 'window.markStart("' + restartName + '");';
|
||||
}
|
||||
return this._driver.executeScript(script);
|
||||
}
|
||||
|
||||
readPerfLog(): Promise<PerfLogEvent> {
|
||||
return this._driver.executeAsyncScript('var cb = arguments[0]; window.getProfile(cb);');
|
||||
}
|
||||
|
||||
perfLogFeatures(): PerfLogFeatures { return new PerfLogFeatures({render: true, gc: true}); }
|
||||
|
||||
supports(capabilities: {[key: string]: any}): boolean {
|
||||
return capabilities['browserName'].toLowerCase() === 'firefox';
|
||||
}
|
||||
}
|
126
packages/benchpress/src/webdriver/ios_driver_extension.ts
Normal file
126
packages/benchpress/src/webdriver/ios_driver_extension.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
import {WebDriverAdapter} from '../web_driver_adapter';
|
||||
import {PerfLogEvent, PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
|
||||
|
||||
@Injectable()
|
||||
export class IOsDriverExtension extends WebDriverExtension {
|
||||
static PROVIDERS = [IOsDriverExtension];
|
||||
|
||||
constructor(private _driver: WebDriverAdapter) { super(); }
|
||||
|
||||
gc(): Promise<any> { throw new Error('Force GC is not supported on iOS'); }
|
||||
|
||||
timeBegin(name: string): Promise<any> {
|
||||
return this._driver.executeScript(`console.time('${name}');`);
|
||||
}
|
||||
|
||||
timeEnd(name: string, restartName: string = null): Promise<any> {
|
||||
let script = `console.timeEnd('${name}');`;
|
||||
if (restartName != null) {
|
||||
script += `console.time('${restartName}');`;
|
||||
}
|
||||
return this._driver.executeScript(script);
|
||||
}
|
||||
|
||||
// See https://github.com/WebKit/webkit/tree/master/Source/WebInspectorUI/Versions
|
||||
readPerfLog() {
|
||||
// TODO(tbosch): Bug in IOsDriver: Need to execute at least one command
|
||||
// so that the browser logs can be read out!
|
||||
return this._driver.executeScript('1+1')
|
||||
.then((_) => this._driver.logs('performance'))
|
||||
.then((entries) => {
|
||||
const records: any[] = [];
|
||||
entries.forEach((entry: any) => {
|
||||
const message = JSON.parse(entry['message'])['message'];
|
||||
if (message['method'] === 'Timeline.eventRecorded') {
|
||||
records.push(message['params']['record']);
|
||||
}
|
||||
});
|
||||
return this._convertPerfRecordsToEvents(records);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _convertPerfRecordsToEvents(records: any[], events: PerfLogEvent[] = null) {
|
||||
if (!events) {
|
||||
events = [];
|
||||
}
|
||||
records.forEach((record) => {
|
||||
let endEvent: PerfLogEvent = null;
|
||||
const type = record['type'];
|
||||
const data = record['data'];
|
||||
const startTime = record['startTime'];
|
||||
const endTime = record['endTime'];
|
||||
|
||||
if (type === 'FunctionCall' && (data == null || data['scriptName'] !== 'InjectedScript')) {
|
||||
events.push(createStartEvent('script', startTime));
|
||||
endEvent = createEndEvent('script', endTime);
|
||||
} else if (type === 'Time') {
|
||||
events.push(createMarkStartEvent(data['message'], startTime));
|
||||
} else if (type === 'TimeEnd') {
|
||||
events.push(createMarkEndEvent(data['message'], startTime));
|
||||
} else if (
|
||||
type === 'RecalculateStyles' || type === 'Layout' || type === 'UpdateLayerTree' ||
|
||||
type === 'Paint' || type === 'Rasterize' || type === 'CompositeLayers') {
|
||||
events.push(createStartEvent('render', startTime));
|
||||
endEvent = createEndEvent('render', endTime);
|
||||
}
|
||||
// Note: ios used to support GCEvent up until iOS 6 :-(
|
||||
if (record['children'] != null) {
|
||||
this._convertPerfRecordsToEvents(record['children'], events);
|
||||
}
|
||||
if (endEvent != null) {
|
||||
events.push(endEvent);
|
||||
}
|
||||
});
|
||||
return events;
|
||||
}
|
||||
|
||||
perfLogFeatures(): PerfLogFeatures { return new PerfLogFeatures({render: true}); }
|
||||
|
||||
supports(capabilities: {[key: string]: any}): boolean {
|
||||
return capabilities['browserName'].toLowerCase() === 'safari';
|
||||
}
|
||||
}
|
||||
|
||||
function createEvent(
|
||||
ph: 'X' | 'B' | 'E' | 'B' | 'E', name: string, time: number, args: any = null) {
|
||||
const result: PerfLogEvent = {
|
||||
'cat': 'timeline',
|
||||
'name': name,
|
||||
'ts': time,
|
||||
'ph': ph,
|
||||
// The ios protocol does not support the notions of multiple processes in
|
||||
// the perflog...
|
||||
'pid': 'pid0'
|
||||
};
|
||||
if (args != null) {
|
||||
result['args'] = args;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function createStartEvent(name: string, time: number, args: any = null) {
|
||||
return createEvent('B', name, time, args);
|
||||
}
|
||||
|
||||
function createEndEvent(name: string, time: number, args: any = null) {
|
||||
return createEvent('E', name, time, args);
|
||||
}
|
||||
|
||||
function createMarkStartEvent(name: string, time: number) {
|
||||
return createEvent('B', name, time);
|
||||
}
|
||||
|
||||
function createMarkEndEvent(name: string, time: number) {
|
||||
return createEvent('E', name, time);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {WebDriverAdapter} from '../web_driver_adapter';
|
||||
|
||||
/**
|
||||
* Adapter for the selenium-webdriver.
|
||||
*/
|
||||
export class SeleniumWebDriverAdapter extends WebDriverAdapter {
|
||||
static PROTRACTOR_PROVIDERS = [{
|
||||
provide: WebDriverAdapter,
|
||||
useFactory: () => new SeleniumWebDriverAdapter((<any>global).browser)
|
||||
}];
|
||||
|
||||
constructor(private _driver: any) { super(); }
|
||||
|
||||
waitFor(callback: () => any): Promise<any> { return this._driver.call(callback); }
|
||||
|
||||
executeScript(script: string): Promise<any> { return this._driver.executeScript(script); }
|
||||
|
||||
executeAsyncScript(script: string): Promise<any> {
|
||||
return this._driver.executeAsyncScript(script);
|
||||
}
|
||||
|
||||
capabilities(): Promise<{[key: string]: any}> {
|
||||
return this._driver.getCapabilities().then((capsObject: any) => {
|
||||
const localData: {[key: string]: any} = {};
|
||||
capsObject.forEach((value: any, key: string) => { localData[key] = value; });
|
||||
return localData;
|
||||
});
|
||||
}
|
||||
|
||||
logs(type: string): Promise<any> {
|
||||
// Needed as selenium-webdriver does not forward
|
||||
// performance logs in the correct way via manage().logs
|
||||
return this._driver.schedule(
|
||||
new Command('getLog').setParameter('type', type),
|
||||
'WebDriver.manage().logs().get(' + type + ')');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of the `Command` class of webdriver as
|
||||
* it is not exposed via index.js in selenium-webdriver.
|
||||
*/
|
||||
class Command {
|
||||
private parameters_: {[key: string]: any} = {};
|
||||
constructor(private name_: string) {}
|
||||
|
||||
getName() { return this.name_; }
|
||||
|
||||
setParameter(name: string, value: any) {
|
||||
this.parameters_[name] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setParameters(parameters: {[key: string]: any}) {
|
||||
this.parameters_ = parameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
getParameter(key: string) { return this.parameters_[key]; }
|
||||
|
||||
getParameters() { return this.parameters_; }
|
||||
}
|
Reference in New Issue
Block a user