UNPKG

14.9 kBPlain TextView Raw
1import {PromiseWrapper, TimerWrapper} from 'angular2/src/facade/async';
2import {
3 isPresent,
4 isBlank,
5 StringWrapper,
6 Math,
7 RegExpWrapper,
8 NumberWrapper
9} from 'angular2/src/facade/lang';
10import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
11import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
12import {bind, provide, Provider, OpaqueToken} from 'angular2/src/core/di';
13
14import {WebDriverExtension, PerfLogFeatures} from '../web_driver_extension';
15import {Metric} from '../metric';
16import {Options} from '../common_options';
17
18/**
19 * A metric that reads out the performance log
20 */
21export class PerflogMetric extends Metric {
22 // TODO(tbosch): use static values when our transpiler supports them
23 static get BINDINGS(): Provider[] { return _PROVIDERS; }
24 // TODO(tbosch): use static values when our transpiler supports them
25 static get SET_TIMEOUT(): OpaqueToken { return _SET_TIMEOUT; }
26
27 private _remainingEvents: Array<{[key: string]: any}>;
28 private _measureCount: number;
29 _perfLogFeatures: PerfLogFeatures;
30
31
32 /**
33 * @param driverExtension
34 * @param setTimeout
35 * @param microMetrics Name and description of metrics provided via console.time / console.timeEnd
36 **/
37 constructor(private _driverExtension: WebDriverExtension, private _setTimeout: Function,
38 private _microMetrics: {[key: string]: any}, private _forceGc: boolean,
39 private _captureFrames: boolean, private _receivedData: boolean,
40 private _requestCount: boolean) {
41 super();
42
43 this._remainingEvents = [];
44 this._measureCount = 0;
45 this._perfLogFeatures = _driverExtension.perfLogFeatures();
46 if (!this._perfLogFeatures.userTiming) {
47 // User timing is needed for navigationStart.
48 this._receivedData = false;
49 this._requestCount = false;
50 }
51 }
52
53 describe(): {[key: string]: any} {
54 var res = {
55 'scriptTime': 'script execution time in ms, including gc and render',
56 'pureScriptTime': 'script execution time in ms, without gc nor render'
57 };
58 if (this._perfLogFeatures.render) {
59 res['renderTime'] = 'render time in ms';
60 }
61 if (this._perfLogFeatures.gc) {
62 res['gcTime'] = 'gc time in ms';
63 res['gcAmount'] = 'gc amount in kbytes';
64 res['majorGcTime'] = 'time of major gcs in ms';
65 if (this._forceGc) {
66 res['forcedGcTime'] = 'forced gc time in ms';
67 res['forcedGcAmount'] = 'forced gc amount in kbytes';
68 }
69 }
70 if (this._receivedData) {
71 res['receivedData'] = 'encoded bytes received since navigationStart';
72 }
73 if (this._requestCount) {
74 res['requestCount'] = 'count of requests sent since navigationStart';
75 }
76 if (this._captureFrames) {
77 if (!this._perfLogFeatures.frameCapture) {
78 var warningMsg = 'WARNING: Metric requested, but not supported by driver';
79 // using dot syntax for metric name to keep them grouped together in console reporter
80 res['frameTime.mean'] = warningMsg;
81 res['frameTime.worst'] = warningMsg;
82 res['frameTime.best'] = warningMsg;
83 res['frameTime.smooth'] = warningMsg;
84 } else {
85 res['frameTime.mean'] = 'mean frame time in ms (target: 16.6ms for 60fps)';
86 res['frameTime.worst'] = 'worst frame time in ms';
87 res['frameTime.best'] = 'best frame time in ms';
88 res['frameTime.smooth'] = 'percentage of frames that hit 60fps';
89 }
90 }
91 StringMapWrapper.forEach(this._microMetrics,
92 (desc, name) => { StringMapWrapper.set(res, name, desc); });
93 return res;
94 }
95
96 beginMeasure(): Promise<any> {
97 var resultPromise = PromiseWrapper.resolve(null);
98 if (this._forceGc) {
99 resultPromise = resultPromise.then((_) => this._driverExtension.gc());
100 }
101 return resultPromise.then((_) => this._beginMeasure());
102 }
103
104 endMeasure(restart: boolean): Promise<{[key: string]: any}> {
105 if (this._forceGc) {
106 return this._endPlainMeasureAndMeasureForceGc(restart);
107 } else {
108 return this._endMeasure(restart);
109 }
110 }
111
112 _endPlainMeasureAndMeasureForceGc(restartMeasure: boolean) {
113 return this._endMeasure(true).then((measureValues) => {
114 // disable frame capture for measurements during forced gc
115 var originalFrameCaptureValue = this._captureFrames;
116 this._captureFrames = false;
117 return this._driverExtension.gc()
118 .then((_) => this._endMeasure(restartMeasure))
119 .then((forceGcMeasureValues) => {
120 this._captureFrames = originalFrameCaptureValue;
121 StringMapWrapper.set(measureValues, 'forcedGcTime', forceGcMeasureValues['gcTime']);
122 StringMapWrapper.set(measureValues, 'forcedGcAmount', forceGcMeasureValues['gcAmount']);
123 return measureValues;
124 });
125 });
126 }
127
128 _beginMeasure(): Promise<any> {
129 return this._driverExtension.timeBegin(this._markName(this._measureCount++));
130 }
131
132 _endMeasure(restart: boolean): Promise<{[key: string]: any}> {
133 var markName = this._markName(this._measureCount - 1);
134 var nextMarkName = restart ? this._markName(this._measureCount++) : null;
135 return this._driverExtension.timeEnd(markName, nextMarkName)
136 .then((_) => this._readUntilEndMark(markName));
137 }
138
139 _readUntilEndMark(markName: string, loopCount: number = 0, startEvent = null) {
140 if (loopCount > _MAX_RETRY_COUNT) {
141 throw new BaseException(`Tried too often to get the ending mark: ${loopCount}`);
142 }
143 return this._driverExtension.readPerfLog().then((events) => {
144 this._addEvents(events);
145 var result = this._aggregateEvents(this._remainingEvents, markName);
146 if (isPresent(result)) {
147 this._remainingEvents = events;
148 return result;
149 }
150 var completer = PromiseWrapper.completer();
151 this._setTimeout(() => completer.resolve(this._readUntilEndMark(markName, loopCount + 1)),
152 100);
153 return completer.promise;
154 });
155 }
156
157 _addEvents(events: { [key: string]: string }[]) {
158 var needSort = false;
159 events.forEach(event => {
160 if (StringWrapper.equals(event['ph'], 'X')) {
161 needSort = true;
162 var startEvent = {};
163 var endEvent = {};
164 StringMapWrapper.forEach(event, (value, prop) => {
165 startEvent[prop] = value;
166 endEvent[prop] = value;
167 });
168 startEvent['ph'] = 'B';
169 endEvent['ph'] = 'E';
170 endEvent['ts'] = startEvent['ts'] + startEvent['dur'];
171 this._remainingEvents.push(startEvent);
172 this._remainingEvents.push(endEvent);
173 } else {
174 this._remainingEvents.push(event);
175 }
176 });
177 if (needSort) {
178 // Need to sort because of the ph==='X' events
179 ListWrapper.sort(this._remainingEvents, (a, b) => {
180 var diff = a['ts'] - b['ts'];
181 return diff > 0 ? 1 : diff < 0 ? -1 : 0;
182 });
183 }
184 }
185
186 _aggregateEvents(events: Array<{[key: string]: any}>, markName): {[key: string]: any} {
187 var result = {'scriptTime': 0, 'pureScriptTime': 0};
188 if (this._perfLogFeatures.gc) {
189 result['gcTime'] = 0;
190 result['majorGcTime'] = 0;
191 result['gcAmount'] = 0;
192 }
193 if (this._perfLogFeatures.render) {
194 result['renderTime'] = 0;
195 }
196 if (this._captureFrames) {
197 result['frameTime.mean'] = 0;
198 result['frameTime.best'] = 0;
199 result['frameTime.worst'] = 0;
200 result['frameTime.smooth'] = 0;
201 }
202 StringMapWrapper.forEach(this._microMetrics, (desc, name) => { result[name] = 0; });
203 if (this._receivedData) {
204 result['receivedData'] = 0;
205 }
206 if (this._requestCount) {
207 result['requestCount'] = 0;
208 }
209
210 var markStartEvent = null;
211 var markEndEvent = null;
212 var gcTimeInScript = 0;
213 var renderTimeInScript = 0;
214
215 var frameTimestamps = [];
216 var frameTimes = [];
217 var frameCaptureStartEvent = null;
218 var frameCaptureEndEvent = null;
219
220 var intervalStarts: {[key: string]: any} = {};
221 var intervalStartCount: {[key: string]: number} = {};
222 events.forEach((event) => {
223 var ph = event['ph'];
224 var name = event['name'];
225 var microIterations = 1;
226 var microIterationsMatch = RegExpWrapper.firstMatch(_MICRO_ITERATIONS_REGEX, name);
227 if (isPresent(microIterationsMatch)) {
228 name = microIterationsMatch[1];
229 microIterations = NumberWrapper.parseInt(microIterationsMatch[2], 10);
230 }
231
232 if (StringWrapper.equals(ph, 'b') && StringWrapper.equals(name, markName)) {
233 markStartEvent = event;
234 } else if (StringWrapper.equals(ph, 'e') && StringWrapper.equals(name, markName)) {
235 markEndEvent = event;
236 }
237
238 let isInstant = StringWrapper.equals(ph, 'I') || StringWrapper.equals(ph, 'i');
239 if (this._requestCount && StringWrapper.equals(name, 'sendRequest')) {
240 result['requestCount'] += 1;
241 } else if (this._receivedData && StringWrapper.equals(name, 'receivedData') && isInstant) {
242 result['receivedData'] += event['args']['encodedDataLength'];
243 } else if (StringWrapper.equals(name, 'navigationStart')) {
244 // We count data + requests since the last navigationStart
245 // (there might be chrome extensions loaded by selenium before our page, so there
246 // will likely be more than one navigationStart).
247 if (this._receivedData) {
248 result['receivedData'] = 0;
249 }
250 if (this._requestCount) {
251 result['requestCount'] = 0;
252 }
253 }
254 if (isPresent(markStartEvent) && isBlank(markEndEvent) &&
255 event['pid'] === markStartEvent['pid']) {
256 if (StringWrapper.equals(ph, 'b') && StringWrapper.equals(name, _MARK_NAME_FRAME_CAPUTRE)) {
257 if (isPresent(frameCaptureStartEvent)) {
258 throw new BaseException('can capture frames only once per benchmark run');
259 }
260 if (!this._captureFrames) {
261 throw new BaseException(
262 'found start event for frame capture, but frame capture was not requested in benchpress')
263 }
264 frameCaptureStartEvent = event;
265 } else if (StringWrapper.equals(ph, 'e') &&
266 StringWrapper.equals(name, _MARK_NAME_FRAME_CAPUTRE)) {
267 if (isBlank(frameCaptureStartEvent)) {
268 throw new BaseException('missing start event for frame capture');
269 }
270 frameCaptureEndEvent = event;
271 }
272
273 if (isInstant) {
274 if (isPresent(frameCaptureStartEvent) && isBlank(frameCaptureEndEvent) &&
275 StringWrapper.equals(name, 'frame')) {
276 frameTimestamps.push(event['ts']);
277 if (frameTimestamps.length >= 2) {
278 frameTimes.push(frameTimestamps[frameTimestamps.length - 1] -
279 frameTimestamps[frameTimestamps.length - 2]);
280 }
281 }
282 }
283
284 if (StringWrapper.equals(ph, 'B') || StringWrapper.equals(ph, 'b')) {
285 if (isBlank(intervalStarts[name])) {
286 intervalStartCount[name] = 1;
287 intervalStarts[name] = event;
288 } else {
289 intervalStartCount[name]++;
290 }
291 } else if ((StringWrapper.equals(ph, 'E') || StringWrapper.equals(ph, 'e')) &&
292 isPresent(intervalStarts[name])) {
293 intervalStartCount[name]--;
294 if (intervalStartCount[name] === 0) {
295 var startEvent = intervalStarts[name];
296 var duration = (event['ts'] - startEvent['ts']);
297 intervalStarts[name] = null;
298 if (StringWrapper.equals(name, 'gc')) {
299 result['gcTime'] += duration;
300 var amount =
301 (startEvent['args']['usedHeapSize'] - event['args']['usedHeapSize']) / 1000;
302 result['gcAmount'] += amount;
303 var majorGc = event['args']['majorGc'];
304 if (isPresent(majorGc) && majorGc) {
305 result['majorGcTime'] += duration;
306 }
307 if (isPresent(intervalStarts['script'])) {
308 gcTimeInScript += duration;
309 }
310 } else if (StringWrapper.equals(name, 'render')) {
311 result['renderTime'] += duration;
312 if (isPresent(intervalStarts['script'])) {
313 renderTimeInScript += duration;
314 }
315 } else if (StringWrapper.equals(name, 'script')) {
316 result['scriptTime'] += duration;
317 } else if (isPresent(this._microMetrics[name])) {
318 result[name] += duration / microIterations;
319 }
320 }
321 }
322 }
323 });
324 if (!isPresent(markStartEvent) || !isPresent(markEndEvent)) {
325 // not all events have been received, no further processing for now
326 return null;
327 }
328
329 if (isPresent(markEndEvent) && isPresent(frameCaptureStartEvent) &&
330 isBlank(frameCaptureEndEvent)) {
331 throw new BaseException('missing end event for frame capture');
332 }
333 if (this._captureFrames && isBlank(frameCaptureStartEvent)) {
334 throw new BaseException(
335 'frame capture requested in benchpress, but no start event was found');
336 }
337 if (frameTimes.length > 0) {
338 this._addFrameMetrics(result, frameTimes);
339 }
340 result['pureScriptTime'] = result['scriptTime'] - gcTimeInScript - renderTimeInScript;
341 return result;
342 }
343
344 _addFrameMetrics(result: {[key: string]: any}, frameTimes: any[]) {
345 result['frameTime.mean'] = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
346 var firstFrame = frameTimes[0];
347 result['frameTime.worst'] = frameTimes.reduce((a, b) => a > b ? a : b, firstFrame);
348 result['frameTime.best'] = frameTimes.reduce((a, b) => a < b ? a : b, firstFrame);
349 result['frameTime.smooth'] =
350 frameTimes.filter(t => t < _FRAME_TIME_SMOOTH_THRESHOLD).length / frameTimes.length;
351 }
352
353 _markName(index) { return `${_MARK_NAME_PREFIX}${index}`; }
354}
355
356var _MICRO_ITERATIONS_REGEX = /(.+)\*(\d+)$/g;
357
358var _MAX_RETRY_COUNT = 20;
359var _MARK_NAME_PREFIX = 'benchpress';
360var _SET_TIMEOUT = new OpaqueToken('PerflogMetric.setTimeout');
361
362var _MARK_NAME_FRAME_CAPUTRE = 'frameCapture';
363// using 17ms as a somewhat looser threshold, instead of 16.6666ms
364var _FRAME_TIME_SMOOTH_THRESHOLD = 17;
365
366var _PROVIDERS = [
367 bind(PerflogMetric)
368 .toFactory(
369 (driverExtension, setTimeout, microMetrics, forceGc, captureFrames, receivedData,
370 requestCount) => new PerflogMetric(driverExtension, setTimeout, microMetrics, forceGc,
371 captureFrames, receivedData, requestCount),
372 [
373 WebDriverExtension,
374 _SET_TIMEOUT,
375 Options.MICRO_METRICS,
376 Options.FORCE_GC,
377 Options.CAPTURE_FRAMES,
378 Options.RECEIVED_DATA,
379 Options.REQUEST_COUNT
380 ]),
381 provide(_SET_TIMEOUT, {useValue: (fn, millis) => TimerWrapper.setTimeout(fn, millis)})
382];