Refactor helper methods for getting random values for a stage.
[WebKit-https.git] / PerformanceTests / Animometer / tests / resources / main.js
1 function Rotater(rotateInterval)
2 {
3     this._timeDelta = 0;
4     this._rotateInterval = rotateInterval;
5 }
6
7 Rotater.prototype =
8 {
9     get interval()
10     {
11         return this._rotateInterval;
12     },
13
14     next: function(timeDelta)
15     {
16         this._timeDelta = (this._timeDelta + timeDelta) % this._rotateInterval;
17     },
18
19     degree: function()
20     {
21         return (360 * this._timeDelta) / this._rotateInterval;
22     },
23
24     rotateZ: function()
25     {
26         return "rotateZ(" + Math.floor(this.degree()) + "deg)";
27     },
28
29     rotate: function(center)
30     {
31         return "rotate(" + Math.floor(this.degree()) + ", " + center.x + "," + center.y + ")";
32     }
33 };
34
35 function BenchmarkState(testInterval)
36 {
37     this._currentTimeOffset = 0;
38     this._stageInterval = testInterval / BenchmarkState.stages.FINISHED;
39 }
40
41 // The enum values and the messages should be in the same order
42 BenchmarkState.stages = {
43     WARMING: 0,
44     SAMPLING: 1,
45     FINISHED: 2,
46 }
47
48 BenchmarkState.prototype =
49 {
50     _timeOffset: function(stage)
51     {
52         return stage * this._stageInterval;
53     },
54
55     _message: function(stage, timeOffset)
56     {
57         if (stage == BenchmarkState.stages.FINISHED)
58             return BenchmarkState.stages.messages[stage];
59
60         return BenchmarkState.stages.messages[stage] + "... ("
61             + Math.floor((timeOffset - this._timeOffset(stage)) / 1000) + "/"
62             + Math.floor((this._timeOffset(stage + 1) - this._timeOffset(stage)) / 1000) + ")";
63     },
64
65     update: function(currentTimeOffset)
66     {
67         this._currentTimeOffset = currentTimeOffset;
68     },
69
70     samplingTimeOffset: function()
71     {
72         return this._timeOffset(BenchmarkState.stages.SAMPLING);
73     },
74
75     currentStage: function()
76     {
77         for (var stage = BenchmarkState.stages.WARMING; stage < BenchmarkState.stages.FINISHED; ++stage) {
78             if (this._currentTimeOffset < this._timeOffset(stage + 1))
79                 return stage;
80         }
81         return BenchmarkState.stages.FINISHED;
82     }
83 }
84
85 function Stage() {}
86
87 Utilities.extendObject(Stage, {
88     random: function(min, max)
89     {
90         return (Math.random() * (max - min)) + min;
91     },
92
93     randomBool: function()
94     {
95         return !!Math.round(this.random(0, 1));
96     },
97
98     randomInt: function(min, max)
99     {
100         return Math.round(this.random(min, max));
101     },
102
103     randomPosition: function(maxPosition)
104     {
105         return new Point(this.randomInt(0, maxPosition.x), this.randomInt(0, maxPosition.y));
106     },
107
108     randomSquareSize: function(min, max)
109     {
110         var side = this.random(min, max);
111         return new Point(side, side);
112     },
113
114     randomVelocity: function(maxVelocity)
115     {
116         return this.random(maxVelocity / 8, maxVelocity);
117     },
118
119     randomAngle: function()
120     {
121         return this.random(0, Math.PI * 2);
122     },
123
124     randomColor: function()
125     {
126         var min = 32;
127         var max = 256 - 32;
128         return "#"
129             + this.randomInt(min, max).toString(16)
130             + this.randomInt(min, max).toString(16)
131             + this.randomInt(min, max).toString(16);
132     },
133
134     randomRotater: function()
135     {
136         return new Rotater(this.random(1000, 10000));
137     }
138 });
139
140 Stage.prototype =
141 {
142     initialize: function(benchmark)
143     {
144         this._benchmark = benchmark;
145         this._element = document.getElementById("stage");
146         this._element.setAttribute("width", document.body.offsetWidth);
147         this._element.setAttribute("height", document.body.offsetHeight);
148         this._size = Point.elementClientSize(this._element).subtract(Insets.elementPadding(this._element).size);
149     },
150
151     get element()
152     {
153         return this._element;
154     },
155
156     get size()
157     {
158         return this._size;
159     },
160
161     complexity: function()
162     {
163         return 0;
164     },
165
166     tune: function()
167     {
168         throw "Not implemented";
169     },
170
171     animate: function()
172     {
173         throw "Not implemented";
174     },
175
176     clear: function()
177     {
178         return this.tune(-this.tune(0));
179     }
180 };
181
182 function Animator()
183 {
184     this._intervalFrameCount = 0;
185     this._numberOfFramesToMeasurePerInterval = 3;
186     this._referenceTime = 0;
187     this._currentTimeOffset = 0;
188 }
189
190 Animator.prototype =
191 {
192     initialize: function(benchmark)
193     {
194         this._benchmark = benchmark;
195
196         // Use Kalman filter to get a more non-fluctuating frame rate.
197         if (benchmark.options["estimated-frame-rate"])
198             this._estimator = new KalmanEstimator(60);
199         else
200             this._estimator = new IdentityEstimator;
201     },
202
203     get benchmark()
204     {
205         return this._benchmark;
206     },
207
208     _intervalTimeDelta: function()
209     {
210         return this._currentTimeOffset - this._startTimeOffset;
211     },
212
213     _shouldRequestAnotherFrame: function()
214     {
215         // Cadence is number of frames to measure, then one more frame to adjust the scene, and drop
216         var currentTime = performance.now();
217
218         if (!this._referenceTime)
219             this._referenceTime = currentTime;
220
221         this._currentTimeOffset = currentTime - this._referenceTime;
222
223         if (!this._intervalFrameCount)
224             this._startTimeOffset = this._currentTimeOffset;
225
226         // Start the work for the next frame.
227         ++this._intervalFrameCount;
228
229         // Drop _dropFrameCount frames and measure the average of _measureFrameCount frames.
230         if (this._intervalFrameCount <= this._numberOfFramesToMeasurePerInterval) {
231             this._benchmark.record(this._currentTimeOffset, -1, -1);
232             return true;
233         }
234
235         // Get the average FPS of _measureFrameCount frames over intervalTimeDelta.
236         var intervalTimeDelta = this._intervalTimeDelta();
237         var intervalFrameRate = 1000 / (intervalTimeDelta / this._numberOfFramesToMeasurePerInterval);
238         var estimatedIntervalFrameRate = this._estimator.estimate(intervalFrameRate);
239         // Record the complexity of the frame we just rendered. The next frame's time respresents the adjusted
240         // complexity
241         this._benchmark.record(this._currentTimeOffset, estimatedIntervalFrameRate, intervalFrameRate);
242
243         // Adjust the test to reach the desired FPS.
244         var shouldContinueRunning = this._benchmark.update(this._currentTimeOffset, intervalTimeDelta, estimatedIntervalFrameRate);
245
246         // Start the next drop/measure cycle.
247         this._intervalFrameCount = 0;
248
249         // If result is false, no more requestAnimationFrame() will be invoked.
250         return shouldContinueRunning;
251     },
252
253     animateLoop: function()
254     {
255         if (this._shouldRequestAnotherFrame()) {
256             this._benchmark.stage.animate(this._intervalTimeDelta());
257             requestAnimationFrame(this.animateLoop.bind(this));
258         }
259     }
260 }
261
262 function Benchmark(stage, options)
263 {
264     this._options = options;
265
266     this._stage = stage;
267     this._stage.initialize(this);
268     this._animator = new Animator();
269     this._animator.initialize(this);
270
271     this._recordInterval = 200;
272     this._isSampling = false;
273     this._controller = new PIDController(this._options["frame-rate"]);
274     this._sampler = new Sampler(4, 60 * this._options["test-interval"], this);
275     this._state = new BenchmarkState(this._options["test-interval"] * 1000);
276 }
277
278 Benchmark.prototype =
279 {
280     get options()
281     {
282         return this._options;
283     },
284
285     get stage()
286     {
287         return this._stage;
288     },
289
290     get animator()
291     {
292         return this._animator;
293     },
294
295     // Called from the load event listener or from this.run().
296     start: function()
297     {
298         this._animator.animateLoop();
299     },
300
301     // Called from the animator to adjust the complexity of the test.
302     update: function(currentTimeOffset, intervalTimeDelta, estimatedIntervalFrameRate)
303     {
304         this._state.update(currentTimeOffset);
305
306         var stage = this._state.currentStage();
307         if (stage == BenchmarkState.stages.FINISHED) {
308             this._stage.clear();
309             return false;
310         }
311
312         if (stage == BenchmarkState.stages.SAMPLING && !this._isSampling) {
313             this._sampler.mark(Strings.json.samplingTimeOffset, {
314                 time: this._state.samplingTimeOffset() / 1000
315             });
316             this._isSampling = true;
317         }
318
319         var tuneValue = 0;
320         if (this._options["adjustment"] == "fixed") {
321             if (this._options["complexity"]) {
322                 // this._stage.tune(0) returns the current complexity of the test.
323                 tuneValue = this._options["complexity"] - this._stage.tune(0);
324             }
325         }
326         else if (!(this._isSampling && this._options["adjustment"] == "fixed-after-warmup")) {
327             // The relationship between frameRate and test complexity is inverse-proportional so we
328             // need to use the negative of PIDController.tune() to change the complexity of the test.
329             tuneValue = -this._controller.tune(currentTimeOffset, intervalTimeDelta, estimatedIntervalFrameRate);
330             tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
331         }
332
333         this._stage.tune(tuneValue);
334
335         if (typeof this._recordTimeOffset == "undefined")
336             this._recordTimeOffset = currentTimeOffset;
337
338         var stage = this._state.currentStage();
339         if (stage != BenchmarkState.stages.FINISHED && currentTimeOffset < this._recordTimeOffset + this._recordInterval)
340             return true;
341
342         this._recordTimeOffset = currentTimeOffset;
343         return true;
344     },
345
346     record: function(currentTimeOffset, estimatedFrameRate, intervalFrameRate)
347     {
348         // If the frame rate is -1 it means we are still recording for this sample
349         this._sampler.record(currentTimeOffset, this.stage.complexity(), estimatedFrameRate, intervalFrameRate);
350     },
351
352     run: function()
353     {
354         return this.waitUntilReady().then(function() {
355             this.start();
356             var promise = new SimplePromise;
357             var resolveWhenFinished = function() {
358                 if (typeof this._state != "undefined" && (this._state.currentStage() == BenchmarkState.stages.FINISHED))
359                     return promise.resolve(this._sampler);
360                 setTimeout(resolveWhenFinished, 50);
361             }.bind(this);
362
363             resolveWhenFinished();
364             return promise;
365         }.bind(this));
366     },
367
368     waitUntilReady: function()
369     {
370         var promise = new SimplePromise;
371         promise.resolve();
372         return promise;
373     },
374
375     processSamples: function(results)
376     {
377         var complexity = new Experiment;
378         var smoothedFPS = new Experiment;
379         var samplingIndex = 0;
380
381         var samplingMark = this._sampler.marks[Strings.json.samplingTimeOffset];
382         if (samplingMark) {
383             samplingIndex = samplingMark.index;
384             results[Strings.json.samplingTimeOffset] = samplingMark.time;
385         }
386
387         results[Strings.json.samples] = this._sampler.samples[0].map(function(d, i) {
388             var result = {
389                 // time offsets represented as seconds
390                 time: d/1000,
391                 complexity: this._sampler.samples[1][i]
392             };
393
394             // time offsets represented as FPS
395             if (i == 0)
396                 result.fps = 60;
397             else
398                 result.fps = 1000 / (d - this._sampler.samples[0][i - 1]);
399
400             var smoothedFPSresult = this._sampler.samples[2][i];
401             if (smoothedFPSresult != -1) {
402                 result.smoothedFPS = smoothedFPSresult;
403                 result.intervalFPS = this._sampler.samples[3][i];
404             }
405
406             if (i >= samplingIndex) {
407                 complexity.sample(result.complexity);
408                 if (smoothedFPSresult != -1) {
409                     smoothedFPS.sample(smoothedFPSresult);
410                 }
411             }
412
413             return result;
414         }, this);
415
416         results[Strings.json.score] = complexity.score(Experiment.defaults.CONCERN);
417         [complexity, smoothedFPS].forEach(function(experiment, index) {
418             var jsonExperiment = !index ? Strings.json.experiments.complexity : Strings.json.experiments.frameRate;
419             results[jsonExperiment] = {};
420             results[jsonExperiment][Strings.json.measurements.average] = experiment.mean();
421             results[jsonExperiment][Strings.json.measurements.concern] = experiment.concern(Experiment.defaults.CONCERN);
422             results[jsonExperiment][Strings.json.measurements.stdev] = experiment.standardDeviation();
423             results[jsonExperiment][Strings.json.measurements.percent] = experiment.percentage();
424         });
425     }
426 };