benchpress
Version:
Benchpress - a framework for e2e performance tests
230 lines (229 loc) • 11.7 kB
JavaScript
import { bind } from 'angular2/src/core/di';
import { ListWrapper, StringMapWrapper } from 'angular2/src/facade/collection';
import { Json, isPresent, isBlank, StringWrapper, NumberWrapper } from 'angular2/src/facade/lang';
import { BaseException } from 'angular2/src/facade/exceptions';
import { WebDriverExtension, PerfLogFeatures } from '../web_driver_extension';
import { WebDriverAdapter } from '../web_driver_adapter';
import { Options } from '../common_options';
/**
* 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 {
constructor(_driver, userAgent) {
super();
this._driver = _driver;
this._majorChromeVersion = this._parseChromeVersion(userAgent);
}
// TODO(tbosch): use static values when our transpiler supports them
static get BINDINGS() { return _PROVIDERS; }
_parseChromeVersion(userAgent) {
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) {
return this._driver.executeScript(`console.time('${name}');`);
}
timeEnd(name, restartName = null) {
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() {
// 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 BaseException('The DevTools trace buffer filled during the test!');
}
});
return this._convertPerfRecordsToEvents(events);
});
}
_convertPerfRecordsToEvents(chromeEvents, normalizedEvents = 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 BaseException('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;
}
_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
}
_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
}
_parseCategories(categories) { return categories.split(','); }
_isEvent(eventCategories, eventName, expectedCategories, expectedName = null) {
var hasCategories = expectedCategories.reduce((value, cat) => { return value && ListWrapper.contains(eventCategories, cat); }, true);
return isBlank(expectedName) ? hasCategories :
hasCategories && StringWrapper.equals(eventName, expectedName);
}
perfLogFeatures() {
return new PerfLogFeatures({ render: true, gc: true, frameCapture: true, userTiming: true });
}
supports(capabilities) {
return this._majorChromeVersion != -1 &&
StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'chrome');
}
}
function normalizeEvent(chromeEvent, data) {
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 = [
bind(ChromeDriverExtension)
.toFactory((driver, userAgent) => new ChromeDriverExtension(driver, userAgent), [WebDriverAdapter, Options.USER_AGENT])
];