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