1 | import {PromiseWrapper, TimerWrapper} from 'angular2/src/facade/async';
|
2 | import {
|
3 | isPresent,
|
4 | isBlank,
|
5 | StringWrapper,
|
6 | Math,
|
7 | RegExpWrapper,
|
8 | NumberWrapper
|
9 | } from 'angular2/src/facade/lang';
|
10 | import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
11 | import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
12 | import {bind, provide, Provider, OpaqueToken} from 'angular2/src/core/di';
|
13 |
|
14 | import {WebDriverExtension, PerfLogFeatures} from '../web_driver_extension';
|
15 | import {Metric} from '../metric';
|
16 | import {Options} from '../common_options';
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | export class PerflogMetric extends Metric {
|
22 |
|
23 | static get BINDINGS(): Provider[] { return _PROVIDERS; }
|
24 |
|
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 |
|
34 |
|
35 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
245 |
|
246 |
|
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 |
|
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 |
|
356 | var _MICRO_ITERATIONS_REGEX = /(.+)\*(\d+)$/g;
|
357 |
|
358 | var _MAX_RETRY_COUNT = 20;
|
359 | var _MARK_NAME_PREFIX = 'benchpress';
|
360 | var _SET_TIMEOUT = new OpaqueToken('PerflogMetric.setTimeout');
|
361 |
|
362 | var _MARK_NAME_FRAME_CAPUTRE = 'frameCapture';
|
363 |
|
364 | var _FRAME_TIME_SMOOTH_THRESHOLD = 17;
|
365 |
|
366 | var _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 | ];
|