focus() / blur() should be on HTMLElement / SVGElement, not Element
[WebKit-https.git] / PerformanceTests / Animometer / tests / resources / main.js
1 Sampler = Utilities.createClass(
2     function(seriesCount, expectedSampleCount, processor)
3     {
4         this._processor = processor;
5
6         this.samples = [];
7         for (var i = 0; i < seriesCount; ++i) {
8             var array = new Array(expectedSampleCount);
9             array.fill(0);
10             this.samples[i] = array;
11         }
12         this.sampleCount = 0;
13     }, {
14
15     record: function() {
16         // Assume that arguments.length == this.samples.length
17         for (var i = 0; i < arguments.length; i++) {
18             this.samples[i][this.sampleCount] = arguments[i];
19         }
20         ++this.sampleCount;
21     },
22
23     processSamples: function()
24     {
25         var results = {};
26
27         // Remove unused capacity
28         this.samples = this.samples.map(function(array) {
29             return array.slice(0, this.sampleCount);
30         }, this);
31
32         this._processor.processSamples(results);
33
34         return results;
35     }
36 });
37
38 Controller = Utilities.createClass(
39     function(benchmark, options)
40     {
41         // Initialize timestamps relative to the start of the benchmark
42         // In start() the timestamps are offset by the start timestamp
43         this._startTimestamp = 0;
44         this._endTimestamp = options["test-interval"];
45         // Default data series: timestamp, complexity, estimatedFrameLength
46         var sampleSize = options["sample-capacity"] || (60 * options["test-interval"] / 1000);
47         this._sampler = new Sampler(options["series-count"] || 3, sampleSize, this);
48         this._marks = {};
49
50         this._frameLengthEstimator = new SimpleKalmanEstimator(options["kalman-process-error"], options["kalman-measurement-error"]);
51         this._isFrameLengthEstimatorEnabled = true;
52
53         // Length of subsequent intervals; a value of 0 means use no intervals
54         this.intervalSamplingLength = 100;
55
56         this.initialComplexity = 0;
57     }, {
58
59     set isFrameLengthEstimatorEnabled(enabled) {
60         this._isFrameLengthEstimatorEnabled = enabled;
61     },
62
63     start: function(startTimestamp, stage)
64     {
65         this._startTimestamp = startTimestamp;
66         this._endTimestamp += startTimestamp;
67         this._previousTimestamp = startTimestamp;
68         this._measureAndResetInterval(startTimestamp);
69         this.recordFirstSample(startTimestamp, stage);
70     },
71
72     recordFirstSample: function(startTimestamp, stage)
73     {
74         this._sampler.record(startTimestamp, stage.complexity(), -1);
75         this.mark(Strings.json.samplingStartTimeOffset, startTimestamp);
76     },
77
78     mark: function(comment, timestamp, data) {
79         data = data || {};
80         data.time = timestamp;
81         data.index = this._sampler.sampleCount;
82         this._marks[comment] = data;
83     },
84
85     containsMark: function(comment) {
86         return comment in this._marks;
87     },
88
89     _measureAndResetInterval: function(currentTimestamp)
90     {
91         var sampleCount = this._sampler.sampleCount;
92         var averageFrameLength = 0;
93
94         if (this._intervalEndTimestamp) {
95             var intervalStartTimestamp = this._sampler.samples[0][this._intervalStartIndex];
96             averageFrameLength = (currentTimestamp - intervalStartTimestamp) / (sampleCount - this._intervalStartIndex);
97         }
98
99         this._intervalStartIndex = sampleCount;
100         this._intervalEndTimestamp = currentTimestamp + this.intervalSamplingLength;
101
102         return averageFrameLength;
103     },
104
105     update: function(timestamp, stage)
106     {
107         var lastFrameLength = timestamp - this._previousTimestamp;
108         this._previousTimestamp = timestamp;
109
110         var frameLengthEstimate = -1, intervalAverageFrameLength = -1;
111         var didFinishInterval = false;
112         if (!this.intervalSamplingLength) {
113             if (this._isFrameLengthEstimatorEnabled) {
114                 this._frameLengthEstimator.sample(lastFrameLength);
115                 frameLengthEstimate = this._frameLengthEstimator.estimate;
116             }
117         } else if (timestamp >= this._intervalEndTimestamp) {
118             var intervalStartTimestamp = this._sampler.samples[0][this._intervalStartIndex];
119             intervalAverageFrameLength = this._measureAndResetInterval(timestamp);
120             if (this._isFrameLengthEstimatorEnabled) {
121                 this._frameLengthEstimator.sample(intervalAverageFrameLength);
122                 frameLengthEstimate = this._frameLengthEstimator.estimate;
123             }
124             didFinishInterval = true;
125             this.didFinishInterval(timestamp, stage, intervalAverageFrameLength);
126         }
127
128         this._sampler.record(timestamp, stage.complexity(), frameLengthEstimate);
129         this.tune(timestamp, stage, lastFrameLength, didFinishInterval, intervalAverageFrameLength);
130     },
131
132     didFinishInterval: function(timestamp, stage, intervalAverageFrameLength)
133     {
134     },
135
136     tune: function(timestamp, stage, lastFrameLength, didFinishInterval, intervalAverageFrameLength)
137     {
138     },
139
140     shouldStop: function(timestamp)
141     {
142         return timestamp > this._endTimestamp;
143     },
144
145     results: function()
146     {
147         return this._sampler.processSamples();
148     },
149
150     _processComplexitySamples: function(complexitySamples, complexityAverageSamples)
151     {
152         complexitySamples.sort(function(a, b) {
153             return a.complexity - b.complexity;
154         });
155
156         // Samples averaged based on complexity
157         var currentComplexity = -1;
158         var experimentAtComplexity;
159         function addSample() {
160             var mean = experimentAtComplexity.mean();
161             var stdev = experimentAtComplexity.standardDeviation();
162             complexityAverageSamples.push({
163                 complexity: currentComplexity,
164                 frameLength: mean,
165                 stdev: stdev
166             });
167         }
168         complexitySamples.forEach(function(sample) {
169             if (sample.complexity != currentComplexity) {
170                 if (currentComplexity > -1)
171                     addSample();
172
173                 currentComplexity = sample.complexity;
174                 experimentAtComplexity = new Experiment;
175             }
176             experimentAtComplexity.sample(sample.frameLength);
177         });
178         // Finish off the last one
179         addSample();
180     },
181
182     processSamples: function(results)
183     {
184         var complexityExperiment = new Experiment;
185         var smoothedFrameLengthExperiment = new Experiment;
186
187         var samples = this._sampler.samples;
188
189         for (var markName in this._marks)
190             this._marks[markName].time -= this._startTimestamp;
191         results[Strings.json.marks] = this._marks;
192
193         results[Strings.json.samples] = {};
194
195         var complexitySamples = [], complexityAverageSamples = [];
196         results[Strings.json.samples][Strings.json.complexity] = complexitySamples;
197         results[Strings.json.samples][Strings.json.complexityAverage] = complexityAverageSamples;
198
199         results[Strings.json.samples][Strings.json.controller] = samples[0].map(function(timestamp, i) {
200             var sample = {
201                 // Represent time in milliseconds
202                 time: timestamp - this._startTimestamp,
203                 complexity: samples[1][i]
204             };
205
206             if (i == 0)
207                 sample.frameLength = 1000/60;
208             else
209                 sample.frameLength = timestamp - samples[0][i - 1];
210
211             if (samples[2][i] != -1)
212                 sample.smoothedFrameLength = samples[2][i];
213
214             complexitySamples.push(sample);
215             return sample;
216         }, this);
217
218         this._processComplexitySamples(complexitySamples, complexityAverageSamples);
219     }
220 });
221
222 FixedController = Utilities.createSubclass(Controller,
223     function(benchmark, options)
224     {
225         Controller.call(this, benchmark, options);
226         this.initialComplexity = options["complexity"];
227         this.intervalSamplingLength = 0;
228     }
229 );
230
231 StepController = Utilities.createSubclass(Controller,
232     function(benchmark, options)
233     {
234         Controller.call(this, benchmark, options);
235         this.initialComplexity = options["complexity"];
236         this.intervalSamplingLength = 0;
237         this._stepped = false;
238         this._stepTime = options["test-interval"] / 2;
239     }, {
240
241     start: function(startTimestamp, stage)
242     {
243         Controller.prototype.start.call(this, startTimestamp, stage);
244         this._stepTime += startTimestamp;
245     },
246
247     tune: function(timestamp, stage)
248     {
249         if (this._stepped || timestamp < this._stepTime)
250             return;
251
252         this.mark(Strings.json.samplingEndTimeOffset, timestamp);
253         this._stepped = true;
254         stage.tune(stage.complexity() * 3);
255     }
256 });
257
258 AdaptiveController = Utilities.createSubclass(Controller,
259     function(benchmark, options)
260     {
261         // Data series: timestamp, complexity, estimatedIntervalFrameLength
262         Controller.call(this, benchmark, options);
263
264         // All tests start at 0, so we expect to see 60 fps quickly.
265         this._samplingTimestamp = options["test-interval"] / 2;
266         this._startedSampling = false;
267         this._targetFrameRate = options["frame-rate"];
268         this._pid = new PIDController(this._targetFrameRate);
269
270         this._intervalFrameCount = 0;
271         this._numberOfFramesToMeasurePerInterval = 4;
272     }, {
273
274     start: function(startTimestamp, stage)
275     {
276         Controller.prototype.start.call(this, startTimestamp, stage);
277
278         this._samplingTimestamp += startTimestamp;
279         this._intervalTimestamp = startTimestamp;
280     },
281
282     recordFirstSample: function(startTimestamp, stage)
283     {
284         this._sampler.record(startTimestamp, stage.complexity(), -1);
285     },
286
287     update: function(timestamp, stage)
288     {
289         if (!this._startedSampling && timestamp >= this._samplingTimestamp) {
290             this._startedSampling = true;
291             this.mark(Strings.json.samplingStartTimeOffset, this._samplingTimestamp);
292         }
293
294         // Start the work for the next frame.
295         ++this._intervalFrameCount;
296
297         if (this._intervalFrameCount < this._numberOfFramesToMeasurePerInterval) {
298             this._sampler.record(timestamp, stage.complexity(), -1);
299             return;
300         }
301
302         // Adjust the test to reach the desired FPS.
303         var intervalLength = timestamp - this._intervalTimestamp;
304         this._frameLengthEstimator.sample(intervalLength / this._numberOfFramesToMeasurePerInterval);
305         var intervalEstimatedFrameRate = 1000 / this._frameLengthEstimator.estimate;
306         var tuneValue = -this._pid.tune(timestamp - this._startTimestamp, intervalLength, intervalEstimatedFrameRate);
307         tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
308         stage.tune(tuneValue);
309
310         this._sampler.record(timestamp, stage.complexity(), this._frameLengthEstimator.estimate);
311
312         // Start the next interval.
313         this._intervalFrameCount = 0;
314         this._intervalTimestamp = timestamp;
315     }
316 });
317
318 RampController = Utilities.createSubclass(Controller,
319     function(benchmark, options)
320     {
321         // The tier warmup takes at most 5 seconds
322         options["sample-capacity"] = (options["test-interval"] / 1000 + 5) * 60;
323         Controller.call(this, benchmark, options);
324
325         // Initially start with a tier test to find the bounds
326         // The number of objects in a tier test is 10^|_tier|
327         this._tier = -.5;
328         // The timestamp is first set after the first interval completes
329         this._tierStartTimestamp = 0;
330         this._minimumComplexity = 0;
331         this._maximumComplexity = 0;
332
333         // After the tier range is determined, figure out the number of ramp iterations
334         var minimumRampLength = 2500;
335         var totalRampIterations = Math.max(1, Math.floor(this._endTimestamp / minimumRampLength));
336         // Give a little extra room to run since the ramps won't be exactly this length
337         this._rampLength = Math.floor((this._endTimestamp - totalRampIterations * this.intervalSamplingLength) / totalRampIterations);
338         this._rampDidWarmup = false;
339         this._rampRegressions = [];
340
341         this._finishedTierSampling = false;
342         this._changePointEstimator = new Experiment;
343         this._minimumComplexityEstimator = new Experiment;
344         // Estimates all frames within an interval
345         this._intervalFrameLengthEstimator = new Experiment;
346     }, {
347
348     // If the engine can handle the tier's complexity at the desired frame rate, test for a short
349     // period, then move on to the next tier
350     tierFastTestLength: 250,
351     // If the engine is under stress, let the test run a little longer to let the measurement settle
352     tierSlowTestLength: 750,
353
354     rampWarmupLength: 200,
355
356     // Used for regression calculations in the ramps
357     frameLengthDesired: 1000/60,
358     // Add some tolerance; frame lengths shorter than this are considered to be @ the desired frame length
359     frameLengthDesiredThreshold: 1000/58,
360     // Represents the lower bound threshold in order to find the right complexity range to sample
361     frameLengthSlowestThreshold: 1000/30,
362     // Try to make each ramp get this slow so that we can cross the break point
363     frameLengthRampLowerThreshold: 1000/45,
364
365     start: function(startTimestamp, stage)
366     {
367         Controller.prototype.start.call(this, startTimestamp, stage);
368         this._rampStartTimestamp = 0;
369     },
370
371     didFinishInterval: function(timestamp, stage, intervalAverageFrameLength)
372     {
373         if (!this._finishedTierSampling) {
374             if (this._tierStartTimestamp > 0 && timestamp < this._tierStartTimestamp + this.tierFastTestLength)
375                 return;
376
377             var currentComplexity = stage.complexity();
378             var currentFrameLength = this._frameLengthEstimator.estimate;
379             if (currentFrameLength < this.frameLengthSlowestThreshold) {
380                 var isAnimatingAt60FPS = currentFrameLength < this.frameLengthDesiredThreshold;
381                 var hasFinishedSlowTierTest = timestamp > this._tierStartTimestamp + this.tierSlowTestLength;
382
383                 if (!isAnimatingAt60FPS && !hasFinishedSlowTierTest)
384                     return;
385
386                 // We're measuring at 60 fps, so quickly move on to the next tier, or
387                 // we've slower than 60 fps, but we've let this tier run long enough to
388                 // get an estimate
389                 this._lastTierComplexity = currentComplexity;
390                 this._lastTierFrameLength = currentFrameLength;
391
392                 this._tier += .5;
393                 var nextTierComplexity = Math.round(Math.pow(10, this._tier));
394                 stage.tune(nextTierComplexity - currentComplexity);
395
396                 // Some tests may be unable to go beyond a certain capacity. If so, don't keep moving up tiers
397                 if (stage.complexity() - currentComplexity > 0 || nextTierComplexity == 1) {
398                     this._tierStartTimestamp = timestamp;
399                     this.mark("Complexity: " + nextTierComplexity, timestamp);
400                     return;
401                 }
402             } else if (timestamp < this._tierStartTimestamp + this.tierSlowTestLength)
403                 return;
404
405             this._finishedTierSampling = true;
406             this.isFrameLengthEstimatorEnabled = false;
407
408             // Extend the test length so that the full test length is made of the ramps
409             this._endTimestamp += timestamp;
410             this.mark(Strings.json.samplingStartTimeOffset, timestamp);
411
412             this._minimumComplexity = 0;
413             this._possibleMinimumComplexity = this._minimumComplexity;
414             this._minimumComplexityEstimator.sample(this._minimumComplexity);
415
416             // Sometimes this last tier will drop the frame length well below the threshold
417             // Avoid going down that far since it means fewer measurements are taken in the 60 fps area
418             // Interpolate a maximum complexity that gets us around the lowest threshold
419             if (this._lastTierComplexity != currentComplexity)
420                 this._maximumComplexity = Math.floor(Utilities.lerp(Utilities.progressValue(this.frameLengthSlowestThreshold, this._lastTierFrameLength, currentFrameLength), this._lastTierComplexity, currentComplexity));
421             else {
422                 // If the browser is capable of handling the most complex version of the test, use that
423                 this._maximumComplexity = currentComplexity;
424             }
425             this._possibleMaximumComplexity = this._maximumComplexity;
426
427             // If we get ourselves onto a ramp where the maximum complexity does not yield slow enough FPS,
428             // We'll use this as a boundary to find a higher maximum complexity for the next ramp
429             this._lastTierComplexity = currentComplexity;
430             this._lastTierFrameLength = currentFrameLength;
431
432             // First ramp
433             stage.tune(this._maximumComplexity - currentComplexity);
434             this._rampDidWarmup = false;
435             // Start timestamp represents start of ramp iteration and warm up
436             this._rampStartTimestamp = timestamp;
437             return;
438         }
439
440         if ((timestamp - this._rampStartTimestamp) < this.rampWarmupLength)
441             return;
442
443         if (this._rampDidWarmup)
444             return;
445
446         this._rampDidWarmup = true;
447         this._currentRampLength = this._rampStartTimestamp + this._rampLength - timestamp;
448         // Start timestamp represents start of ramp down, after warm up
449         this._rampStartTimestamp = timestamp;
450         this._rampStartIndex = this._sampler.sampleCount;
451     },
452
453     tune: function(timestamp, stage, lastFrameLength, didFinishInterval, intervalAverageFrameLength)
454     {
455         if (!this._rampDidWarmup)
456             return;
457
458         this._intervalFrameLengthEstimator.sample(lastFrameLength);
459         if (!didFinishInterval)
460             return;
461
462         var currentComplexity = stage.complexity();
463         var intervalFrameLengthMean = this._intervalFrameLengthEstimator.mean();
464         var intervalFrameLengthStandardDeviation = this._intervalFrameLengthEstimator.standardDeviation();
465
466         if (intervalFrameLengthMean < this.frameLengthDesiredThreshold && this._intervalFrameLengthEstimator.cdf(this.frameLengthDesiredThreshold) > .95) {
467             this._possibleMinimumComplexity = Math.max(this._possibleMinimumComplexity, currentComplexity);
468         } else if (intervalFrameLengthStandardDeviation > 2) {
469             // In the case where we might have found a previous interval where 60fps was reached. We hit a significant blip,
470             // so we should resample this area in the next ramp.
471             this._possibleMinimumComplexity = 0;
472         }
473         if (intervalFrameLengthMean - intervalFrameLengthStandardDeviation > this.frameLengthRampLowerThreshold)
474             this._possibleMaximumComplexity = Math.min(this._possibleMaximumComplexity, currentComplexity);
475         this._intervalFrameLengthEstimator.reset();
476
477         var progress = (timestamp - this._rampStartTimestamp) / this._currentRampLength;
478
479         if (progress < 1) {
480             stage.tune(Math.floor(Utilities.lerp(progress, this._maximumComplexity, this._minimumComplexity)) - currentComplexity);
481             return;
482         }
483
484         var regression = new Regression(this._sampler.samples, this._getComplexity, this._getFrameLength,
485             this._sampler.sampleCount - 1, this._rampStartIndex, this.frameLengthDesired);
486         this._rampRegressions.push(regression);
487
488         var interpolatedFrameLength = regression.valueAt(this._maximumComplexity);
489         if (interpolatedFrameLength < this.frameLengthRampLowerThreshold)
490             this._possibleMaximumComplexity = Math.floor(Utilities.lerp(Utilities.progressValue(this.frameLengthRampLowerThreshold, interpolatedFrameLength, this._lastTierFrameLength), this._maximumComplexity, this._lastTierComplexity));
491
492         interpolatedFrameLength = regression.valueAt(this._minimumComplexity);
493         this._minimumComplexityEstimator.sample(this._possibleMinimumComplexity);
494
495         this._changePointEstimator.sample(regression.complexity);
496
497         this._minimumComplexity = Math.round(this._minimumComplexityEstimator.mean());
498         this._maximumComplexity = Math.round(this._minimumComplexity +
499             Math.max(5,
500                 this._possibleMaximumComplexity - this._minimumComplexity,
501                 (this._changePointEstimator.mean() - this._minimumComplexity) * 2));
502
503         // Next ramp
504         stage.tune(this._maximumComplexity - stage.complexity());
505         this._rampDidWarmup = false;
506         // Start timestamp represents start of ramp iteration and warm up
507         this._rampStartTimestamp = timestamp;
508         this._possibleMinimumComplexity = 0;
509         this._possibleMaximumComplexity = this._maximumComplexity;
510     },
511
512     _getComplexity: function(samples, i) {
513         return samples[1][i];
514     },
515
516     _getFrameLength: function(samples, i) {
517         return samples[0][i] - samples[0][i - 1];
518     },
519
520     processSamples: function(results)
521     {
522         Controller.prototype.processSamples.call(this, results);
523
524         // Have samplingTimeOffset represent time 0
525         var startTimestamp = this._marks[Strings.json.samplingStartTimeOffset].time;
526
527         for (var markName in results[Strings.json.marks]) {
528             results[Strings.json.marks][markName].time -= startTimestamp;
529         }
530
531         var timeSamples = results[Strings.json.samples][Strings.json.controller];
532         timeSamples.forEach(function(timeSample) {
533             timeSample.time -= startTimestamp;
534         });
535
536         // Aggregate all of the ramps into one big complexity-frameLength dataset
537         var complexitySamples = [], complexityAverageSamples = [];
538         results[Strings.json.samples][Strings.json.complexity] = complexitySamples;
539         results[Strings.json.samples][Strings.json.complexityAverage] = complexityAverageSamples;
540
541         results[Strings.json.controller] = [];
542         this._rampRegressions.forEach(function(ramp) {
543             var startIndex = ramp.startIndex, endIndex = ramp.endIndex;
544             var startTime = timeSamples[startIndex].time, endTime = timeSamples[endIndex].time;
545             var startComplexity = timeSamples[startIndex].complexity, endComplexity = timeSamples[endIndex].complexity;
546
547             var regression = {};
548             results[Strings.json.controller].push(regression);
549
550             var percentage = (ramp.complexity - startComplexity) / (endComplexity - startComplexity);
551             var inflectionTime = startTime + percentage * (endTime - startTime);
552
553             regression[Strings.json.regressions.segment1] = [
554                 [startTime, ramp.s2 + ramp.t2 * startComplexity],
555                 [inflectionTime, ramp.s2 + ramp.t2 * ramp.complexity]
556             ];
557             regression[Strings.json.regressions.segment2] = [
558                 [inflectionTime, ramp.s1 + ramp.t1 * ramp.complexity],
559                 [endTime, ramp.s1 + ramp.t1 * endComplexity]
560             ];
561             regression[Strings.json.complexity] = ramp.complexity;
562             regression[Strings.json.regressions.startIndex] = startIndex;
563             regression[Strings.json.regressions.endIndex] = endIndex;
564
565             for (var j = startIndex; j <= endIndex; ++j)
566                 complexitySamples.push(timeSamples[j]);
567         });
568         this._processComplexitySamples(complexitySamples, complexityAverageSamples);
569     }
570 });
571
572 Ramp30Controller = Utilities.createSubclass(RampController,
573     function(benchmark, options)
574     {
575         RampController.call(this, benchmark, options);
576     }, {
577
578     frameLengthDesired: 1000/30,
579     frameLengthDesiredThreshold: 1000/29,
580     frameLengthSlowestThreshold: 1000/20,
581     frameLengthRampLowerThreshold: 1000/20
582 });
583
584 Stage = Utilities.createClass(
585     function()
586     {
587     }, {
588
589     initialize: function(benchmark)
590     {
591         this._benchmark = benchmark;
592         this._element = document.getElementById("stage");
593         this._element.setAttribute("width", document.body.offsetWidth);
594         this._element.setAttribute("height", document.body.offsetHeight);
595         this._size = Point.elementClientSize(this._element).subtract(Insets.elementPadding(this._element).size);
596     },
597
598     get element()
599     {
600         return this._element;
601     },
602
603     get size()
604     {
605         return this._size;
606     },
607
608     complexity: function()
609     {
610         return 0;
611     },
612
613     tune: function()
614     {
615         throw "Not implemented";
616     },
617
618     animate: function()
619     {
620         throw "Not implemented";
621     },
622
623     clear: function()
624     {
625         return this.tune(-this.tune(0));
626     }
627 });
628
629 Utilities.extendObject(Stage, {
630     random: function(min, max)
631     {
632         return (Pseudo.random() * (max - min)) + min;
633     },
634
635     randomBool: function()
636     {
637         return !!Math.round(Pseudo.random());
638     },
639
640     randomSign: function()
641     {
642         return Pseudo.random() >= .5 ? 1 : -1;
643     },
644
645     randomInt: function(min, max)
646     {
647         return Math.floor(this.random(min, max + 1));
648     },
649
650     randomPosition: function(maxPosition)
651     {
652         return new Point(this.randomInt(0, maxPosition.x), this.randomInt(0, maxPosition.y));
653     },
654
655     randomSquareSize: function(min, max)
656     {
657         var side = this.random(min, max);
658         return new Point(side, side);
659     },
660
661     randomVelocity: function(maxVelocity)
662     {
663         return this.random(maxVelocity / 8, maxVelocity);
664     },
665
666     randomAngle: function()
667     {
668         return this.random(0, Math.PI * 2);
669     },
670
671     randomColor: function()
672     {
673         var min = 32;
674         var max = 256 - 32;
675         return "#"
676             + this.randomInt(min, max).toString(16)
677             + this.randomInt(min, max).toString(16)
678             + this.randomInt(min, max).toString(16);
679     },
680
681     randomStyleMixBlendMode: function()
682     {
683         var mixBlendModeList = [
684           'normal',
685           'multiply',
686           'screen',
687           'overlay',
688           'darken',
689           'lighten',
690           'color-dodge',
691           'color-burn',
692           'hard-light',
693           'soft-light',
694           'difference',
695           'exclusion',
696           'hue',
697           'saturation',
698           'color',
699           'luminosity'
700         ];
701
702         return mixBlendModeList[this.randomInt(0, mixBlendModeList.length)];
703     },
704
705     randomStyleFilter: function()
706     {
707         var filterList = [
708             'grayscale(50%)',
709             'sepia(50%)',
710             'saturate(50%)',
711             'hue-rotate(180)',
712             'invert(50%)',
713             'opacity(50%)',
714             'brightness(50%)',
715             'contrast(50%)',
716             'blur(10px)',
717             'drop-shadow(10px 10px 10px gray)'
718         ];
719
720         return filterList[this.randomInt(0, filterList.length)];
721     },
722
723     rotatingColor: function(cycleLengthMs, saturation, lightness)
724     {
725         return "hsl("
726             + Stage.dateFractionalValue(cycleLengthMs) * 360 + ", "
727             + ((saturation || .8) * 100).toFixed(0) + "%, "
728             + ((lightness || .35) * 100).toFixed(0) + "%)";
729     },
730
731     // Returns a fractional value that wraps around within [0,1]
732     dateFractionalValue: function(cycleLengthMs)
733     {
734         return (Date.now() / (cycleLengthMs || 2000)) % 1;
735     },
736
737     // Returns an increasing value slowed down by factor
738     dateCounterValue: function(factor)
739     {
740         return Date.now() / factor;
741     },
742
743     randomRotater: function()
744     {
745         return new Rotater(this.random(1000, 10000));
746     }
747 });
748
749 Rotater = Utilities.createClass(
750     function(rotateInterval)
751     {
752         this._timeDelta = 0;
753         this._rotateInterval = rotateInterval;
754         this._isSampling = false;
755     }, {
756
757     get interval()
758     {
759         return this._rotateInterval;
760     },
761
762     next: function(timeDelta)
763     {
764         this._timeDelta = (this._timeDelta + timeDelta) % this._rotateInterval;
765     },
766
767     degree: function()
768     {
769         return (360 * this._timeDelta) / this._rotateInterval;
770     },
771
772     rotateZ: function()
773     {
774         return "rotateZ(" + Math.floor(this.degree()) + "deg)";
775     },
776
777     rotate: function(center)
778     {
779         return "rotate(" + Math.floor(this.degree()) + ", " + center.x + "," + center.y + ")";
780     }
781 });
782
783 Benchmark = Utilities.createClass(
784     function(stage, options)
785     {
786         this._animateLoop = this._animateLoop.bind(this);
787
788         this._stage = stage;
789         this._stage.initialize(this, options);
790
791         switch (options["time-measurement"])
792         {
793         case "performance":
794             this._getTimestamp = performance.now.bind(performance);
795             break;
796         case "date":
797             this._getTimestamp = Date.now;
798             break;
799         }
800
801         options["test-interval"] *= 1000;
802         switch (options["controller"])
803         {
804         case "fixed":
805             this._controller = new FixedController(this, options);
806             break;
807         case "step":
808             this._controller = new StepController(this, options);
809             break;
810         case "adaptive":
811             this._controller = new AdaptiveController(this, options);
812             break;
813         case "ramp":
814             this._controller = new RampController(this, options);
815             break;
816         case "ramp30":
817             this._controller = new Ramp30Controller(this, options);
818         }
819     }, {
820
821     get stage()
822     {
823         return this._stage;
824     },
825
826     get timestamp()
827     {
828         return this._currentTimestamp - this._startTimestamp;
829     },
830
831     backgroundColor: function()
832     {
833         var stage = window.getComputedStyle(document.getElementById("stage"));
834         return stage["background-color"];
835     },
836
837     run: function()
838     {
839         return this.waitUntilReady().then(function() {
840             this._finishPromise = new SimplePromise;
841             this._previousTimestamp = this._getTimestamp();
842             this._didWarmUp = false;
843             this._stage.tune(this._controller.initialComplexity - this._stage.complexity());
844             this._animateLoop();
845             return this._finishPromise;
846         }.bind(this));
847     },
848
849     // Subclasses should override this if they have setup to do prior to commencing.
850     waitUntilReady: function()
851     {
852         var promise = new SimplePromise;
853         promise.resolve();
854         return promise;
855     },
856
857     _animateLoop: function()
858     {
859         this._currentTimestamp = this._getTimestamp();
860
861         if (this._controller.shouldStop(this._currentTimestamp)) {
862             this._finishPromise.resolve(this._controller.results());
863             return;
864         }
865
866         if (!this._didWarmUp) {
867             if (this._currentTimestamp - this._previousTimestamp >= 100) {
868                 this._didWarmUp = true;
869                 this._startTimestamp = this._currentTimestamp;
870                 this._controller.start(this._currentTimestamp, this._stage);
871                 this._previousTimestamp = this._currentTimestamp;
872             }
873
874             this._stage.animate(0);
875             requestAnimationFrame(this._animateLoop);
876             return;
877         }
878
879         this._controller.update(this._currentTimestamp, this._stage);
880         this._stage.animate(this._currentTimestamp - this._previousTimestamp);
881         this._previousTimestamp = this._currentTimestamp;
882         requestAnimationFrame(this._animateLoop);
883     }
884 });