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