| /* |
| * 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); |
| } |
| }); |