Simplify the test harness
[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
86 function Stage() {}
87
88 Stage.prototype =
89 {
90     initialize: function(benchmark)
91     {
92         this._benchmark = benchmark;
93         this._element = document.getElementById("stage");
94         this._element.setAttribute("width", document.body.offsetWidth);
95         this._element.setAttribute("height", document.body.offsetHeight);
96         this._size = Point.elementClientSize(this._element).subtract(Insets.elementPadding(this._element).size);
97     },
98
99     get element()
100     {
101         return this._element;
102     },
103
104     get size()
105     {
106         return this._size;
107     },
108
109     complexity: function()
110     {
111         return 0;
112     },
113
114     random: function(min, max)
115     {
116         return (Math.random() * (max - min)) + min;
117     },
118
119     randomBool: function()
120     {
121         return !!Math.round(this.random(0, 1));
122     },
123
124     randomInt: function(min, max)
125     {
126         return Math.round(this.random(min, max));
127     },
128
129     randomPosition: function(maxPosition)
130     {
131         return new Point(this.randomInt(0, maxPosition.x), this.randomInt(0, maxPosition.y));
132     },
133
134     randomSquareSize: function(min, max)
135     {
136         var side = this.random(min, max);
137         return new Point(side, side);
138     },
139
140     randomVelocity: function(maxVelocity)
141     {
142         return this.random(maxVelocity / 8, maxVelocity);
143     },
144
145     randomAngle: function()
146     {
147         return this.random(0, Math.PI * 2);
148     },
149
150     randomColor: function()
151     {
152         var min = 32;
153         var max = 256 - 32;
154         return "#"
155             + this.randomInt(min, max).toString(16)
156             + this.randomInt(min, max).toString(16)
157             + this.randomInt(min, max).toString(16);
158     },
159
160     randomRotater: function()
161     {
162         return new Rotater(this.random(1000, 10000));
163     },
164
165     tune: function()
166     {
167         throw "Not implemented";
168     },
169
170     animate: function()
171     {
172         throw "Not implemented";
173     },
174
175     clear: function()
176     {
177         return this.tune(-this.tune(0));
178     }
179 };
180
181 function Animator()
182 {
183     this._frameCount = 0;
184     this._dropFrameCount = 1;
185     this._measureFrameCount = 3;
186     this._referenceTime = 0;
187     this._currentTimeOffset = 0;
188     this._estimator = new KalmanEstimator(60);
189 }
190
191 Animator.prototype =
192 {
193     initialize: function(benchmark)
194     {
195         this._benchmark = benchmark;
196         this._estimateFrameRate = benchmark.options["estimated-frame-rate"];
197     },
198
199     get benchmark()
200     {
201         return this._benchmark;
202     },
203
204     timeDelta: function()
205     {
206         return this._currentTimeOffset - this._startTimeOffset;
207     },
208
209     _shouldRequestAnotherFrame: function()
210     {
211         var currentTime = performance.now();
212         
213         if (!this._referenceTime)
214             this._referenceTime = currentTime;
215         else
216             this._currentTimeOffset = currentTime - this._referenceTime;
217
218         if (!this._frameCount)
219             this._startTimeOffset = this._currentTimeOffset;
220
221         ++this._frameCount;
222
223         // Start measuring after dropping _dropFrameCount frames.
224         if (this._frameCount == this._dropFrameCount)
225             this._measureTimeOffset = this._currentTimeOffset;
226
227         // Drop _dropFrameCount frames and measure the average of _measureFrameCount frames.
228         if (this._frameCount < this._dropFrameCount + this._measureFrameCount)
229             return true;
230
231         // Get the average FPS of _measureFrameCount frames over measureTimeDelta.
232         var measureTimeDelta = this._currentTimeOffset - this._measureTimeOffset;
233         var currentFrameRate = Math.floor(1000 / (measureTimeDelta / this._measureFrameCount));
234
235         // Use Kalman filter to get a more non-fluctuating frame rate.
236         if (this._estimateFrameRate)
237             currentFrameRate = this._estimator.estimate(currentFrameRate);
238
239         // Adjust the test to reach the desired FPS.
240         var result = this._benchmark.update(this._currentTimeOffset, this.timeDelta(), currentFrameRate);
241
242         // Start the next drop/measure cycle.
243         this._frameCount = 0;
244
245         // If result == 0, no more requestAnimationFrame() will be invoked.
246         return result;
247     },
248
249     animateLoop: function()
250     {
251         if (this._shouldRequestAnotherFrame()) {
252             this._benchmark.stage.animate(this.timeDelta());
253             requestAnimationFrame(this.animateLoop.bind(this));
254         }
255     }
256 }
257
258 function Benchmark(stage, options)
259 {
260     this._options = options;
261
262     this._stage = stage;
263     this._stage.initialize(this);
264     this._animator = new Animator();
265     this._animator.initialize(this);
266
267     this._recordInterval = 200;
268     this._isSampling = false;
269     this._controller = new PIDController(this._options["frame-rate"]);
270     this._sampler = new Sampler(2);
271     this._state = new BenchmarkState(this._options["test-interval"] * 1000);
272 }
273
274 Benchmark.prototype =
275 {
276     get options()
277     {
278         return this._options;
279     },
280
281     get stage()
282     {
283         return this._stage;
284     },
285
286     get animator()
287     {
288         return this._animator;
289     },
290
291     // Called from the load event listener or from this.run().
292     start: function()
293     {
294         this._animator.animateLoop();
295     },
296
297     // Called from the animator to adjust the complexity of the test.
298     update: function(currentTimeOffset, timeDelta, currentFrameRate)
299     {
300         this._state.update(currentTimeOffset);
301
302         var stage = this._state.currentStage();
303         if (stage == BenchmarkState.stages.FINISHED) {
304             this._stage.clear();
305             return false;
306         }
307
308         if (stage == BenchmarkState.stages.SAMPLING && !this._isSampling) {
309             this._sampler.startSampling(this._state.samplingTimeOffset());
310             this._isSampling = true;
311         }
312
313         var tuneValue = 0;
314         if (this._options["adjustment"] == "fixed") {
315             if (this._options["complexity"]) {
316                 // this._stage.tune(0) returns the current complexity of the test.
317                 tuneValue = this._options["complexity"] - this._stage.tune(0);
318             }
319         }
320         else if (!(this._isSampling && this._options["adjustment"] == "fixed-after-warmup")) {
321             // The relationship between frameRate and test complexity is inverse-proportional so we
322             // need to use the negative of PIDController.tune() to change the complexity of the test.
323             tuneValue = -this._controller.tune(currentTimeOffset, timeDelta, currentFrameRate);
324             tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
325         }
326
327         var currentComplexity = this._stage.tune(tuneValue);
328         this.record(currentTimeOffset, currentComplexity, currentFrameRate);
329         return true;
330     },
331
332     record: function(currentTimeOffset, currentComplexity, currentFrameRate)
333     {
334         this._sampler.sample(currentTimeOffset, [currentComplexity, currentFrameRate]);
335
336         if (typeof this._recordTimeOffset == "undefined")
337             this._recordTimeOffset = currentTimeOffset;
338
339         var stage = this._state.currentStage();
340         if (stage != BenchmarkState.stages.FINISHED && currentTimeOffset < this._recordTimeOffset + this._recordInterval)
341             return;
342
343         this._recordTimeOffset = currentTimeOffset;
344     },
345
346     run: function()
347     {
348         this.start();
349
350         var promise = new SimplePromise;
351         var self = this;
352         function resolveWhenFinished() {
353             if (typeof self._state != "undefined" && (self._state.currentStage() == BenchmarkState.stages.FINISHED))
354                 return promise.resolve(self._sampler);
355             setTimeout(resolveWhenFinished.bind(self), 50);
356         }
357
358         resolveWhenFinished();
359         return promise;
360     }
361 };