UNPKG

11.5 kBPlain TextView Raw
1import {bind, provide, Provider} from 'angular2/src/core/di';
2import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
3import {
4 Json,
5 isPresent,
6 isBlank,
7 RegExpWrapper,
8 StringWrapper,
9 NumberWrapper
10} from 'angular2/src/facade/lang';
11import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
12
13import {WebDriverExtension, PerfLogFeatures} from '../web_driver_extension';
14import {WebDriverAdapter} from '../web_driver_adapter';
15import {Options} from '../common_options';
16
17/**
18 * Set the following 'traceCategories' to collect metrics in Chrome:
19 * 'v8,blink.console,disabled-by-default-devtools.timeline,devtools.timeline'
20 *
21 * In order to collect the frame rate related metrics, add 'benchmark'
22 * to the list above.
23 */
24export class ChromeDriverExtension extends WebDriverExtension {
25 // TODO(tbosch): use static values when our transpiler supports them
26 static get BINDINGS(): Provider[] { return _PROVIDERS; }
27
28 private _majorChromeVersion: number;
29
30 constructor(private _driver: WebDriverAdapter, userAgent: string) {
31 super();
32 this._majorChromeVersion = this._parseChromeVersion(userAgent);
33 }
34
35 private _parseChromeVersion(userAgent: string): number {
36 if (isBlank(userAgent)) {
37 return -1;
38 }
39 var v = StringWrapper.split(userAgent, /Chrom(e|ium)\//g)[2];
40 if (isBlank(v)) {
41 return -1;
42 }
43 v = v.split('.')[0];
44 if (isBlank(v)) {
45 return -1;
46 }
47 return NumberWrapper.parseInt(v, 10);
48 }
49
50 gc() { return this._driver.executeScript('window.gc()'); }
51
52 timeBegin(name: string): Promise<any> {
53 return this._driver.executeScript(`console.time('${name}');`);
54 }
55
56 timeEnd(name: string, restartName: string = null): Promise<any> {
57 var script = `console.timeEnd('${name}');`;
58 if (isPresent(restartName)) {
59 script += `console.time('${restartName}');`
60 }
61 return this._driver.executeScript(script);
62 }
63
64 // See [Chrome Trace Event
65 // Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit)
66 readPerfLog(): Promise<any> {
67 // TODO(tbosch): Chromedriver bug https://code.google.com/p/chromedriver/issues/detail?id=1098
68 // Need to execute at least one command so that the browser logs can be read out!
69 return this._driver.executeScript('1+1')
70 .then((_) => this._driver.logs('performance'))
71 .then((entries) => {
72 var events = [];
73 entries.forEach(entry => {
74 var message = Json.parse(entry['message'])['message'];
75 if (StringWrapper.equals(message['method'], 'Tracing.dataCollected')) {
76 events.push(message['params']);
77 }
78 if (StringWrapper.equals(message['method'], 'Tracing.bufferUsage')) {
79 throw new BaseException('The DevTools trace buffer filled during the test!');
80 }
81 });
82 return this._convertPerfRecordsToEvents(events);
83 });
84 }
85
86 private _convertPerfRecordsToEvents(chromeEvents: Array<{[key: string]: any}>,
87 normalizedEvents: Array<{[key: string]: any}> = null) {
88 if (isBlank(normalizedEvents)) {
89 normalizedEvents = [];
90 }
91 var majorGCPids = {};
92 chromeEvents.forEach((event) => {
93 var categories = this._parseCategories(event['cat']);
94 var name = event['name'];
95 if (this._isEvent(categories, name, ['blink.console'])) {
96 normalizedEvents.push(normalizeEvent(event, {'name': name}));
97 } else if (this._isEvent(categories, name, ['benchmark'],
98 'BenchmarkInstrumentation::ImplThreadRenderingStats')) {
99 // TODO(goderbauer): Instead of BenchmarkInstrumentation::ImplThreadRenderingStats the
100 // following events should be used (if available) for more accurate measurments:
101 // 1st choice: vsync_before - ground truth on Android
102 // 2nd choice: BenchmarkInstrumentation::DisplayRenderingStats - available on systems with
103 // new surfaces framework (not broadly enabled yet)
104 // 3rd choice: BenchmarkInstrumentation::ImplThreadRenderingStats - fallback event that is
105 // always available if something is rendered
106 var frameCount = event['args']['data']['frame_count'];
107 if (frameCount > 1) {
108 throw new BaseException('multi-frame render stats not supported');
109 }
110 if (frameCount == 1) {
111 normalizedEvents.push(normalizeEvent(event, {'name': 'frame'}));
112 }
113 } else if (this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
114 'Rasterize') ||
115 this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
116 'CompositeLayers')) {
117 normalizedEvents.push(normalizeEvent(event, {'name': 'render'}));
118 } else if (this._majorChromeVersion < 45) {
119 var normalizedEvent = this._processAsPreChrome45Event(event, categories, majorGCPids);
120 if (normalizedEvent != null) normalizedEvents.push(normalizedEvent);
121 } else {
122 var normalizedEvent = this._processAsPostChrome44Event(event, categories);
123 if (normalizedEvent != null) normalizedEvents.push(normalizedEvent);
124 }
125 });
126 return normalizedEvents;
127 }
128
129 private _processAsPreChrome45Event(event, categories, majorGCPids) {
130 var name = event['name'];
131 var args = event['args'];
132 var pid = event['pid'];
133 var ph = event['ph'];
134 if (this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
135 'FunctionCall') &&
136 (isBlank(args) || isBlank(args['data']) ||
137 !StringWrapper.equals(args['data']['scriptName'], 'InjectedScript'))) {
138 return normalizeEvent(event, {'name': 'script'});
139 } else if (this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
140 'RecalculateStyles') ||
141 this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
142 'Layout') ||
143 this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
144 'UpdateLayerTree') ||
145 this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
146 'Paint')) {
147 return normalizeEvent(event, {'name': 'render'});
148 } else if (this._isEvent(categories, name, ['disabled-by-default-devtools.timeline'],
149 'GCEvent')) {
150 var normArgs = {
151 'usedHeapSize': isPresent(args['usedHeapSizeAfter']) ? args['usedHeapSizeAfter'] :
152 args['usedHeapSizeBefore']
153 };
154 if (StringWrapper.equals(ph, 'E')) {
155 normArgs['majorGc'] = isPresent(majorGCPids[pid]) && majorGCPids[pid];
156 }
157 majorGCPids[pid] = false;
158 return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
159 } else if (this._isEvent(categories, name, ['v8'], 'majorGC') &&
160 StringWrapper.equals(ph, 'B')) {
161 majorGCPids[pid] = true;
162 }
163 return null; // nothing useful in this event
164 }
165
166 private _processAsPostChrome44Event(event, categories) {
167 var name = event['name'];
168 var args = event['args'];
169 if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'MajorGC')) {
170 var normArgs = {
171 'majorGc': true,
172 'usedHeapSize': isPresent(args['usedHeapSizeAfter']) ? args['usedHeapSizeAfter'] :
173 args['usedHeapSizeBefore']
174 };
175 return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
176 } else if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'MinorGC')) {
177 var normArgs = {
178 'majorGc': false,
179 'usedHeapSize': isPresent(args['usedHeapSizeAfter']) ? args['usedHeapSizeAfter'] :
180 args['usedHeapSizeBefore']
181 };
182 return normalizeEvent(event, {'name': 'gc', 'args': normArgs});
183 } else if (this._isEvent(categories, name, ['devtools.timeline', 'v8'], 'FunctionCall') &&
184 (isBlank(args) || isBlank(args['data']) ||
185 (!StringWrapper.equals(args['data']['scriptName'], 'InjectedScript') &&
186 !StringWrapper.equals(args['data']['scriptName'], '')))) {
187 return normalizeEvent(event, {'name': 'script'});
188 } else if (this._isEvent(categories, name, ['devtools.timeline', 'blink'],
189 'UpdateLayoutTree')) {
190 return normalizeEvent(event, {'name': 'render'});
191 } else if (this._isEvent(categories, name, ['devtools.timeline'], 'UpdateLayerTree') ||
192 this._isEvent(categories, name, ['devtools.timeline'], 'Layout') ||
193 this._isEvent(categories, name, ['devtools.timeline'], 'Paint')) {
194 return normalizeEvent(event, {'name': 'render'});
195 } else if (this._isEvent(categories, name, ['devtools.timeline'], 'ResourceReceivedData')) {
196 let normArgs = {'encodedDataLength': args['data']['encodedDataLength']};
197 return normalizeEvent(event, {'name': 'receivedData', 'args': normArgs});
198 } else if (this._isEvent(categories, name, ['devtools.timeline'], 'ResourceSendRequest')) {
199 let data = args['data'];
200 let normArgs = {'url': data['url'], 'method': data['requestMethod']};
201 return normalizeEvent(event, {'name': 'sendRequest', 'args': normArgs});
202 } else if (this._isEvent(categories, name, ['blink.user_timing'], 'navigationStart')) {
203 return normalizeEvent(event, {'name': name});
204 }
205 return null; // nothing useful in this event
206 }
207
208 private _parseCategories(categories: string): string[] { return categories.split(','); }
209
210 private _isEvent(eventCategories: string[], eventName: string, expectedCategories: string[],
211 expectedName: string = null): boolean {
212 var hasCategories = expectedCategories.reduce(
213 (value, cat) => { return value && ListWrapper.contains(eventCategories, cat); }, true);
214 return isBlank(expectedName) ? hasCategories :
215 hasCategories && StringWrapper.equals(eventName, expectedName);
216 }
217
218 perfLogFeatures(): PerfLogFeatures {
219 return new PerfLogFeatures({render: true, gc: true, frameCapture: true, userTiming: true});
220 }
221
222 supports(capabilities: {[key: string]: any}): boolean {
223 return this._majorChromeVersion != -1 &&
224 StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'chrome');
225 }
226}
227
228function normalizeEvent(chromeEvent: {[key: string]: any},
229 data: {[key: string]: any}): {[key: string]: any} {
230 var ph = chromeEvent['ph'];
231 if (StringWrapper.equals(ph, 'S')) {
232 ph = 'b';
233 } else if (StringWrapper.equals(ph, 'F')) {
234 ph = 'e';
235 }
236 var result =
237 {'pid': chromeEvent['pid'], 'ph': ph, 'cat': 'timeline', 'ts': chromeEvent['ts'] / 1000};
238 if (chromeEvent['ph'] === 'X') {
239 var dur = chromeEvent['dur'];
240 if (isBlank(dur)) {
241 dur = chromeEvent['tdur'];
242 }
243 result['dur'] = isBlank(dur) ? 0.0 : dur / 1000;
244 }
245 StringMapWrapper.forEach(data, (value, prop) => { result[prop] = value; });
246 return result;
247}
248
249var _PROVIDERS = [
250 bind(ChromeDriverExtension)
251 .toFactory((driver, userAgent) => new ChromeDriverExtension(driver, userAgent),
252 [WebDriverAdapter, Options.USER_AGENT])
253];