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