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