2016-08-03 22:00:07 +00:00
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
|
|
|
|
|
2016-08-03 19:32:26 +00:00
|
|
|
import {ListWrapper, StringMapWrapper} from '@angular/facade/src/collection';
|
2016-08-05 16:50:49 +00:00
|
|
|
import {Json, NumberWrapper, StringWrapper, isBlank, isPresent} from '@angular/facade/src/lang';
|
2015-05-27 21:57:54 +00:00
|
|
|
|
2015-09-03 18:48:24 +00:00
|
|
|
import {Options} from '../common_options';
|
2016-08-03 22:00:07 +00:00
|
|
|
import {WebDriverAdapter} from '../web_driver_adapter';
|
|
|
|
|
import {PerfLogFeatures, WebDriverExtension} from '../web_driver_extension';
|
|
|
|
|
|
2015-05-27 21:57:54 +00:00
|
|
|
|
2015-06-09 22:19:26 +00:00
|
|
|
/**
|
|
|
|
|
* Set the following 'traceCategories' to collect metrics in Chrome:
|
2015-09-03 18:48:24 +00:00
|
|
|
* 'v8,blink.console,disabled-by-default-devtools.timeline,devtools.timeline'
|
2015-06-09 22:19:26 +00:00
|
|
|
*
|
|
|
|
|
* In order to collect the frame rate related metrics, add 'benchmark'
|
|
|
|
|
* to the list above.
|
|
|
|
|
*/
|
2015-05-27 21:57:54 +00:00
|
|
|
export class ChromeDriverExtension extends WebDriverExtension {
|
|
|
|
|
// TODO(tbosch): use static values when our transpiler supports them
|
2016-06-03 00:30:40 +00:00
|
|
|
static get PROVIDERS(): any[] { return _PROVIDERS; }
|
2015-05-27 21:57:54 +00:00
|
|
|
|
2015-09-03 18:48:24 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2015-10-31 20:04:26 +00:00
|
|
|
v = v.split('.')[0];
|
2015-09-03 18:48:24 +00:00
|
|
|
if (isBlank(v)) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
return NumberWrapper.parseInt(v, 10);
|
|
|
|
|
}
|
2015-05-27 21:57:54 +00:00
|
|
|
|
|
|
|
|
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)) {
|
2016-08-03 22:00:07 +00:00
|
|
|
script += `console.time('${restartName}');`;
|
2015-05-27 21:57:54 +00:00
|
|
|
}
|
|
|
|
|
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 = [];
|
2015-10-07 16:09:43 +00:00
|
|
|
entries.forEach(entry => {
|
2015-05-27 21:57:54 +00:00
|
|
|
var message = Json.parse(entry['message'])['message'];
|
|
|
|
|
if (StringWrapper.equals(message['method'], 'Tracing.dataCollected')) {
|
2015-06-17 18:17:21 +00:00
|
|
|
events.push(message['params']);
|
2015-05-27 21:57:54 +00:00
|
|
|
}
|
|
|
|
|
if (StringWrapper.equals(message['method'], 'Tracing.bufferUsage')) {
|
2016-08-25 07:50:16 +00:00
|
|
|
throw new Error('The DevTools trace buffer filled during the test!');
|
2015-05-27 21:57:54 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return this._convertPerfRecordsToEvents(events);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-03 22:00:07 +00:00
|
|
|
private _convertPerfRecordsToEvents(
|
|
|
|
|
chromeEvents: Array<{[key: string]: any}>,
|
|
|
|
|
normalizedEvents: Array<{[key: string]: any}> = null) {
|
2015-05-27 21:57:54 +00:00
|
|
|
if (isBlank(normalizedEvents)) {
|
|
|
|
|
normalizedEvents = [];
|
|
|
|
|
}
|
|
|
|
|
var majorGCPids = {};
|
|
|
|
|
chromeEvents.forEach((event) => {
|
2015-09-03 18:48:24 +00:00
|
|
|
var categories = this._parseCategories(event['cat']);
|
2015-05-27 21:57:54 +00:00
|
|
|
var name = event['name'];
|
2015-09-03 18:48:24 +00:00
|
|
|
if (this._isEvent(categories, name, ['blink.console'])) {
|
2015-06-17 18:17:21 +00:00
|
|
|
normalizedEvents.push(normalizeEvent(event, {'name': name}));
|
2016-08-03 22:00:07 +00:00
|
|
|
} else if (this._isEvent(
|
|
|
|
|
categories, name, ['benchmark'],
|
|
|
|
|
'BenchmarkInstrumentation::ImplThreadRenderingStats')) {
|
2015-06-18 19:54:41 +00:00
|
|
|
// 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
|
2015-12-16 07:47:48 +00:00
|
|
|
// always available if something is rendered
|
2015-09-03 18:48:24 +00:00
|
|
|
var frameCount = event['args']['data']['frame_count'];
|
|
|
|
|
if (frameCount > 1) {
|
2016-08-25 07:50:16 +00:00
|
|
|
throw new Error('multi-frame render stats not supported');
|
2015-06-09 22:19:26 +00:00
|
|
|
}
|
2015-09-03 18:48:24 +00:00
|
|
|
if (frameCount == 1) {
|
|
|
|
|
normalizedEvents.push(normalizeEvent(event, {'name': 'frame'}));
|
|
|
|
|
}
|
2016-08-03 22:00:07 +00:00
|
|
|
} else if (
|
|
|
|
|
this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'], 'Rasterize') ||
|
|
|
|
|
this._isEvent(
|
|
|
|
|
categories, name, ['disabled-by-default-devtools.timeline'], 'CompositeLayers')) {
|
2015-09-03 18:48:24 +00:00
|
|
|
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);
|
2015-05-27 21:57:54 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return normalizedEvents;
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-03 18:48:24 +00:00
|
|
|
private _processAsPreChrome45Event(event, categories, majorGCPids) {
|
|
|
|
|
var name = event['name'];
|
|
|
|
|
var args = event['args'];
|
|
|
|
|
var pid = event['pid'];
|
|
|
|
|
var ph = event['ph'];
|
2016-08-03 22:00:07 +00:00
|
|
|
if (this._isEvent(
|
|
|
|
|
categories, name, ['disabled-by-default-devtools.timeline'], 'FunctionCall') &&
|
2015-09-03 18:48:24 +00:00
|
|
|
(isBlank(args) || isBlank(args['data']) ||
|
|
|
|
|
!StringWrapper.equals(args['data']['scriptName'], 'InjectedScript'))) {
|
|
|
|
|
return normalizeEvent(event, {'name': 'script'});
|
2016-08-03 22:00:07 +00:00
|
|
|
} 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')) {
|
2015-09-03 18:48:24 +00:00
|
|
|
return normalizeEvent(event, {'name': 'render'});
|
2016-08-03 22:00:07 +00:00
|
|
|
} else if (this._isEvent(
|
|
|
|
|
categories, name, ['disabled-by-default-devtools.timeline'], 'GCEvent')) {
|
2015-09-03 18:48:24 +00:00
|
|
|
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});
|
2016-08-03 22:00:07 +00:00
|
|
|
} else if (
|
|
|
|
|
this._isEvent(categories, name, ['v8'], 'majorGC') && StringWrapper.equals(ph, 'B')) {
|
2015-09-03 18:48:24 +00:00
|
|
|
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});
|
2016-08-03 22:00:07 +00:00
|
|
|
} 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'], '')))) {
|
2015-09-03 18:48:24 +00:00
|
|
|
return normalizeEvent(event, {'name': 'script'});
|
2016-08-03 22:00:07 +00:00
|
|
|
} else if (this._isEvent(
|
|
|
|
|
categories, name, ['devtools.timeline', 'blink'], 'UpdateLayoutTree')) {
|
2015-09-03 18:48:24 +00:00
|
|
|
return normalizeEvent(event, {'name': 'render'});
|
2016-08-03 22:00:07 +00:00
|
|
|
} else if (
|
|
|
|
|
this._isEvent(categories, name, ['devtools.timeline'], 'UpdateLayerTree') ||
|
|
|
|
|
this._isEvent(categories, name, ['devtools.timeline'], 'Layout') ||
|
|
|
|
|
this._isEvent(categories, name, ['devtools.timeline'], 'Paint')) {
|
2015-09-03 18:48:24 +00:00
|
|
|
return normalizeEvent(event, {'name': 'render'});
|
2015-12-10 01:15:55 +00:00
|
|
|
} 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});
|
2015-09-03 18:48:24 +00:00
|
|
|
}
|
|
|
|
|
return null; // nothing useful in this event
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-31 20:04:26 +00:00
|
|
|
private _parseCategories(categories: string): string[] { return categories.split(','); }
|
2015-09-03 18:48:24 +00:00
|
|
|
|
2016-08-03 22:00:07 +00:00
|
|
|
private _isEvent(
|
|
|
|
|
eventCategories: string[], eventName: string, expectedCategories: string[],
|
|
|
|
|
expectedName: string = null): boolean {
|
2015-10-09 16:07:58 +00:00
|
|
|
var hasCategories = expectedCategories.reduce(
|
|
|
|
|
(value, cat) => { return value && ListWrapper.contains(eventCategories, cat); }, true);
|
2015-09-03 18:48:24 +00:00
|
|
|
return isBlank(expectedName) ? hasCategories :
|
|
|
|
|
hasCategories && StringWrapper.equals(eventName, expectedName);
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-09 22:19:26 +00:00
|
|
|
perfLogFeatures(): PerfLogFeatures {
|
2015-12-10 01:15:55 +00:00
|
|
|
return new PerfLogFeatures({render: true, gc: true, frameCapture: true, userTiming: true});
|
2015-06-09 22:19:26 +00:00
|
|
|
}
|
2015-05-27 21:57:54 +00:00
|
|
|
|
2015-10-02 23:47:54 +00:00
|
|
|
supports(capabilities: {[key: string]: any}): boolean {
|
2015-09-03 18:48:24 +00:00
|
|
|
return this._majorChromeVersion != -1 &&
|
2016-08-03 22:00:07 +00:00
|
|
|
StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'chrome');
|
2015-05-27 21:57:54 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-03 22:00:07 +00:00
|
|
|
function normalizeEvent(
|
|
|
|
|
chromeEvent: {[key: string]: any}, data: {[key: string]: any}): {[key: string]: any} {
|
2015-05-27 21:57:54 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-03 22:00:07 +00:00
|
|
|
var _PROVIDERS = [{
|
|
|
|
|
provide: ChromeDriverExtension,
|
|
|
|
|
useFactory: (driver, userAgent) => new ChromeDriverExtension(driver, userAgent),
|
|
|
|
|
deps: [WebDriverAdapter, Options.USER_AGENT]
|
|
|
|
|
}];
|