chore: move benchpress to @angular/benchpress

This commit is contained in:
Tobias Bosch
2016-08-26 16:13:54 -07:00
parent ef0f29c372
commit db280fc67e
72 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,68 @@
/**
* @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 {OpaqueToken} from '@angular/core/src/di';
import {DateWrapper} from '@angular/facade/src/lang';
export class Options {
static get DEFAULT_PROVIDERS(): any[] { return _DEFAULT_PROVIDERS; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get SAMPLE_ID() { return _SAMPLE_ID; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get DEFAULT_DESCRIPTION() { return _DEFAULT_DESCRIPTION; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get SAMPLE_DESCRIPTION() { return _SAMPLE_DESCRIPTION; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get FORCE_GC() { return _FORCE_GC; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get PREPARE() { return _PREPARE; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get EXECUTE() { return _EXECUTE; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get CAPABILITIES() { return _CAPABILITIES; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get USER_AGENT() { return _USER_AGENT; }
// TODO(tbosch): use static initializer when our transpiler supports it
static get NOW() { return _NOW; }
// TODO(tbosch): use static values when our transpiler supports them
static get WRITE_FILE() { return _WRITE_FILE; }
// TODO(tbosch): use static values when our transpiler supports them
static get MICRO_METRICS() { return _MICRO_METRICS; }
// TODO(tbosch): use static values when our transpiler supports them
static get USER_METRICS() { return _USER_METRICS; }
// TODO(tbosch): use static values when our transpiler supports them
static get RECEIVED_DATA() { return _RECEIVED_DATA; }
// TODO(tbosch): use static values when our transpiler supports them
static get REQUEST_COUNT() { return _REQUEST_COUNT; }
// TODO(tbosch): use static values when our transpiler supports them
static get CAPTURE_FRAMES() { return _CAPTURE_FRAMES; }
}
var _SAMPLE_ID = new OpaqueToken('Options.sampleId');
var _DEFAULT_DESCRIPTION = new OpaqueToken('Options.defaultDescription');
var _SAMPLE_DESCRIPTION = new OpaqueToken('Options.sampleDescription');
var _FORCE_GC = new OpaqueToken('Options.forceGc');
var _PREPARE = new OpaqueToken('Options.prepare');
var _EXECUTE = new OpaqueToken('Options.execute');
var _CAPABILITIES = new OpaqueToken('Options.capabilities');
var _USER_AGENT = new OpaqueToken('Options.userAgent');
var _MICRO_METRICS = new OpaqueToken('Options.microMetrics');
var _USER_METRICS = new OpaqueToken('Options.userMetrics');
var _NOW = new OpaqueToken('Options.now');
var _WRITE_FILE = new OpaqueToken('Options.writeFile');
var _RECEIVED_DATA = new OpaqueToken('Options.receivedData');
var _REQUEST_COUNT = new OpaqueToken('Options.requestCount');
var _CAPTURE_FRAMES = new OpaqueToken('Options.frameCapture');
var _DEFAULT_PROVIDERS = [
{provide: _DEFAULT_DESCRIPTION, useValue: {}}, {provide: _SAMPLE_DESCRIPTION, useValue: {}},
{provide: _FORCE_GC, useValue: false}, {provide: _PREPARE, useValue: false},
{provide: _MICRO_METRICS, useValue: {}}, {provide: _USER_METRICS, useValue: {}},
{provide: _NOW, useValue: () => DateWrapper.now()}, {provide: _RECEIVED_DATA, useValue: false},
{provide: _REQUEST_COUNT, useValue: false}, {provide: _CAPTURE_FRAMES, useValue: false}
];

View File

@ -0,0 +1,2 @@
*.xpi
addon-sdk*

View File

@ -0,0 +1,3 @@
library benchpress.src.firefox_extension.data.installed_script;
//no dart implementation

View File

@ -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;
declare var unsafeWindow;
exportFunction(function() {
var 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) {
(<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) {
var curTime = unsafeWindow.performance.now();
(<any>self).port.emit('markStart', name, curTime);
}, unsafeWindow, {defineAs: 'markStart'});
exportFunction(function(name) {
var curTime = unsafeWindow.performance.now();
(<any>self).port.emit('markEnd', name, curTime);
}, unsafeWindow, {defineAs: 'markEnd'});

View File

@ -0,0 +1,3 @@
library benchpress.src.firefox_extension.lib.main;
//no dart implementation

View File

@ -0,0 +1,74 @@
/**
* @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
*/
var {Cc, Ci, Cu} = require('chrome');
var os = Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
var ParserUtil = require('./parser_util');
class Profiler {
private _profiler;
private _markerEvents: any[];
private _profilerStartTime: number;
constructor() { this._profiler = Cc['@mozilla.org/tools/profiler;1'].getService(Ci.nsIProfiler); }
start(entries, interval, features, timeStarted) {
this._profiler.StartProfiler(entries, interval, features, features.length);
this._profilerStartTime = timeStarted;
this._markerEvents = [];
}
stop() { this._profiler.StopProfiler(); }
getProfilePerfEvents() {
var profileData = this._profiler.getProfileData();
var perfEvents = ParserUtil.convertPerfProfileToEvents(profileData);
perfEvents = this._mergeMarkerEvents(perfEvents);
perfEvents.sort(function(event1, event2) { 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);
};
var mod = require('sdk/page-mod');
var data = require('sdk/self').data;
var profiler = new Profiler();
mod.PageMod({
include: ['*'],
contentScriptFile: data.url('installed_script.js'),
onAttach: worker => {
worker.port.on(
'startProfiler',
(timeStarted) => 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, timeStarted) => profiler.addStartEvent(name, timeStarted));
worker.port.on('markEnd', (name, timeEnded) => profiler.addEndEvent(name, timeEnded));
}
});

View File

@ -0,0 +1,3 @@
library benchpress.src.firefox_extension.lib.parser_util;
//no dart implementation

View File

@ -0,0 +1,90 @@
/**
* @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[] {
var inProgressEvents = new Map(); // map from event name to start time
var finishedEvents = []; // Event[] finished events
var addFinishedEvent = function(eventName, startTime, endTime) {
var categorizedEventName = categorizeEvent(eventName);
var args = 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});
}
};
var 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 (var i = 0; i < samples.length; ++i) {
var sample = samples[i];
var sampleTime = sample.time;
// Add all the frames into a set so it's easier/faster to find the set
// differences
var sampleFrames = new Set();
sample.frames.forEach(function(frame) { 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.
var 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.
var 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';
}
}

View File

@ -0,0 +1,3 @@
library benchpress.src.firefox_extension.lib.test_helper;
//no dart implementation

View 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
*/
var q = require('q');
var FirefoxProfile = require('firefox-profile');
var jpm = require('jpm/lib/xpi');
var pathUtil = require('path');
var PERF_ADDON_PACKAGE_JSON_DIR = '..';
exports.getAbsolutePath = function(path) {
var normalizedPath = pathUtil.normalize(path);
if (pathUtil.resolve(normalizedPath) == normalizedPath) {
// Already absolute path
return normalizedPath;
} else {
return pathUtil.join(__dirname, normalizedPath);
}
};
exports.getFirefoxProfile = function(extensionPath) {
var deferred = q.defer();
var firefoxProfile = new FirefoxProfile();
firefoxProfile.addExtensions([extensionPath], () => {
firefoxProfile.encoded(encodedProfile => {
var multiCapabilities = [{browserName: 'firefox', firefox_profile: encodedProfile}];
deferred.resolve(multiCapabilities);
});
});
return deferred.promise;
};
exports.getFirefoxProfileWithExtension = function() {
var absPackageJsonDir = pathUtil.join(__dirname, PERF_ADDON_PACKAGE_JSON_DIR);
var packageJson = require(pathUtil.join(absPackageJsonDir, 'package.json'));
var savedCwd = process.cwd();
process.chdir(absPackageJsonDir);
return jpm(packageJson).then(xpiPath => {
process.chdir(savedCwd);
return exports.getFirefoxProfile(xpiPath);
});
};

View File

@ -0,0 +1 @@
{ "version" : "0.0.1", "main" : "lib/main.js", "name" : "ffperf-addon" }

View File

@ -0,0 +1,23 @@
/**
* @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 {Map} from '@angular/facade/src/collection';
import {Date, DateWrapper} from '@angular/facade/src/lang';
export class MeasureValues {
constructor(
public runIndex: number, public timeStamp: Date, public values: {[key: string]: any}) {}
toJson() {
return {
'timeStamp': DateWrapper.toJson(this.timeStamp),
'runIndex': this.runIndex,
'values': this.values
};
}
}

View File

@ -0,0 +1,35 @@
/**
* @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 {
static bindTo(delegateToken): any[] {
return [{provide: Metric, useFactory: (delegate) => delegate, deps: [delegateToken]}];
}
/**
* 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]: any} { throw new Error('NYI'); }
}

View File

@ -0,0 +1,61 @@
/**
* @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 {Injector, OpaqueToken} from '@angular/core/src/di';
import {StringMapWrapper} from '@angular/facade/src/collection';
import {Metric} from '../metric';
export class MultiMetric extends Metric {
static createBindings(childTokens: any[]): any[] {
return [
{
provide: _CHILDREN,
useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),
deps: [Injector]
},
{provide: MultiMetric, useFactory: children => 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} {
var result = {};
maps.forEach(
map => { StringMapWrapper.forEach(map, (value, prop) => { result[prop] = value; }); });
return result;
}
var _CHILDREN = new OpaqueToken('MultiMetric.children');

View File

@ -0,0 +1,403 @@
/**
* @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 {OpaqueToken} from '@angular/core/src/di';
import {ListWrapper, StringMapWrapper} from '@angular/facade/src/collection';
import {Math, NumberWrapper, StringWrapper, isBlank, isPresent} from '@angular/facade/src/lang';
import {Options} from '../common_options';
import {Metric} from '../metric';
import {PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
/**
* A metric that reads out the performance log
*/
export class PerflogMetric extends Metric {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
// TODO(tbosch): use static values when our transpiler supports them
static get SET_TIMEOUT(): OpaqueToken { return _SET_TIMEOUT; }
/** @internal */
private _remainingEvents: Array<{[key: string]: any}>;
/** @internal */
private _measureCount: number;
/** @internal */
private _perfLogFeatures: PerfLogFeatures;
/**
* @param driverExtension
* @param setTimeout
* @param microMetrics Name and description of metrics provided via console.time / console.timeEnd
**/
constructor(
/** @internal */
private _driverExtension: WebDriverExtension,
/** @internal */
private _setTimeout: Function,
/** @internal */
private _microMetrics: {[key: string]: any},
/** @internal */
private _forceGc: boolean,
/** @internal */
private _captureFrames: boolean,
/** @internal */
private _receivedData: boolean,
/** @internal */
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]: any} {
var res = {
'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) {
var 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';
}
}
StringMapWrapper.forEach(
this._microMetrics, (desc, name) => { StringMapWrapper.set(res, name, desc); });
return res;
}
beginMeasure(): Promise<any> {
var resultPromise = Promise.resolve(null);
if (this._forceGc) {
resultPromise = resultPromise.then((_) => this._driverExtension.gc());
}
return resultPromise.then((_) => this._beginMeasure());
}
endMeasure(restart: boolean): Promise<{[key: string]: any}> {
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
var originalFrameCaptureValue = this._captureFrames;
this._captureFrames = false;
return this._driverExtension.gc()
.then((_) => this._endMeasure(restartMeasure))
.then((forceGcMeasureValues) => {
this._captureFrames = originalFrameCaptureValue;
StringMapWrapper.set(measureValues, 'forcedGcTime', forceGcMeasureValues['gcTime']);
StringMapWrapper.set(measureValues, 'forcedGcAmount', forceGcMeasureValues['gcAmount']);
return measureValues;
});
});
}
/** @internal */
private _beginMeasure(): Promise<any> {
return this._driverExtension.timeBegin(this._markName(this._measureCount++));
}
/** @internal */
private _endMeasure(restart: boolean): Promise<{[key: string]: any}> {
var markName = this._markName(this._measureCount - 1);
var nextMarkName = restart ? this._markName(this._measureCount++) : null;
return this._driverExtension.timeEnd(markName, nextMarkName)
.then((_) => this._readUntilEndMark(markName));
}
/** @internal */
private _readUntilEndMark(markName: string, loopCount: number = 0, startEvent = 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);
var result = this._aggregateEvents(this._remainingEvents, markName);
if (isPresent(result)) {
this._remainingEvents = events;
return result;
}
var resolve: (result: any) => void;
var promise = new Promise(res => { resolve = res; });
this._setTimeout(() => resolve(this._readUntilEndMark(markName, loopCount + 1)), 100);
return promise;
});
}
/** @internal */
private _addEvents(events: {[key: string]: string}[]) {
var needSort = false;
events.forEach(event => {
if (StringWrapper.equals(event['ph'], 'X')) {
needSort = true;
var startEvent = {};
var endEvent = {};
StringMapWrapper.forEach(event, (value, prop) => {
startEvent[prop] = value;
endEvent[prop] = value;
});
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
ListWrapper.sort(this._remainingEvents, (a, b) => {
var diff = a['ts'] - b['ts'];
return diff > 0 ? 1 : diff < 0 ? -1 : 0;
});
}
}
/** @internal */
private _aggregateEvents(events: Array<{[key: string]: any}>, markName): {[key: string]: any} {
var result = {'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;
}
StringMapWrapper.forEach(this._microMetrics, (desc, name) => { result[name] = 0; });
if (this._receivedData) {
result['receivedData'] = 0;
}
if (this._requestCount) {
result['requestCount'] = 0;
}
var markStartEvent = null;
var markEndEvent = null;
var gcTimeInScript = 0;
var renderTimeInScript = 0;
var frameTimestamps = [];
var frameTimes = [];
var frameCaptureStartEvent = null;
var frameCaptureEndEvent = null;
var intervalStarts: {[key: string]: any} = {};
var intervalStartCount: {[key: string]: number} = {};
events.forEach((event) => {
var ph = event['ph'];
var name = event['name'];
var microIterations = 1;
var microIterationsMatch = name.match(_MICRO_ITERATIONS_REGEX);
if (isPresent(microIterationsMatch)) {
name = microIterationsMatch[1];
microIterations = NumberWrapper.parseInt(microIterationsMatch[2], 10);
}
if (StringWrapper.equals(ph, 'b') && StringWrapper.equals(name, markName)) {
markStartEvent = event;
} else if (StringWrapper.equals(ph, 'e') && StringWrapper.equals(name, markName)) {
markEndEvent = event;
}
let isInstant = StringWrapper.equals(ph, 'I') || StringWrapper.equals(ph, 'i');
if (this._requestCount && StringWrapper.equals(name, 'sendRequest')) {
result['requestCount'] += 1;
} else if (this._receivedData && StringWrapper.equals(name, 'receivedData') && isInstant) {
result['receivedData'] += event['args']['encodedDataLength'];
} else if (StringWrapper.equals(name, 'navigationStart')) {
// We count data + requests since the last navigationStart
// (there might be chrome extensions loaded by selenium before our page, so there
// will likely be more than one navigationStart).
if (this._receivedData) {
result['receivedData'] = 0;
}
if (this._requestCount) {
result['requestCount'] = 0;
}
}
if (isPresent(markStartEvent) && isBlank(markEndEvent) &&
event['pid'] === markStartEvent['pid']) {
if (StringWrapper.equals(ph, 'b') && StringWrapper.equals(name, _MARK_NAME_FRAME_CAPUTRE)) {
if (isPresent(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 (
StringWrapper.equals(ph, 'e') && StringWrapper.equals(name, _MARK_NAME_FRAME_CAPUTRE)) {
if (isBlank(frameCaptureStartEvent)) {
throw new Error('missing start event for frame capture');
}
frameCaptureEndEvent = event;
}
if (isInstant) {
if (isPresent(frameCaptureStartEvent) && isBlank(frameCaptureEndEvent) &&
StringWrapper.equals(name, 'frame')) {
frameTimestamps.push(event['ts']);
if (frameTimestamps.length >= 2) {
frameTimes.push(
frameTimestamps[frameTimestamps.length - 1] -
frameTimestamps[frameTimestamps.length - 2]);
}
}
}
if (StringWrapper.equals(ph, 'B') || StringWrapper.equals(ph, 'b')) {
if (isBlank(intervalStarts[name])) {
intervalStartCount[name] = 1;
intervalStarts[name] = event;
} else {
intervalStartCount[name]++;
}
} else if (
(StringWrapper.equals(ph, 'E') || StringWrapper.equals(ph, 'e')) &&
isPresent(intervalStarts[name])) {
intervalStartCount[name]--;
if (intervalStartCount[name] === 0) {
var startEvent = intervalStarts[name];
var duration = (event['ts'] - startEvent['ts']);
intervalStarts[name] = null;
if (StringWrapper.equals(name, 'gc')) {
result['gcTime'] += duration;
var amount =
(startEvent['args']['usedHeapSize'] - event['args']['usedHeapSize']) / 1000;
result['gcAmount'] += amount;
var majorGc = event['args']['majorGc'];
if (isPresent(majorGc) && majorGc) {
result['majorGcTime'] += duration;
}
if (isPresent(intervalStarts['script'])) {
gcTimeInScript += duration;
}
} else if (StringWrapper.equals(name, 'render')) {
result['renderTime'] += duration;
if (isPresent(intervalStarts['script'])) {
renderTimeInScript += duration;
}
} else if (StringWrapper.equals(name, 'script')) {
result['scriptTime'] += duration;
} else if (isPresent(this._microMetrics[name])) {
result[name] += duration / microIterations;
}
}
}
}
});
if (!isPresent(markStartEvent) || !isPresent(markEndEvent)) {
// not all events have been received, no further processing for now
return null;
}
if (isPresent(markEndEvent) && isPresent(frameCaptureStartEvent) &&
isBlank(frameCaptureEndEvent)) {
throw new Error('missing end event for frame capture');
}
if (this._captureFrames && isBlank(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;
}
/** @internal */
private _addFrameMetrics(result: {[key: string]: any}, frameTimes: any[]) {
result['frameTime.mean'] = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
var 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;
}
/** @internal */
private _markName(index) { return `${_MARK_NAME_PREFIX}${index}`; }
}
var _MICRO_ITERATIONS_REGEX = /(.+)\*(\d+)$/;
var _MAX_RETRY_COUNT = 20;
var _MARK_NAME_PREFIX = 'benchpress';
var _SET_TIMEOUT = new OpaqueToken('PerflogMetric.setTimeout');
var _MARK_NAME_FRAME_CAPUTRE = 'frameCapture';
// using 17ms as a somewhat looser threshold, instead of 16.6666ms
var _FRAME_TIME_SMOOTH_THRESHOLD = 17;
var _PROVIDERS = [
{
provide: PerflogMetric,
useFactory: (driverExtension, setTimeout, microMetrics, forceGc, captureFrames, receivedData,
requestCount) =>
new PerflogMetric(
driverExtension, setTimeout, microMetrics, forceGc, captureFrames,
receivedData, requestCount),
deps: [
WebDriverExtension, _SET_TIMEOUT, Options.MICRO_METRICS, Options.FORCE_GC,
Options.CAPTURE_FRAMES, Options.RECEIVED_DATA, Options.REQUEST_COUNT
]
},
{provide: _SET_TIMEOUT, useValue: (fn, millis) => <any>setTimeout(fn, millis)}
];

View File

@ -0,0 +1,75 @@
/**
* @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 {OpaqueToken, Provider} from '@angular/core';
import {StringMapWrapper} from '@angular/facade/src/collection';
import {isNumber} from '@angular/facade/src/lang';
import {Options} from '../common_options';
import {Metric} from '../metric';
import {WebDriverAdapter} from '../web_driver_adapter';
export class UserMetric extends Metric {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): Provider[] { return _PROVIDERS; }
constructor(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;
let promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
let adapter = this._wdAdapter;
let names = StringMapWrapper.keys(this._userMetrics);
function getAndClearValues() {
Promise.all(names.map(name => adapter.executeScript(`return window.${name}`)))
.then((values: any[]) => {
if (values.every(isNumber)) {
Promise.all(names.map(name => adapter.executeScript(`delete window.${name}`)))
.then((_: any[]) => {
let map = StringMapWrapper.create();
for (let i = 0, n = names.length; i < n; i++) {
StringMapWrapper.set(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; }
}
var _PROVIDERS: Provider[] = [{
provide: UserMetric,
useFactory: (userMetrics, wdAdapter) => new UserMetric(userMetrics, wdAdapter),
deps: [Options.USER_METRICS, WebDriverAdapter]
}];

View File

@ -0,0 +1,24 @@
/**
* @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 {
static bindTo(delegateToken): any[] {
return [{provide: Reporter, useFactory: (delegate) => delegate, deps: [delegateToken]}];
}
reportMeasureValues(values: MeasureValues): Promise<any> { throw new Error('NYI'); }
reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]): Promise<any> {
throw new Error('NYI');
}
}

View File

@ -0,0 +1,116 @@
/**
* @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 {OpaqueToken} from '@angular/core/src/di';
import {ListWrapper, StringMapWrapper} from '@angular/facade/src/collection';
import {NumberWrapper, isBlank, isPresent, print} from '@angular/facade/src/lang';
import {Math} from '@angular/facade/src/math';
import {MeasureValues} from '../measure_values';
import {Reporter} from '../reporter';
import {SampleDescription} from '../sample_description';
import {Statistic} from '../statistic';
/**
* A reporter for the console
*/
export class ConsoleReporter extends Reporter {
// TODO(tbosch): use static values when our transpiler supports them
static get PRINT(): OpaqueToken { return _PRINT; }
// TODO(tbosch): use static values when our transpiler supports them
static get COLUMN_WIDTH(): OpaqueToken { return _COLUMN_WIDTH; }
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
/** @internal */
private static _lpad(value, columnWidth, fill = ' ') {
var result = '';
for (var i = 0; i < columnWidth - value.length; i++) {
result += fill;
}
return result + value;
}
/** @internal */
private static _formatNum(n) { return NumberWrapper.toFixed(n, 2); }
/** @internal */
private static _sortedProps(obj) {
var props = [];
StringMapWrapper.forEach(obj, (value, prop) => props.push(prop));
props.sort();
return props;
}
/** @internal */
private _metricNames: string[];
constructor(private _columnWidth: number, sampleDescription, private _print: Function) {
super();
this._metricNames = ConsoleReporter._sortedProps(sampleDescription.metrics);
this._printDescription(sampleDescription);
}
/** @internal */
private _printDescription(sampleDescription) {
this._print(`BENCHMARK ${sampleDescription.id}`);
this._print('Description:');
var props = ConsoleReporter._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> {
var formattedValues = this._metricNames.map(metricName => {
var value = measureValues.values[metricName];
return ConsoleReporter._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 => {
var samples = validSamples.map(measureValues => measureValues.values[metricName]);
var mean = Statistic.calculateMean(samples);
var cv = Statistic.calculateCoefficientOfVariation(samples, mean);
var formattedMean = ConsoleReporter._formatNum(mean);
// Note: Don't use the unicode character for +- as it might cause
// hickups for consoles...
return NumberWrapper.isNaN(cv) ? formattedMean : `${formattedMean}+-${Math.floor(cv)}%`;
}));
return Promise.resolve(null);
}
/** @internal */
private _printStringRow(parts: any[], fill = ' ') {
this._print(
parts.map(part => ConsoleReporter._lpad(part, this._columnWidth, fill)).join(' | '));
}
}
var _PRINT = new OpaqueToken('ConsoleReporter.print');
var _COLUMN_WIDTH = new OpaqueToken('ConsoleReporter.columnWidth');
var _PROVIDERS = [
{
provide: ConsoleReporter,
useFactory: (columnWidth, sampleDescription, print) =>
new ConsoleReporter(columnWidth, sampleDescription, print),
deps: [_COLUMN_WIDTH, SampleDescription, _PRINT]
},
{provide: _COLUMN_WIDTH, useValue: 18}, {provide: _PRINT, useValue: print}
];

View File

@ -0,0 +1,67 @@
/**
* @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 {OpaqueToken} from '@angular/core/src/di';
import {DateWrapper, Json, isBlank, isPresent} from '@angular/facade/src/lang';
import {Options} from '../common_options';
import {MeasureValues} from '../measure_values';
import {Reporter} from '../reporter';
import {SampleDescription} from '../sample_description';
/**
* A reporter that writes results into a json file.
*/
export class JsonFileReporter extends Reporter {
// TODO(tbosch): use static values when our transpiler supports them
static get PATH(): OpaqueToken { return _PATH; }
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
/** @internal */
private _writeFile: Function;
/** @internal */
private _path: string;
/** @internal */
private _description: SampleDescription;
/** @internal */
private _now: Function;
constructor(sampleDescription, path, writeFile, now) {
super();
this._description = sampleDescription;
this._path = path;
this._writeFile = writeFile;
this._now = now;
}
reportMeasureValues(measureValues: MeasureValues): Promise<any> { return Promise.resolve(null); }
reportSample(completeSample: MeasureValues[], validSample: MeasureValues[]): Promise<any> {
var content = Json.stringify({
'description': this._description,
'completeSample': completeSample,
'validSample': validSample
});
var filePath =
`${this._path}/${this._description.id}_${DateWrapper.toMillis(this._now())}.json`;
return this._writeFile(filePath, content);
}
}
var _PATH = new OpaqueToken('JsonFileReporter.path');
var _PROVIDERS = [
{
provide: JsonFileReporter,
useFactory: (sampleDescription, path, writeFile, now) =>
new JsonFileReporter(sampleDescription, path, writeFile, now),
deps: [SampleDescription, _PATH, Options.WRITE_FILE, Options.NOW]
},
{provide: _PATH, useValue: '.'}
];

View File

@ -0,0 +1,48 @@
/**
* @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 {Injector, OpaqueToken} from '@angular/core/src/di';
import {MeasureValues} from '../measure_values';
import {Reporter} from '../reporter';
export class MultiReporter extends Reporter {
static createBindings(childTokens: any[]): any[] {
return [
{
provide: _CHILDREN,
useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),
deps: [Injector],
},
{
provide: MultiReporter,
useFactory: children => new MultiReporter(children),
deps: [_CHILDREN]
}
];
}
/** @internal */
private _reporters: Reporter[];
constructor(reporters) {
super();
this._reporters = reporters;
}
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)));
}
}
var _CHILDREN = new OpaqueToken('MultiReporter.children');

View File

@ -0,0 +1,114 @@
/**
* @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 {isBlank, isPresent} from '@angular/facade/src/lang';
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 {
private _defaultProviders: Provider[];
constructor(defaultProviders: Provider[] = null) {
if (isBlank(defaultProviders)) {
defaultProviders = [];
}
this._defaultProviders = defaultProviders;
}
sample({id, execute, prepare, microMetrics, providers, userMetrics}: {
id: string,
execute?: any,
prepare?: any,
microMetrics?: any,
providers?: any,
userMetrics?: any
}): Promise<SampleState> {
var sampleProviders = [
_DEFAULT_PROVIDERS, this._defaultProviders, {provide: Options.SAMPLE_ID, useValue: id},
{provide: Options.EXECUTE, useValue: execute}
];
if (isPresent(prepare)) {
sampleProviders.push({provide: Options.PREPARE, useValue: prepare});
}
if (isPresent(microMetrics)) {
sampleProviders.push({provide: Options.MICRO_METRICS, useValue: microMetrics});
}
if (isPresent(userMetrics)) {
sampleProviders.push({provide: Options.USER_METRICS, useValue: userMetrics});
}
if (isPresent(providers)) {
sampleProviders.push(providers);
}
var inj = ReflectiveInjector.resolveAndCreate(sampleProviders);
var adapter = inj.get(WebDriverAdapter);
return Promise
.all([adapter.capabilities(), adapter.executeScript('return window.navigator.userAgent;')])
.then((args) => {
var capabilities = args[0];
var 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.
var injector = ReflectiveInjector.resolveAndCreate([
sampleProviders, {provide: Options.CAPABILITIES, useValue: capabilities},
{provide: Options.USER_AGENT, useValue: userAgent},
{provide: WebDriverAdapter, useValue: adapter}
]);
var sampler = injector.get(Sampler);
return sampler.sample();
});
}
}
var _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.createBindings([ConsoleReporter]),
MultiMetric.createBindings([PerflogMetric, UserMetric]),
Reporter.bindTo(MultiReporter),
Validator.bindTo(RegressionSlopeValidator),
WebDriverExtension.bindTo([ChromeDriverExtension, FirefoxDriverExtension, IOsDriverExtension]),
Metric.bindTo(MultiMetric),
];

View File

@ -0,0 +1,50 @@
/**
* @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 {StringMapWrapper} from '@angular/facade/src/collection';
import {Options} from './common_options';
import {Metric} from './metric';
import {Validator} from './validator';
/**
* SampleDescription merges all available descriptions about a sample
*/
export class SampleDescription {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
description: {[key: string]: any};
constructor(
public id: string, descriptions: Array<{[key: string]: any}>,
public metrics: {[key: string]: any}) {
this.description = {};
descriptions.forEach(description => {
StringMapWrapper.forEach(description, (value, prop) => this.description[prop] = value);
});
}
toJson() { return {'id': this.id, 'description': this.description, 'metrics': this.metrics}; }
}
var _PROVIDERS = [{
provide: SampleDescription,
useFactory: (metric, id, forceGc, userAgent, validator, defaultDesc, userDesc) =>
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
]
}];

View File

@ -0,0 +1,129 @@
/**
* @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 {Date, DateWrapper, isBlank, isPresent} from '@angular/facade/src/lang';
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
*/
export class Sampler {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
/** @internal */
private _driver: WebDriverAdapter;
/** @internal */
private _metric: Metric;
/** @internal */
private _reporter: Reporter;
/** @internal */
private _validator: Validator;
/** @internal */
private _prepare: Function;
/** @internal */
private _execute: Function;
/** @internal */
private _now: Function;
constructor({driver, metric, reporter, validator, prepare, execute, now}: {
driver?: WebDriverAdapter,
metric?: Metric,
reporter?: Reporter,
validator?: Validator,
prepare?: Function,
execute?: Function,
now?: Function
} = {}) {
this._driver = driver;
this._metric = metric;
this._reporter = reporter;
this._validator = validator;
this._prepare = prepare;
this._execute = execute;
this._now = now;
}
sample(): Promise<SampleState> {
var loop;
loop = (lastState) => {
return this._iterate(lastState).then((newState) => {
if (isPresent(newState.validSample)) {
return newState;
} else {
return loop(newState);
}
});
};
return loop(new SampleState([], null));
}
/** @internal */
private _iterate(lastState): Promise<SampleState> {
var resultPromise: Promise<any>;
if (isPresent(this._prepare)) {
resultPromise = this._driver.waitFor(this._prepare);
} else {
resultPromise = Promise.resolve(null);
}
if (isPresent(this._prepare) || lastState.completeSample.length === 0) {
resultPromise = resultPromise.then((_) => this._metric.beginMeasure());
}
return resultPromise.then((_) => this._driver.waitFor(this._execute))
.then((_) => this._metric.endMeasure(isBlank(this._prepare)))
.then((measureValues) => this._report(lastState, measureValues));
}
/** @internal */
private _report(state: SampleState, metricValues: {[key: string]: any}): Promise<SampleState> {
var measureValues = new MeasureValues(state.completeSample.length, this._now(), metricValues);
var completeSample = state.completeSample.concat([measureValues]);
var validSample = this._validator.validate(completeSample);
var resultPromise = this._reporter.reportMeasureValues(measureValues);
if (isPresent(validSample)) {
resultPromise =
resultPromise.then((_) => this._reporter.reportSample(completeSample, validSample));
}
return resultPromise.then((_) => new SampleState(completeSample, validSample));
}
}
export class SampleState {
constructor(public completeSample: any[], public validSample: any[]) {}
}
var _PROVIDERS = [{
provide: Sampler,
useFactory: (driver, metric, reporter, validator, prepare, execute, now) => new Sampler({
driver: driver,
reporter: reporter,
validator: validator,
metric: metric,
// TODO(tbosch): DI right now does not support null/undefined objects
// Mostly because the cache would have to be initialized with a
// special null object, which is expensive.
prepare: prepare !== false ? prepare : null,
execute: execute,
now: now
}),
deps: [
WebDriverAdapter, Metric, Reporter, Validator, Options.PREPARE, Options.EXECUTE, Options.NOW
]
}];

View File

@ -0,0 +1,43 @@
/**
* @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 {Math} from '@angular/facade/src/math';
export class Statistic {
static calculateCoefficientOfVariation(sample, mean) {
return Statistic.calculateStandardDeviation(sample, mean) / mean * 100;
}
static calculateMean(samples: number[]) {
var total = 0;
// TODO: use reduce
samples.forEach(x => total += x);
return total / samples.length;
}
static calculateStandardDeviation(samples: number[], mean) {
var 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
var dividendSum = 0;
var divisorSum = 0;
for (var i = 0; i < xValues.length; i++) {
dividendSum += (xValues[i] - xMean) * (yValues[i] - yMean);
divisorSum += Math.pow(xValues[i] - xMean, 2);
}
return dividendSum / divisorSum;
}
}

View 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
*/
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 {
static bindTo(delegateToken): any[] {
return [{provide: Validator, useFactory: (delegate) => delegate, deps: [delegateToken]}];
}
/**
* 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'); }
}

View File

@ -0,0 +1,74 @@
/**
* @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 {OpaqueToken} from '@angular/core/src/di';
import {ListWrapper} from '@angular/facade/src/collection';
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.
*/
export class RegressionSlopeValidator extends Validator {
// TODO(tbosch): use static values when our transpiler supports them
static get SAMPLE_SIZE(): OpaqueToken { return _SAMPLE_SIZE; }
// TODO(tbosch): use static values when our transpiler supports them
static get METRIC(): OpaqueToken { return _METRIC; }
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
/** @internal */
private _sampleSize: number;
/** @internal */
private _metric: string;
constructor(sampleSize, metric) {
super();
this._sampleSize = sampleSize;
this._metric = metric;
}
describe(): {[key: string]: any} {
return {'sampleSize': this._sampleSize, 'regressionSlopeMetric': this._metric};
}
validate(completeSample: MeasureValues[]): MeasureValues[] {
if (completeSample.length >= this._sampleSize) {
var latestSample = ListWrapper.slice(
completeSample, completeSample.length - this._sampleSize, completeSample.length);
var xValues = [];
var yValues = [];
for (var 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]);
}
var regressionSlope = Statistic.calculateRegressionSlope(
xValues, Statistic.calculateMean(xValues), yValues, Statistic.calculateMean(yValues));
return regressionSlope >= 0 ? latestSample : null;
} else {
return null;
}
}
}
var _SAMPLE_SIZE = new OpaqueToken('RegressionSlopeValidator.sampleSize');
var _METRIC = new OpaqueToken('RegressionSlopeValidator.metric');
var _PROVIDERS = [
{
provide: RegressionSlopeValidator,
useFactory: (sampleSize, metric) => new RegressionSlopeValidator(sampleSize, metric),
deps: [_SAMPLE_SIZE, _METRIC]
},
{provide: _SAMPLE_SIZE, useValue: 10}, {provide: _METRIC, useValue: 'scriptTime'}
];

View 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 {OpaqueToken} from '@angular/core/src/di';
import {ListWrapper} from '@angular/facade/src/collection';
import {MeasureValues} from '../measure_values';
import {Validator} from '../validator';
/**
* A validator that waits for the sample to have a certain size.
*/
export class SizeValidator extends Validator {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
// TODO(tbosch): use static values when our transpiler supports them
static get SAMPLE_SIZE() { return _SAMPLE_SIZE; }
/** @internal */
private _sampleSize: number;
constructor(size) {
super();
this._sampleSize = size;
}
describe(): {[key: string]: any} { return {'sampleSize': this._sampleSize}; }
validate(completeSample: MeasureValues[]): MeasureValues[] {
if (completeSample.length >= this._sampleSize) {
return ListWrapper.slice(
completeSample, completeSample.length - this._sampleSize, completeSample.length);
} else {
return null;
}
}
}
var _SAMPLE_SIZE = new OpaqueToken('SizeValidator.sampleSize');
var _PROVIDERS = [
{provide: SizeValidator, useFactory: (size) => new SizeValidator(size), deps: [_SAMPLE_SIZE]},
{provide: _SAMPLE_SIZE, useValue: 10}
];

View File

@ -0,0 +1,26 @@
/**
* @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 {
static bindTo(delegateToken): any[] {
return [{provide: WebDriverAdapter, useFactory: (delegate) => delegate, deps: [delegateToken]}];
}
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<Map<string, any>> { throw new Error('NYI'); }
logs(type: string): Promise<any[]> { throw new Error('NYI'); }
}

View File

@ -0,0 +1,90 @@
/**
* @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 {Injector, OpaqueToken} from '@angular/core/src/di';
import {isBlank, isPresent} from '@angular/facade/src/lang';
import {Options} from './common_options';
/**
* 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 bindTo(childTokens: any[]): any[] {
var res = [
{
provide: _CHILDREN,
useFactory: (injector: Injector) => childTokens.map(token => injector.get(token)),
deps: [Injector]
},
{
provide: WebDriverExtension,
useFactory: (children: WebDriverExtension[], capabilities) => {
var delegate;
children.forEach(extension => {
if (extension.supports(capabilities)) {
delegate = extension;
}
});
if (isBlank(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), 'b' (nestable start), 'e' (nestable end), 'X' (Complete
*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<any[]> { 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;
}
}
var _CHILDREN = new OpaqueToken('WebDriverExtension.children');

View File

@ -0,0 +1,35 @@
library benchpress.src.webdriver.async_webdriver_adapter_dart;
import 'dart:async';
import 'package:webdriver/webdriver.dart' show WebDriver, LogEntry;
import '../web_driver_adapter.dart' show WebDriverAdapter;
class AsyncWebDriverAdapter extends WebDriverAdapter {
WebDriver _driver;
AsyncWebDriverAdapter(this._driver);
Future waitFor(Function callback) {
return callback();
}
Future executeScript(String script) {
return _driver.execute(script, const []);
}
Future executeAsyncScript(String script) {
return _driver.executeAsync(script, const []);
}
Future<Map> capabilities() {
return new Future.value(_driver.capabilities);
}
Future<List<Map>> logs(String type) {
return _driver.logs
.get(type)
.map((LogEntry entry) => {'message': entry.message})
.fold(<Map>[], (log, Map entry) {
return log..add(entry);
});
}
}

View File

@ -0,0 +1,257 @@
/**
* @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 {ListWrapper, StringMapWrapper} from '@angular/facade/src/collection';
import {Json, NumberWrapper, StringWrapper, isBlank, isPresent} from '@angular/facade/src/lang';
import {Options} from '../common_options';
import {WebDriverAdapter} from '../web_driver_adapter';
import {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'
*
* In order to collect the frame rate related metrics, add 'benchmark'
* to the list above.
*/
export class ChromeDriverExtension extends WebDriverExtension {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
private _majorChromeVersion: number;
constructor(private _driver: WebDriverAdapter, userAgent: string) {
super();
this._majorChromeVersion = this._parseChromeVersion(userAgent);
}
private _parseChromeVersion(userAgent: string): number {
if (isBlank(userAgent)) {
return -1;
}
var v = StringWrapper.split(userAgent, /Chrom(e|ium)\//g)[2];
if (isBlank(v)) {
return -1;
}
v = v.split('.')[0];
if (isBlank(v)) {
return -1;
}
return NumberWrapper.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> {
var script = `console.timeEnd('${name}');`;
if (isPresent(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<any> {
// 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) => {
var events = [];
entries.forEach(entry => {
var message = Json.parse(entry['message'])['message'];
if (StringWrapper.equals(message['method'], 'Tracing.dataCollected')) {
events.push(message['params']);
}
if (StringWrapper.equals(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: Array<{[key: string]: any}> = null) {
if (isBlank(normalizedEvents)) {
normalizedEvents = [];
}
var majorGCPids = {};
chromeEvents.forEach((event) => {
var categories = this._parseCategories(event['cat']);
var name = event['name'];
if (this._isEvent(categories, name, ['blink.console'])) {
normalizedEvents.push(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
var frameCount = event['args']['data']['frame_count'];
if (frameCount > 1) {
throw new Error('multi-frame render stats not supported');
}
if (frameCount == 1) {
normalizedEvents.push(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')) {
normalizedEvents.push(normalizeEvent(event, {'name': 'render'}));
} else if (this._majorChromeVersion < 45) {
var normalizedEvent = this._processAsPreChrome45Event(event, categories, majorGCPids);
if (normalizedEvent != null) normalizedEvents.push(normalizedEvent);
} else {
var normalizedEvent = this._processAsPostChrome44Event(event, categories);
if (normalizedEvent != null) normalizedEvents.push(normalizedEvent);
}
});
return normalizedEvents;
}
private _processAsPreChrome45Event(event, categories, majorGCPids) {
var name = event['name'];
var args = event['args'];
var pid = event['pid'];
var ph = event['ph'];
if (this._isEvent(
categories, name, ['disabled-by-default-devtools.timeline'], 'FunctionCall') &&
(isBlank(args) || isBlank(args['data']) ||
!StringWrapper.equals(args['data']['scriptName'], 'InjectedScript'))) {
return normalizeEvent(event, {'name': 'script'});
} else if (
this._isEvent(
categories, name, ['disabled-by-default-devtools.timeline'], 'RecalculateStyles') ||
this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'], 'Layout') ||
this._isEvent(
categories, name, ['disabled-by-default-devtools.timeline'], 'UpdateLayerTree') ||
this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'], 'Paint')) {
return normalizeEvent(event, {'name': 'render'});
} else if (this._isEvent(
categories, name, ['disabled-by-default-devtools.timeline'], 'GCEvent')) {
var normArgs = {
'usedHeapSize': isPresent(args['usedHeapSizeAfter']) ? args['usedHeapSizeAfter'] :
args['usedHeapSizeBefore']
};
if (StringWrapper.equals(ph, 'E')) {
normArgs['majorGc'] = isPresent(majorGCPids[pid]) && majorGCPids[pid];
}
majorGCPids[pid] = false;
return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
} else if (
this._isEvent(categories, name, ['v8'], 'majorGC') && StringWrapper.equals(ph, 'B')) {
majorGCPids[pid] = true;
}
return null; // nothing useful in this event
}
private _processAsPostChrome44Event(event, categories) {
var name = event['name'];
var args = event['args'];
if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'MajorGC')) {
var normArgs = {
'majorGc': true,
'usedHeapSize': isPresent(args['usedHeapSizeAfter']) ? args['usedHeapSizeAfter'] :
args['usedHeapSizeBefore']
};
return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
} else if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'MinorGC')) {
var normArgs = {
'majorGc': false,
'usedHeapSize': isPresent(args['usedHeapSizeAfter']) ? args['usedHeapSizeAfter'] :
args['usedHeapSizeBefore']
};
return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
} else if (
this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'FunctionCall') &&
(isBlank(args) || isBlank(args['data']) ||
(!StringWrapper.equals(args['data']['scriptName'], 'InjectedScript') &&
!StringWrapper.equals(args['data']['scriptName'], '')))) {
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')) {
let normArgs = {'encodedDataLength': args['data']['encodedDataLength']};
return normalizeEvent(event, {'name': 'receivedData', 'args': normArgs});
} else if (this._isEvent(categories, name, ['devtools.timeline'], 'ResourceSendRequest')) {
let data = args['data'];
let 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': name});
}
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 {
var hasCategories = expectedCategories.reduce(
(value, cat) => { return value && ListWrapper.contains(eventCategories, cat); }, true);
return isBlank(expectedName) ? hasCategories :
hasCategories && StringWrapper.equals(eventName, expectedName);
}
perfLogFeatures(): PerfLogFeatures {
return new PerfLogFeatures({render: true, gc: true, frameCapture: true, userTiming: true});
}
supports(capabilities: {[key: string]: any}): boolean {
return this._majorChromeVersion != -1 &&
StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'chrome');
}
}
function normalizeEvent(
chromeEvent: {[key: string]: any}, data: {[key: string]: any}): {[key: string]: any} {
var ph = chromeEvent['ph'];
if (StringWrapper.equals(ph, 'S')) {
ph = 'b';
} else if (StringWrapper.equals(ph, 'F')) {
ph = 'e';
}
var result =
{'pid': chromeEvent['pid'], 'ph': ph, 'cat': 'timeline', 'ts': chromeEvent['ts'] / 1000};
if (chromeEvent['ph'] === 'X') {
var dur = chromeEvent['dur'];
if (isBlank(dur)) {
dur = chromeEvent['tdur'];
}
result['dur'] = isBlank(dur) ? 0.0 : dur / 1000;
}
StringMapWrapper.forEach(data, (value, prop) => { result[prop] = value; });
return result;
}
var _PROVIDERS = [{
provide: ChromeDriverExtension,
useFactory: (driver, userAgent) => new ChromeDriverExtension(driver, userAgent),
deps: [WebDriverAdapter, Options.USER_AGENT]
}];

View File

@ -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 {StringWrapper, isPresent} from '@angular/facade/src/lang';
import {WebDriverAdapter} from '../web_driver_adapter';
import {PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
export class FirefoxDriverExtension extends WebDriverExtension {
static get PROVIDERS(): any[] { return _PROVIDERS; }
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> {
var script = 'window.markEnd("' + name + '");';
if (isPresent(restartName)) {
script += 'window.markStart("' + restartName + '");';
}
return this._driver.executeScript(script);
}
readPerfLog(): Promise<any> {
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 StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'firefox');
}
}
var _PROVIDERS = [{
provide: FirefoxDriverExtension,
useFactory: (driver) => new FirefoxDriverExtension(driver),
deps: [WebDriverAdapter]
}];

View File

@ -0,0 +1,134 @@
/**
* @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 {Json, StringWrapper, isBlank, isPresent} from '@angular/facade/src/lang';
import {WebDriverAdapter} from '../web_driver_adapter';
import {PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
export class IOsDriverExtension extends WebDriverExtension {
// TODO(tbosch): use static values when our transpiler supports them
static get PROVIDERS(): any[] { return _PROVIDERS; }
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> {
var script = `console.timeEnd('${name}');`;
if (isPresent(restartName)) {
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) => {
var records = [];
entries.forEach(entry => {
var message = Json.parse(entry['message'])['message'];
if (StringWrapper.equals(message['method'], 'Timeline.eventRecorded')) {
records.push(message['params']['record']);
}
});
return this._convertPerfRecordsToEvents(records);
});
}
/** @internal */
private _convertPerfRecordsToEvents(records: any[], events: any[] = null) {
if (isBlank(events)) {
events = [];
}
records.forEach((record) => {
var endEvent = null;
var type = record['type'];
var data = record['data'];
var startTime = record['startTime'];
var endTime = record['endTime'];
if (StringWrapper.equals(type, 'FunctionCall') &&
(isBlank(data) || !StringWrapper.equals(data['scriptName'], 'InjectedScript'))) {
events.push(createStartEvent('script', startTime));
endEvent = createEndEvent('script', endTime);
} else if (StringWrapper.equals(type, 'Time')) {
events.push(createMarkStartEvent(data['message'], startTime));
} else if (StringWrapper.equals(type, 'TimeEnd')) {
events.push(createMarkEndEvent(data['message'], startTime));
} else if (
StringWrapper.equals(type, 'RecalculateStyles') || StringWrapper.equals(type, 'Layout') ||
StringWrapper.equals(type, 'UpdateLayerTree') || StringWrapper.equals(type, 'Paint') ||
StringWrapper.equals(type, 'Rasterize') ||
StringWrapper.equals(type, 'CompositeLayers')) {
events.push(createStartEvent('render', startTime));
endEvent = createEndEvent('render', endTime);
}
// Note: ios used to support GCEvent up until iOS 6 :-(
if (isPresent(record['children'])) {
this._convertPerfRecordsToEvents(record['children'], events);
}
if (isPresent(endEvent)) {
events.push(endEvent);
}
});
return events;
}
perfLogFeatures(): PerfLogFeatures { return new PerfLogFeatures({render: true}); }
supports(capabilities: {[key: string]: any}): boolean {
return StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'safari');
}
}
function createEvent(ph, name, time, args = null) {
var result = {
'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 (isPresent(args)) {
result['args'] = args;
}
return result;
}
function createStartEvent(name, time, args = null) {
return createEvent('B', name, time, args);
}
function createEndEvent(name, time, args = null) {
return createEvent('E', name, time, args);
}
function createMarkStartEvent(name, time) {
return createEvent('b', name, time);
}
function createMarkEndEvent(name, time) {
return createEvent('e', name, time);
}
var _PROVIDERS = [{
provide: IOsDriverExtension,
useFactory: (driver) => new IOsDriverExtension(driver),
deps: [WebDriverAdapter]
}];

View File

@ -0,0 +1,3 @@
library benchpress.src.webdriver.selenium_webdriver_adapter;
//no dart implementation

View File

@ -0,0 +1,77 @@
/**
* @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 * as webdriver from 'selenium-webdriver';
import {WebDriverAdapter} from '../web_driver_adapter';
/**
* Adapter for the selenium-webdriver.
*/
export class SeleniumWebDriverAdapter extends WebDriverAdapter {
static get PROTRACTOR_BINDINGS(): any[] { return _PROTRACTOR_BINDINGS; }
constructor(private _driver: any) { super(); }
/** @internal */
private _convertPromise(thenable) {
var resolve: (result: any) => void;
var reject: (error: any) => void;
var promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
thenable.then(
// selenium-webdriver uses an own Node.js context,
// so we need to convert data into objects of this context.
// Previously needed for rtts_asserts.
(data) => resolve(convertToLocalProcess(data)), reject);
return promise;
}
waitFor(callback): Promise<any> {
return this._convertPromise(this._driver.controlFlow().execute(callback));
}
executeScript(script: string): Promise<any> {
return this._convertPromise(this._driver.executeScript(script));
}
executeAsyncScript(script: string): Promise<any> {
return this._convertPromise(this._driver.executeAsyncScript(script));
}
capabilities(): Promise<any> {
return this._convertPromise(
this._driver.getCapabilities().then((capsObject) => capsObject.serialize()));
}
logs(type: string): Promise<any> {
// Needed as selenium-webdriver does not forward
// performance logs in the correct way via manage().logs
return this._convertPromise(this._driver.schedule(
new webdriver.Command(webdriver.CommandName.GET_LOG).setParameter('type', type),
'WebDriver.manage().logs().get(' + type + ')'));
}
}
function convertToLocalProcess(data): Object {
var serialized = JSON.stringify(data);
if ('' + serialized === 'undefined') {
return undefined;
}
return JSON.parse(serialized);
}
var _PROTRACTOR_BINDINGS = [{
provide: WebDriverAdapter,
useFactory: () => new SeleniumWebDriverAdapter((<any>global).browser),
deps: []
}];