blob: 23c864102c687fc416db5724d4122b8a81654637 [file] [log] [blame]
/*
* Copyright (C) 2015-2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
Sampler = Utilities.createClass(
function(seriesCount, expectedSampleCount, processor)
{
this._processor = processor;
this.samples = [];
for (var i = 0; i < seriesCount; ++i) {
var array = new Array(expectedSampleCount);
array.fill(0);
this.samples[i] = array;
}
this.sampleCount = 0;
}, {
record: function() {
// Assume that arguments.length == this.samples.length
for (var i = 0; i < arguments.length; i++) {
this.samples[i][this.sampleCount] = arguments[i];
}
++this.sampleCount;
},
processSamples: function()
{
var results = {};
// Remove unused capacity
this.samples = this.samples.map(function(array) {
return array.slice(0, this.sampleCount);
}, this);
this._processor.processSamples(results);
return results;
}
});
Controller = Utilities.createClass(
function(benchmark, options)
{
// Initialize timestamps relative to the start of the benchmark
// In start() the timestamps are offset by the start timestamp
this._startTimestamp = 0;
this._endTimestamp = options["test-interval"];
// Default data series: timestamp, complexity, estimatedFrameLength
var sampleSize = options["sample-capacity"] || (60 * options["test-interval"] / 1000);
this._sampler = new Sampler(options["series-count"] || 3, sampleSize, this);
this._marks = {};
this._frameLengthEstimator = new SimpleKalmanEstimator(options["kalman-process-error"], options["kalman-measurement-error"]);
this._isFrameLengthEstimatorEnabled = true;
// Length of subsequent intervals; a value of 0 means use no intervals
this.intervalSamplingLength = 100;
this.initialComplexity = 1;
}, {
set isFrameLengthEstimatorEnabled(enabled) {
this._isFrameLengthEstimatorEnabled = enabled;
},
start: function(startTimestamp, stage)
{
this._startTimestamp = startTimestamp;
this._endTimestamp += startTimestamp;
this._previousTimestamp = startTimestamp;
this._measureAndResetInterval(startTimestamp);
this.recordFirstSample(startTimestamp, stage);
},
recordFirstSample: function(startTimestamp, stage)
{
this._sampler.record(startTimestamp, stage.complexity(), -1);
this.mark(Strings.json.samplingStartTimeOffset, startTimestamp);
},
mark: function(comment, timestamp, data) {
data = data || {};
data.time = timestamp;
data.index = this._sampler.sampleCount;
this._marks[comment] = data;
},
containsMark: function(comment) {
return comment in this._marks;
},
_measureAndResetInterval: function(currentTimestamp)
{
var sampleCount = this._sampler.sampleCount;
var averageFrameLength = 0;
if (this._intervalEndTimestamp) {
var intervalStartTimestamp = this._sampler.samples[0][this._intervalStartIndex];
averageFrameLength = (currentTimestamp - intervalStartTimestamp) / (sampleCount - this._intervalStartIndex);
}
this._intervalStartIndex = sampleCount;
this._intervalEndTimestamp = currentTimestamp + this.intervalSamplingLength;
return averageFrameLength;
},
update: function(timestamp, stage)
{
var lastFrameLength = timestamp - this._previousTimestamp;
this._previousTimestamp = timestamp;
var frameLengthEstimate = -1, intervalAverageFrameLength = -1;
var didFinishInterval = false;
if (!this.intervalSamplingLength) {
if (this._isFrameLengthEstimatorEnabled) {
this._frameLengthEstimator.sample(lastFrameLength);
frameLengthEstimate = this._frameLengthEstimator.estimate;
}
} else if (timestamp >= this._intervalEndTimestamp) {
var intervalStartTimestamp = this._sampler.samples[0][this._intervalStartIndex];
intervalAverageFrameLength = this._measureAndResetInterval(timestamp);
if (this._isFrameLengthEstimatorEnabled) {
this._frameLengthEstimator.sample(intervalAverageFrameLength);
frameLengthEstimate = this._frameLengthEstimator.estimate;
}
didFinishInterval = true;
this.didFinishInterval(timestamp, stage, intervalAverageFrameLength);
}
this._sampler.record(timestamp, stage.complexity(), frameLengthEstimate);
this.tune(timestamp, stage, lastFrameLength, didFinishInterval, intervalAverageFrameLength);
},
didFinishInterval: function(timestamp, stage, intervalAverageFrameLength)
{
},
tune: function(timestamp, stage, lastFrameLength, didFinishInterval, intervalAverageFrameLength)
{
},
shouldStop: function(timestamp)
{
return timestamp > this._endTimestamp;
},
results: function()
{
return this._sampler.processSamples();
},
_processComplexitySamples: function(complexitySamples, complexityAverageSamples)
{
complexityAverageSamples.addField(Strings.json.complexity, 0);
complexityAverageSamples.addField(Strings.json.frameLength, 1);
complexityAverageSamples.addField(Strings.json.measurements.stdev, 2);
complexitySamples.sort(function(a, b) {
return complexitySamples.getFieldInDatum(a, Strings.json.complexity) - complexitySamples.getFieldInDatum(b, Strings.json.complexity);
});
// Samples averaged based on complexity
var currentComplexity = -1;
var experimentAtComplexity;
function addSample() {
var mean = experimentAtComplexity.mean();
var stdev = experimentAtComplexity.standardDeviation();
var averageSample = complexityAverageSamples.createDatum();
complexityAverageSamples.push(averageSample);
complexityAverageSamples.setFieldInDatum(averageSample, Strings.json.complexity, currentComplexity);
complexityAverageSamples.setFieldInDatum(averageSample, Strings.json.frameLength, mean);
complexityAverageSamples.setFieldInDatum(averageSample, Strings.json.measurements.stdev, stdev);
}
complexitySamples.forEach(function(sample) {
var sampleComplexity = complexitySamples.getFieldInDatum(sample, Strings.json.complexity);
if (sampleComplexity != currentComplexity) {
if (currentComplexity > -1)
addSample();
currentComplexity = sampleComplexity;
experimentAtComplexity = new Experiment;
}
experimentAtComplexity.sample(complexitySamples.getFieldInDatum(sample, Strings.json.frameLength));
});
// Finish off the last one
addSample();
},
processSamples: function(results)
{
var complexityExperiment = new Experiment;
var smoothedFrameLengthExperiment = new Experiment;
var samples = this._sampler.samples;
for (var markName in this._marks)
this._marks[markName].time -= this._startTimestamp;
results[Strings.json.marks] = this._marks;
results[Strings.json.samples] = {};
var controllerSamples = new SampleData;
results[Strings.json.samples][Strings.json.controller] = controllerSamples;
controllerSamples.addField(Strings.json.time, 0);
controllerSamples.addField(Strings.json.complexity, 1);
controllerSamples.addField(Strings.json.frameLength, 2);
controllerSamples.addField(Strings.json.smoothedFrameLength, 3);
var complexitySamples = new SampleData(controllerSamples.fieldMap);
results[Strings.json.samples][Strings.json.complexity] = complexitySamples;
samples[0].forEach(function(timestamp, i) {
var sample = controllerSamples.createDatum();
controllerSamples.push(sample);
complexitySamples.push(sample);
// Represent time in milliseconds
controllerSamples.setFieldInDatum(sample, Strings.json.time, timestamp - this._startTimestamp);
controllerSamples.setFieldInDatum(sample, Strings.json.complexity, samples[1][i]);
if (i == 0)
controllerSamples.setFieldInDatum(sample, Strings.json.frameLength, 1000/60);
else
controllerSamples.setFieldInDatum(sample, Strings.json.frameLength, timestamp - samples[0][i - 1]);
if (samples[2][i] != -1)
controllerSamples.setFieldInDatum(sample, Strings.json.smoothedFrameLength, samples[2][i]);
}, this);
var complexityAverageSamples = new SampleData;
results[Strings.json.samples][Strings.json.complexityAverage] = complexityAverageSamples;
this._processComplexitySamples(complexitySamples, complexityAverageSamples);
}
});
FixedController = Utilities.createSubclass(Controller,
function(benchmark, options)
{
Controller.call(this, benchmark, options);
this.initialComplexity = options["complexity"];
this.intervalSamplingLength = 0;
}
);
StepController = Utilities.createSubclass(Controller,
function(benchmark, options)
{
Controller.call(this, benchmark, options);
this.initialComplexity = options["complexity"];
this.intervalSamplingLength = 0;
this._stepped = false;
this._stepTime = options["test-interval"] / 2;
}, {
start: function(startTimestamp, stage)
{
Controller.prototype.start.call(this, startTimestamp, stage);
this._stepTime += startTimestamp;
},
tune: function(timestamp, stage)
{
if (this._stepped || timestamp < this._stepTime)
return;
this.mark(Strings.json.samplingEndTimeOffset, timestamp);
this._stepped = true;
stage.tune(stage.complexity() * 3);
}
});
AdaptiveController = Utilities.createSubclass(Controller,
function(benchmark, options)
{
// Data series: timestamp, complexity, estimatedIntervalFrameLength
Controller.call(this, benchmark, options);
// All tests start at 0, so we expect to see 60 fps quickly.
this._samplingTimestamp = options["test-interval"] / 2;
this._startedSampling = false;
this._targetFrameRate = options["frame-rate"];
this._pid = new PIDController(this._targetFrameRate);
this._intervalFrameCount = 0;
this._numberOfFramesToMeasurePerInterval = 4;
}, {
start: function(startTimestamp, stage)
{
Controller.prototype.start.call(this, startTimestamp, stage);
this._samplingTimestamp += startTimestamp;
this._intervalTimestamp = startTimestamp;
},
recordFirstSample: function(startTimestamp, stage)
{
this._sampler.record(startTimestamp, stage.complexity(), -1);
},
update: function(timestamp, stage)
{
if (!this._startedSampling && timestamp >= this._samplingTimestamp) {
this._startedSampling = true;
this.mark(Strings.json.samplingStartTimeOffset, this._samplingTimestamp);
}
// Start the work for the next frame.
++this._intervalFrameCount;
if (this._intervalFrameCount < this._numberOfFramesToMeasurePerInterval) {
this._sampler.record(timestamp, stage.complexity(), -1);
return;
}
// Adjust the test to reach the desired FPS.
var intervalLength = timestamp - this._intervalTimestamp;
this._frameLengthEstimator.sample(intervalLength / this._numberOfFramesToMeasurePerInterval);
var intervalEstimatedFrameRate = 1000 / this._frameLengthEstimator.estimate;
var tuneValue = -this._pid.tune(timestamp - this._startTimestamp, intervalLength, intervalEstimatedFrameRate);
tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
stage.tune(tuneValue);
this._sampler.record(timestamp, stage.complexity(), this._frameLengthEstimator.estimate);
// Start the next interval.
this._intervalFrameCount = 0;
this._intervalTimestamp = timestamp;
}
});
RampController = Utilities.createSubclass(Controller,
function(benchmark, options)
{
// The tier warmup takes at most 5 seconds
options["sample-capacity"] = (options["test-interval"] / 1000 + 5) * 60;
Controller.call(this, benchmark, options);
// Initially start with a tier test to find the bounds
// The number of objects in a tier test is 10^|_tier|
this._tier = -.5;
// The timestamp is first set after the first interval completes
this._tierStartTimestamp = 0;
this._minimumComplexity = 1;
this._maximumComplexity = 1;
// After the tier range is determined, figure out the number of ramp iterations
var minimumRampLength = 3000;
var totalRampIterations = Math.max(1, Math.floor(this._endTimestamp / minimumRampLength));
// Give a little extra room to run since the ramps won't be exactly this length
this._rampLength = Math.floor((this._endTimestamp - totalRampIterations * this.intervalSamplingLength) / totalRampIterations);
this._rampDidWarmup = false;
this._rampRegressions = [];
this._finishedTierSampling = false;
this._changePointEstimator = new Experiment;
this._minimumComplexityEstimator = new Experiment;
// Estimates all frames within an interval
this._intervalFrameLengthEstimator = new Experiment;
}, {
// If the engine can handle the tier's complexity at the desired frame rate, test for a short
// period, then move on to the next tier
tierFastTestLength: 250,
// If the engine is under stress, let the test run a little longer to let the measurement settle
tierSlowTestLength: 750,
rampWarmupLength: 200,
// Used for regression calculations in the ramps
frameLengthDesired: 1000/60,
// Add some tolerance; frame lengths shorter than this are considered to be @ the desired frame length
frameLengthDesiredThreshold: 1000/58,
// During tier sampling get at least this slow to find the right complexity range
frameLengthTierThreshold: 1000/30,
// Try to make each ramp get this slow so that we can cross the break point
frameLengthRampLowerThreshold: 1000/45,
// Do not let the regression calculation at the maximum complexity of a ramp get slower than this threshold
frameLengthRampUpperThreshold: 1000/20,
start: function(startTimestamp, stage)
{
Controller.prototype.start.call(this, startTimestamp, stage);
this._rampStartTimestamp = 0;
this.intervalSamplingLength = 100;
},
didFinishInterval: function(timestamp, stage, intervalAverageFrameLength)
{
if (!this._finishedTierSampling) {
if (this._tierStartTimestamp > 0 && timestamp < this._tierStartTimestamp + this.tierFastTestLength)
return;
var currentComplexity = stage.complexity();
var currentFrameLength = this._frameLengthEstimator.estimate;
if (currentFrameLength < this.frameLengthTierThreshold) {
var isAnimatingAt60FPS = currentFrameLength < this.frameLengthDesiredThreshold;
var hasFinishedSlowTierTest = timestamp > this._tierStartTimestamp + this.tierSlowTestLength;
if (!isAnimatingAt60FPS && !hasFinishedSlowTierTest)
return;
// We're measuring at 60 fps, so quickly move on to the next tier, or
// we've slower than 60 fps, but we've let this tier run long enough to
// get an estimate
this._lastTierComplexity = currentComplexity;
this._lastTierFrameLength = currentFrameLength;
this._tier += .5;
var nextTierComplexity = Math.round(Math.pow(10, this._tier));
stage.tune(nextTierComplexity - currentComplexity);
// Some tests may be unable to go beyond a certain capacity. If so, don't keep moving up tiers
if (stage.complexity() - currentComplexity > 0 || nextTierComplexity == 1) {
this._tierStartTimestamp = timestamp;
this.mark("Complexity: " + nextTierComplexity, timestamp);
return;
}
} else if (timestamp < this._tierStartTimestamp + this.tierSlowTestLength)
return;
this._finishedTierSampling = true;
this.isFrameLengthEstimatorEnabled = false;
this.intervalSamplingLength = 120;
// Extend the test length so that the full test length is made of the ramps
this._endTimestamp += timestamp;
this.mark(Strings.json.samplingStartTimeOffset, timestamp);
this._minimumComplexity = 1;
this._possibleMinimumComplexity = this._minimumComplexity;
this._minimumComplexityEstimator.sample(this._minimumComplexity);
// Sometimes this last tier will drop the frame length well below the threshold.
// Avoid going down that far since it means fewer measurements are taken in the 60 fps area.
// Interpolate a maximum complexity that gets us around the lowest threshold.
// Avoid doing this calculation if we never get out of the first tier (where this._lastTierComplexity is undefined).
if (this._lastTierComplexity && this._lastTierComplexity != currentComplexity)
this._maximumComplexity = Math.floor(Utilities.lerp(Utilities.progressValue(this.frameLengthTierThreshold, this._lastTierFrameLength, currentFrameLength), this._lastTierComplexity, currentComplexity));
else {
// If the browser is capable of handling the most complex version of the test, use that
this._maximumComplexity = currentComplexity;
}
this._possibleMaximumComplexity = this._maximumComplexity;
// If we get ourselves onto a ramp where the maximum complexity does not yield slow enough FPS,
// We'll use this as a boundary to find a higher maximum complexity for the next ramp
this._lastTierComplexity = currentComplexity;
this._lastTierFrameLength = currentFrameLength;
// First ramp
stage.tune(this._maximumComplexity - currentComplexity);
this._rampDidWarmup = false;
// Start timestamp represents start of ramp iteration and warm up
this._rampStartTimestamp = timestamp;
return;
}
if ((timestamp - this._rampStartTimestamp) < this.rampWarmupLength)
return;
if (this._rampDidWarmup)
return;
this._rampDidWarmup = true;
this._currentRampLength = this._rampStartTimestamp + this._rampLength - timestamp;
// Start timestamp represents start of ramp down, after warm up
this._rampStartTimestamp = timestamp;
this._rampStartIndex = this._sampler.sampleCount;
},
tune: function(timestamp, stage, lastFrameLength, didFinishInterval, intervalAverageFrameLength)
{
if (!this._rampDidWarmup)
return;
this._intervalFrameLengthEstimator.sample(lastFrameLength);
if (!didFinishInterval)
return;
var currentComplexity = stage.complexity();
var intervalFrameLengthMean = this._intervalFrameLengthEstimator.mean();
var intervalFrameLengthStandardDeviation = this._intervalFrameLengthEstimator.standardDeviation();
if (intervalFrameLengthMean < this.frameLengthDesiredThreshold && this._intervalFrameLengthEstimator.cdf(this.frameLengthDesiredThreshold) > .9) {
this._possibleMinimumComplexity = Math.max(this._possibleMinimumComplexity, currentComplexity);
} else if (intervalFrameLengthStandardDeviation > 2) {
// In the case where we might have found a previous interval where 60fps was reached. We hit a significant blip,
// so we should resample this area in the next ramp.
this._possibleMinimumComplexity = 1;
}
if (intervalFrameLengthMean - intervalFrameLengthStandardDeviation > this.frameLengthRampLowerThreshold)
this._possibleMaximumComplexity = Math.min(this._possibleMaximumComplexity, currentComplexity);
this._intervalFrameLengthEstimator.reset();
var progress = (timestamp - this._rampStartTimestamp) / this._currentRampLength;
if (progress < 1) {
// Reframe progress percentage so that the last interval of the ramp can sample at minimum complexity
progress = (timestamp - this._rampStartTimestamp) / (this._currentRampLength - this.intervalSamplingLength);
stage.tune(Math.max(this._minimumComplexity, Math.floor(Utilities.lerp(progress, this._maximumComplexity, this._minimumComplexity))) - currentComplexity);
return;
}
var regression = new Regression(this._sampler.samples, this._getComplexity, this._getFrameLength,
this._sampler.sampleCount - 1, this._rampStartIndex, { desiredFrameLength: this.frameLengthDesired });
this._rampRegressions.push(regression);
var frameLengthAtMaxComplexity = regression.valueAt(this._maximumComplexity);
if (frameLengthAtMaxComplexity < this.frameLengthRampLowerThreshold)
this._possibleMaximumComplexity = Math.floor(Utilities.lerp(Utilities.progressValue(this.frameLengthRampLowerThreshold, frameLengthAtMaxComplexity, this._lastTierFrameLength), this._maximumComplexity, this._lastTierComplexity));
// If the regression doesn't fit the first segment at all, keep the minimum bound at 1
if ((timestamp - this._sampler.samples[0][this._sampler.sampleCount - regression.n1]) / this._currentRampLength < .25)
this._possibleMinimumComplexity = 1;
this._minimumComplexityEstimator.sample(this._possibleMinimumComplexity);
this._minimumComplexity = Math.round(this._minimumComplexityEstimator.mean());
if (frameLengthAtMaxComplexity < this.frameLengthRampUpperThreshold) {
this._changePointEstimator.sample(regression.complexity);
// 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
// range along the complexity (x) axis for a good regression calculation to be made, so force at least a range of 5
// particles. Make it possible to increase the maximum complexity in case unexpected noise caps the regression too low.
this._maximumComplexity = Math.round(this._minimumComplexity +
Math.max(5,
this._possibleMaximumComplexity - this._minimumComplexity,
(this._changePointEstimator.mean() - this._minimumComplexity) * 2));
} else {
// The slowest samples weighed the regression too heavily
this._maximumComplexity = Math.max(Math.round(.8 * this._maximumComplexity), this._minimumComplexity + 5);
}
// Next ramp
stage.tune(this._maximumComplexity - stage.complexity());
this._rampDidWarmup = false;
// Start timestamp represents start of ramp iteration and warm up
this._rampStartTimestamp = timestamp;
this._possibleMinimumComplexity = 1;
this._possibleMaximumComplexity = this._maximumComplexity;
},
_getComplexity: function(samples, i) {
return samples[1][i];
},
_getFrameLength: function(samples, i) {
return samples[0][i] - samples[0][i - 1];
},
processSamples: function(results)
{
Controller.prototype.processSamples.call(this, results);
// Have samplingTimeOffset represent time 0
var startTimestamp = this._marks[Strings.json.samplingStartTimeOffset].time;
for (var markName in results[Strings.json.marks]) {
results[Strings.json.marks][markName].time -= startTimestamp;
}
var controllerSamples = results[Strings.json.samples][Strings.json.controller];
controllerSamples.forEach(function(timeSample) {
controllerSamples.setFieldInDatum(timeSample, Strings.json.time, controllerSamples.getFieldInDatum(timeSample, Strings.json.time) - startTimestamp);
});
// Aggregate all of the ramps into one big complexity-frameLength dataset
var complexitySamples = new SampleData(controllerSamples.fieldMap);
results[Strings.json.samples][Strings.json.complexity] = complexitySamples;
results[Strings.json.controller] = [];
this._rampRegressions.forEach(function(ramp) {
var startIndex = ramp.startIndex, endIndex = ramp.endIndex;
var startTime = controllerSamples.getFieldInDatum(startIndex, Strings.json.time);
var endTime = controllerSamples.getFieldInDatum(endIndex, Strings.json.time);
var startComplexity = controllerSamples.getFieldInDatum(startIndex, Strings.json.complexity);
var endComplexity = controllerSamples.getFieldInDatum(endIndex, Strings.json.complexity);
var regression = {};
results[Strings.json.controller].push(regression);
var percentage = (ramp.complexity - startComplexity) / (endComplexity - startComplexity);
var inflectionTime = startTime + percentage * (endTime - startTime);
regression[Strings.json.regressions.segment1] = [
[startTime, ramp.s2 + ramp.t2 * startComplexity],
[inflectionTime, ramp.s2 + ramp.t2 * ramp.complexity]
];
regression[Strings.json.regressions.segment2] = [
[inflectionTime, ramp.s1 + ramp.t1 * ramp.complexity],
[endTime, ramp.s1 + ramp.t1 * endComplexity]
];
regression[Strings.json.complexity] = ramp.complexity;
regression[Strings.json.regressions.startIndex] = startIndex;
regression[Strings.json.regressions.endIndex] = endIndex;
regression[Strings.json.regressions.profile] = ramp.profile;
for (var j = startIndex; j <= endIndex; ++j)
complexitySamples.push(controllerSamples.at(j));
});
var complexityAverageSamples = new SampleData;
results[Strings.json.samples][Strings.json.complexityAverage] = complexityAverageSamples;
this._processComplexitySamples(complexitySamples, complexityAverageSamples);
}
});
Ramp30Controller = Utilities.createSubclass(RampController,
function(benchmark, options)
{
RampController.call(this, benchmark, options);
}, {
frameLengthDesired: 1000/30,
frameLengthDesiredThreshold: 1000/29,
frameLengthTierThreshold: 1000/20,
frameLengthRampLowerThreshold: 1000/20,
frameLengthRampUpperThreshold: 1000/12
});
Stage = Utilities.createClass(
function()
{
}, {
initialize: function(benchmark)
{
this._benchmark = benchmark;
this._element = document.getElementById("stage");
this._element.setAttribute("width", document.body.offsetWidth);
this._element.setAttribute("height", document.body.offsetHeight);
this._size = Point.elementClientSize(this._element).subtract(Insets.elementPadding(this._element).size);
},
get element()
{
return this._element;
},
get size()
{
return this._size;
},
complexity: function()
{
return 0;
},
tune: function()
{
throw "Not implemented";
},
animate: function()
{
throw "Not implemented";
},
clear: function()
{
return this.tune(-this.tune(0));
}
});
Utilities.extendObject(Stage, {
random: function(min, max)
{
return (Pseudo.random() * (max - min)) + min;
},
randomBool: function()
{
return !!Math.round(Pseudo.random());
},
randomSign: function()
{
return Pseudo.random() >= .5 ? 1 : -1;
},
randomInt: function(min, max)
{
return Math.floor(this.random(min, max + 1));
},
randomPosition: function(maxPosition)
{
return new Point(this.randomInt(0, maxPosition.x), this.randomInt(0, maxPosition.y));
},
randomSquareSize: function(min, max)
{
var side = this.random(min, max);
return new Point(side, side);
},
randomVelocity: function(maxVelocity)
{
return this.random(maxVelocity / 8, maxVelocity);
},
randomAngle: function()
{
return this.random(0, Math.PI * 2);
},
randomColor: function()
{
var min = 32;
var max = 256 - 32;
return "#"
+ this.randomInt(min, max).toString(16)
+ this.randomInt(min, max).toString(16)
+ this.randomInt(min, max).toString(16);
},
randomStyleMixBlendMode: function()
{
var mixBlendModeList = [
'normal',
'multiply',
'screen',
'overlay',
'darken',
'lighten',
'color-dodge',
'color-burn',
'hard-light',
'soft-light',
'difference',
'exclusion',
'hue',
'saturation',
'color',
'luminosity'
];
return mixBlendModeList[this.randomInt(0, mixBlendModeList.length)];
},
randomStyleFilter: function()
{
var filterList = [
'grayscale(50%)',
'sepia(50%)',
'saturate(50%)',
'hue-rotate(180)',
'invert(50%)',
'opacity(50%)',
'brightness(50%)',
'contrast(50%)',
'blur(10px)',
'drop-shadow(10px 10px 10px gray)'
];
return filterList[this.randomInt(0, filterList.length)];
},
randomElementInArray: function(array)
{
return array[Stage.randomInt(0, array.length - 1)];
},
rotatingColor: function(cycleLengthMs, saturation, lightness)
{
return "hsl("
+ Stage.dateFractionalValue(cycleLengthMs) * 360 + ", "
+ ((saturation || .8) * 100).toFixed(0) + "%, "
+ ((lightness || .35) * 100).toFixed(0) + "%)";
},
// Returns a fractional value that wraps around within [0,1]
dateFractionalValue: function(cycleLengthMs)
{
return (Date.now() / (cycleLengthMs || 2000)) % 1;
},
// Returns an increasing value slowed down by factor
dateCounterValue: function(factor)
{
return Date.now() / factor;
},
randomRotater: function()
{
return new Rotater(this.random(1000, 10000));
}
});
Rotater = Utilities.createClass(
function(rotateInterval)
{
this._timeDelta = 0;
this._rotateInterval = rotateInterval;
this._isSampling = false;
}, {
get interval()
{
return this._rotateInterval;
},
next: function(timeDelta)
{
this._timeDelta = (this._timeDelta + timeDelta) % this._rotateInterval;
},
degree: function()
{
return (360 * this._timeDelta) / this._rotateInterval;
},
rotateZ: function()
{
return "rotateZ(" + Math.floor(this.degree()) + "deg)";
},
rotate: function(center)
{
return "rotate(" + Math.floor(this.degree()) + ", " + center.x + "," + center.y + ")";
}
});
Benchmark = Utilities.createClass(
function(stage, options)
{
this._animateLoop = this._animateLoop.bind(this);
this._stage = stage;
this._stage.initialize(this, options);
switch (options["time-measurement"])
{
case "performance":
if (window.performance && window.performance.now)
this._getTimestamp = performance.now.bind(performance);
else
this._getTimestamp = null;
break;
case "raf":
this._getTimestamp = null;
break;
case "date":
this._getTimestamp = Date.now;
break;
}
options["test-interval"] *= 1000;
switch (options["controller"])
{
case "fixed":
this._controller = new FixedController(this, options);
break;
case "step":
this._controller = new StepController(this, options);
break;
case "adaptive":
this._controller = new AdaptiveController(this, options);
break;
case "ramp":
this._controller = new RampController(this, options);
break;
case "ramp30":
this._controller = new Ramp30Controller(this, options);
}
}, {
get stage()
{
return this._stage;
},
get timestamp()
{
return this._currentTimestamp - this._startTimestamp;
},
backgroundColor: function()
{
var stage = window.getComputedStyle(document.getElementById("stage"));
return stage["background-color"];
},
run: function()
{
return this.waitUntilReady().then(function() {
this._finishPromise = new SimplePromise;
this._previousTimestamp = undefined;
this._didWarmUp = false;
this._stage.tune(this._controller.initialComplexity - this._stage.complexity());
this._animateLoop();
return this._finishPromise;
}.bind(this));
},
// Subclasses should override this if they have setup to do prior to commencing.
waitUntilReady: function()
{
var promise = new SimplePromise;
promise.resolve();
return promise;
},
_animateLoop: function(timestamp)
{
timestamp = (this._getTimestamp && this._getTimestamp()) || timestamp;
this._currentTimestamp = timestamp;
if (this._controller.shouldStop(timestamp)) {
this._finishPromise.resolve(this._controller.results());
return;
}
if (!this._didWarmUp) {
if (!this._previousTimestamp)
this._previousTimestamp = timestamp;
else if (timestamp - this._previousTimestamp >= 100) {
this._didWarmUp = true;
this._startTimestamp = timestamp;
this._controller.start(timestamp, this._stage);
this._previousTimestamp = timestamp;
}
this._stage.animate(0);
requestAnimationFrame(this._animateLoop);
return;
}
this._controller.update(timestamp, this._stage);
this._stage.animate(timestamp - this._previousTimestamp);
this._previousTimestamp = timestamp;
requestAnimationFrame(this._animateLoop);
}
});