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