Address Said's comments on the benchmark, and do some clean up.
[WebKit-https.git] / PerformanceTests / Animometer / tests / resources / main.js
1 Controller = Utilities.createClass(
2     function(benchmark, options)
3     {
4         // Initialize timestamps relative to the start of the benchmark
5         // In start() the timestamps are offset by the start timestamp
6         this._startTimestamp = 0;
7         this._endTimestamp = options["test-interval"];
8         // Default data series: timestamp, complexity, estimatedFrameLength
9         this._sampler = new Sampler(options["series-count"] || 3, (60 * options["test-interval"] / 1000), this);
10         this._marks = {};
11
12         this._frameLengthEstimator = new SimpleKalmanEstimator(options["kalman-process-error"], options["kalman-measurement-error"]);
13         this._isFrameLengthEstimatorEnabled = true;
14
15         // Length of subsequent intervals; a value of 0 means use no intervals
16         this._intervalLength = options["interval-length"] || 100;
17
18         this.initialComplexity = 0;
19     }, {
20
21     set isFrameLengthEstimatorEnabled(enabled) {
22         this._isFrameLengthEstimatorEnabled = enabled;
23     },
24
25     start: function(startTimestamp, stage)
26     {
27         this._startTimestamp = startTimestamp;
28         this._endTimestamp += startTimestamp;
29         this._measureAndResetInterval(startTimestamp);
30         this.recordFirstSample(startTimestamp, stage);
31     },
32
33     recordFirstSample: function(startTimestamp, stage)
34     {
35         this._sampler.record(startTimestamp, stage.complexity(), -1);
36         this.mark(Strings.json.samplingStartTimeOffset, startTimestamp);
37     },
38
39     mark: function(comment, timestamp, data) {
40         data = data || {};
41         data.time = timestamp;
42         data.index = this._sampler.sampleCount;
43         this._marks[comment] = data;
44     },
45
46     containsMark: function(comment) {
47         return comment in this._marks;
48     },
49
50     _measureAndResetInterval: function(currentTimestamp)
51     {
52         var sampleCount = this._sampler.sampleCount;
53         var averageFrameLength = 0;
54
55         if (this._intervalEndTimestamp) {
56             var intervalStartTimestamp = this._sampler.samples[0][this._intervalStartIndex];
57             averageFrameLength = (currentTimestamp - intervalStartTimestamp) / (sampleCount - this._intervalStartIndex);
58         }
59
60         this._intervalStartIndex = sampleCount;
61         this._intervalEndTimestamp = currentTimestamp + this._intervalLength;
62
63         return averageFrameLength;
64     },
65
66     update: function(timestamp, stage)
67     {
68         var frameLengthEstimate = -1;
69         var didFinishInterval = false;
70         if (!this._intervalLength) {
71             if (this._isFrameLengthEstimatorEnabled) {
72                 this._frameLengthEstimator.sample(timestamp - this._sampler.samples[0][this._sampler.sampleCount - 1]);
73                 frameLengthEstimate = this._frameLengthEstimator.estimate;
74             }
75         } else if (timestamp >= this._intervalEndTimestamp) {
76             var intervalStartTimestamp = this._sampler.samples[0][this._intervalStartIndex];
77             intervalAverageFrameLength = this._measureAndResetInterval(timestamp);
78             if (this._isFrameLengthEstimatorEnabled) {
79                 this._frameLengthEstimator.sample(intervalAverageFrameLength);
80                 frameLengthEstimate = this._frameLengthEstimator.estimate;
81             }
82             didFinishInterval = true;
83             this.didFinishInterval(timestamp, stage, intervalAverageFrameLength);
84         }
85
86         this._sampler.record(timestamp, stage.complexity(), frameLengthEstimate);
87         this.tune(timestamp, stage, didFinishInterval);
88     },
89
90     didFinishInterval: function(timestamp, stage, intervalAverageFrameLength)
91     {
92     },
93
94     tune: function(timestamp, stage, didFinishInterval)
95     {
96     },
97
98     shouldStop: function(timestamp)
99     {
100         return timestamp > this._endTimestamp;
101     },
102
103     results: function()
104     {
105         return this._sampler.processSamples();
106     },
107
108     processSamples: function(results)
109     {
110         var complexityExperiment = new Experiment;
111         var smoothedFrameLengthExperiment = new Experiment;
112
113         var samples = this._sampler.samples;
114
115         var samplingStartIndex = 0, samplingEndIndex = -1;
116         if (Strings.json.samplingStartTimeOffset in this._marks)
117             samplingStartIndex = this._marks[Strings.json.samplingStartTimeOffset].index;
118         if (Strings.json.samplingEndTimeOffset in this._marks)
119             samplingEndIndex = this._marks[Strings.json.samplingEndTimeOffset].index;
120
121         for (var markName in this._marks)
122             this._marks[markName].time -= this._startTimestamp;
123         results[Strings.json.marks] = this._marks;
124
125         results[Strings.json.samples] = samples[0].map(function(timestamp, i) {
126             var result = {
127                 // Represent time in milliseconds
128                 time: timestamp - this._startTimestamp,
129                 complexity: samples[1][i]
130             };
131
132             if (i == 0)
133                 result.frameLength = 1000/60;
134             else
135                 result.frameLength = timestamp - samples[0][i - 1];
136
137             if (samples[2][i] != -1)
138                 result.smoothedFrameLength = samples[2][i];
139
140             // Don't start adding data to the experiments until we reach the sampling timestamp
141             if (i >= samplingStartIndex && (samplingEndIndex == -1 || i < samplingEndIndex)) {
142                 complexityExperiment.sample(result.complexity);
143                 if (result.smoothedFrameLength && result.smoothedFrameLength != -1)
144                     smoothedFrameLengthExperiment.sample(result.smoothedFrameLength);
145             }
146
147             return result;
148         }, this);
149
150         results[Strings.json.score] = complexityExperiment.score(Experiment.defaults.CONCERN);
151
152         var complexityResults = {};
153         results[Strings.json.experiments.complexity] = complexityResults;
154         complexityResults[Strings.json.measurements.average] = complexityExperiment.mean();
155         complexityResults[Strings.json.measurements.concern] = complexityExperiment.concern(Experiment.defaults.CONCERN);
156         complexityResults[Strings.json.measurements.stdev] = complexityExperiment.standardDeviation();
157         complexityResults[Strings.json.measurements.percent] = complexityExperiment.percentage();
158
159         var smoothedFrameLengthResults = {};
160         results[Strings.json.experiments.frameRate] = smoothedFrameLengthResults;
161         smoothedFrameLengthResults[Strings.json.measurements.average] = 1000 / smoothedFrameLengthExperiment.mean();
162         smoothedFrameLengthResults[Strings.json.measurements.concern] = smoothedFrameLengthExperiment.concern(Experiment.defaults.CONCERN);
163         smoothedFrameLengthResults[Strings.json.measurements.stdev] = smoothedFrameLengthExperiment.standardDeviation();
164         smoothedFrameLengthResults[Strings.json.measurements.percent] = smoothedFrameLengthExperiment.percentage();
165     }
166 });
167
168 StepController = Utilities.createSubclass(Controller,
169     function(benchmark, options)
170     {
171         options["interval-length"] = 0;
172         Controller.call(this, benchmark, options);
173         this.initialComplexity = options["complexity"];
174         this._stepped = false;
175         this._stepTime = options["test-interval"] / 2;
176     }, {
177
178     start: function(startTimestamp, stage)
179     {
180         Controller.prototype.start.call(this, startTimestamp, stage);
181         this._stepTime += startTimestamp;
182     },
183
184     tune: function(timestamp, stage)
185     {
186         if (this._stepped || timestamp < this._stepTime)
187             return;
188
189         this.mark(Strings.json.samplingEndTimeOffset, timestamp);
190         this._stepped = true;
191         stage.tune(stage.complexity() * 3);
192     }
193 });
194
195 AdaptiveController = Utilities.createSubclass(Controller,
196     function(benchmark, options)
197     {
198         // Data series: timestamp, complexity, estimatedIntervalFrameLength
199         Controller.call(this, benchmark, options);
200
201         // All tests start at 0, so we expect to see 60 fps quickly.
202         this._samplingTimestamp = options["test-interval"] / 2;
203         this._startedSampling = false;
204         this._targetFrameRate = options["frame-rate"];
205         this._pid = new PIDController(this._targetFrameRate);
206
207         this._intervalFrameCount = 0;
208         this._numberOfFramesToMeasurePerInterval = 4;
209     }, {
210
211     start: function(startTimestamp, stage)
212     {
213         Controller.prototype.start.call(this, startTimestamp, stage);
214
215         this._samplingTimestamp += startTimestamp;
216         this._intervalTimestamp = startTimestamp;
217     },
218
219     recordFirstSample: function(startTimestamp, stage)
220     {
221         this._sampler.record(startTimestamp, stage.complexity(), -1);
222     },
223
224     update: function(timestamp, stage)
225     {
226         if (!this._startedSampling && timestamp >= this._samplingTimestamp) {
227             this._startedSampling = true;
228             this.mark(Strings.json.samplingStartTimeOffset, this._samplingTimestamp);
229         }
230
231         // Start the work for the next frame.
232         ++this._intervalFrameCount;
233
234         if (this._intervalFrameCount < this._numberOfFramesToMeasurePerInterval) {
235             this._sampler.record(timestamp, stage.complexity(), -1);
236             return;
237         }
238
239         // Adjust the test to reach the desired FPS.
240         var intervalLength = timestamp - this._intervalTimestamp;
241         this._frameLengthEstimator.sample(intervalLength / this._numberOfFramesToMeasurePerInterval);
242         var intervalEstimatedFrameRate = 1000 / this._frameLengthEstimator.estimate;
243         var tuneValue = -this._pid.tune(timestamp - this._startTimestamp, intervalLength, intervalEstimatedFrameRate);
244         tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
245         stage.tune(tuneValue);
246
247         this._sampler.record(timestamp, stage.complexity(), this._frameLengthEstimator.estimate);
248
249         // Start the next interval.
250         this._intervalFrameCount = 0;
251         this._intervalTimestamp = timestamp;
252     },
253
254     processSamples: function(results)
255     {
256         Controller.prototype.processSamples.call(this, results);
257         results[Strings.json.targetFrameLength] = 1000 / this._targetFrameRate;
258     }
259 });
260
261 Stage = Utilities.createClass(
262     function()
263     {
264     }, {
265
266     initialize: function(benchmark)
267     {
268         this._benchmark = benchmark;
269         this._element = document.getElementById("stage");
270         this._element.setAttribute("width", document.body.offsetWidth);
271         this._element.setAttribute("height", document.body.offsetHeight);
272         this._size = Point.elementClientSize(this._element).subtract(Insets.elementPadding(this._element).size);
273     },
274
275     get element()
276     {
277         return this._element;
278     },
279
280     get size()
281     {
282         return this._size;
283     },
284
285     complexity: function()
286     {
287         return 0;
288     },
289
290     tune: function()
291     {
292         throw "Not implemented";
293     },
294
295     animate: function()
296     {
297         throw "Not implemented";
298     },
299
300     clear: function()
301     {
302         return this.tune(-this.tune(0));
303     }
304 });
305
306 Utilities.extendObject(Stage, {
307     random: function(min, max)
308     {
309         return (Math.random() * (max - min)) + min;
310     },
311
312     randomBool: function()
313     {
314         return !!Math.round(this.random(0, 1));
315     },
316
317     randomInt: function(min, max)
318     {
319         return Math.floor(this.random(min, max + 1));
320     },
321
322     randomPosition: function(maxPosition)
323     {
324         return new Point(this.randomInt(0, maxPosition.x), this.randomInt(0, maxPosition.y));
325     },
326
327     randomSquareSize: function(min, max)
328     {
329         var side = this.random(min, max);
330         return new Point(side, side);
331     },
332
333     randomVelocity: function(maxVelocity)
334     {
335         return this.random(maxVelocity / 8, maxVelocity);
336     },
337
338     randomAngle: function()
339     {
340         return this.random(0, Math.PI * 2);
341     },
342
343     randomColor: function()
344     {
345         var min = 32;
346         var max = 256 - 32;
347         return "#"
348             + this.randomInt(min, max).toString(16)
349             + this.randomInt(min, max).toString(16)
350             + this.randomInt(min, max).toString(16);
351     },
352
353     rotatingColor: function(cycleLengthMs, saturation, lightness)
354     {
355         return "hsl("
356             + Stage.dateFractionalValue(cycleLengthMs) * 360 + ", "
357             + ((saturation || .8) * 100).toFixed(0) + "%, "
358             + ((lightness || .35) * 100).toFixed(0) + "%)";
359     },
360
361     // Returns a fractional value that wraps around within [0,1]
362     dateFractionalValue: function(cycleLengthMs)
363     {
364         return (Date.now() / (cycleLengthMs || 2000)) % 1;
365     },
366
367     // Returns an increasing value slowed down by factor
368     dateCounterValue: function(factor)
369     {
370         return Date.now() / factor;
371     },
372
373     randomRotater: function()
374     {
375         return new Rotater(this.random(1000, 10000));
376     }
377 });
378
379 Rotater = Utilities.createClass(
380     function(rotateInterval)
381     {
382         this._timeDelta = 0;
383         this._rotateInterval = rotateInterval;
384         this._isSampling = false;
385     }, {
386
387     get interval()
388     {
389         return this._rotateInterval;
390     },
391
392     next: function(timeDelta)
393     {
394         this._timeDelta = (this._timeDelta + timeDelta) % this._rotateInterval;
395     },
396
397     degree: function()
398     {
399         return (360 * this._timeDelta) / this._rotateInterval;
400     },
401
402     rotateZ: function()
403     {
404         return "rotateZ(" + Math.floor(this.degree()) + "deg)";
405     },
406
407     rotate: function(center)
408     {
409         return "rotate(" + Math.floor(this.degree()) + ", " + center.x + "," + center.y + ")";
410     }
411 });
412
413 Benchmark = Utilities.createClass(
414     function(stage, options)
415     {
416         this._animateLoop = this._animateLoop.bind(this);
417
418         this._stage = stage;
419         this._stage.initialize(this, options);
420
421         switch (options["time-measurement"])
422         {
423         case "performance":
424             this._getTimestamp = performance.now.bind(performance);
425             break;
426         case "date":
427             this._getTimestamp = Date.now;
428             break;
429         }
430
431         options["test-interval"] *= 1000;
432         switch (options["adjustment"])
433         {
434         case "step":
435             this._controller = new StepController(this, options);
436             break;
437         case "adaptive":
438         default:
439             this._controller = new AdaptiveController(this, options);
440             break;
441         }
442     }, {
443
444     get stage()
445     {
446         return this._stage;
447     },
448
449     run: function()
450     {
451         return this.waitUntilReady().then(function() {
452             this._finishPromise = new SimplePromise;
453             this._previousTimestamp = this._getTimestamp();
454             this._didWarmUp = false;
455             this._stage.tune(this._controller.initialComplexity - this._stage.complexity());
456             this._animateLoop();
457             return this._finishPromise;
458         }.bind(this));
459     },
460
461     // Subclasses should override this if they have setup to do prior to commencing.
462     waitUntilReady: function()
463     {
464         var promise = new SimplePromise;
465         promise.resolve();
466         return promise;
467     },
468
469     _animateLoop: function()
470     {
471         this._currentTimestamp = this._getTimestamp();
472
473         if (this._controller.shouldStop(this._currentTimestamp)) {
474             this._finishPromise.resolve(this._controller.results());
475             return;
476         }
477
478         if (!this._didWarmUp) {
479             if (this._currentTimestamp - this._previousTimestamp >= 100) {
480                 this._didWarmUp = true;
481                 this._controller.start(this._currentTimestamp, this._stage);
482                 this._previousTimestamp = this._currentTimestamp;
483             }
484
485             this._stage.animate(0);
486             requestAnimationFrame(this._animateLoop);
487             return;
488         }
489
490         this._controller.update(this._currentTimestamp, this._stage);
491         this._stage.animate(this._currentTimestamp - this._previousTimestamp);
492         this._previousTimestamp = this._currentTimestamp;
493         requestAnimationFrame(this._animateLoop);
494     }
495 });