From 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:47:29 +0200 Subject: Adding upstream version 115.8.0esr. Signed-off-by: Daniel Baumann --- .../MotionMark/tests/resources/main.js | 934 +++++++++++++++++++++ .../MotionMark/tests/resources/math.js | 268 ++++++ .../MotionMark/tests/resources/stage.css | 27 + .../MotionMark/tests/resources/star.svg | 8 + .../MotionMark/tests/resources/yin-yang.png | Bin 0 -> 4082 bytes .../MotionMark/tests/resources/yin-yang.svg | 17 + 6 files changed, 1254 insertions(+) create mode 100644 third_party/webkit/PerformanceTests/MotionMark/tests/resources/main.js create mode 100644 third_party/webkit/PerformanceTests/MotionMark/tests/resources/math.js create mode 100644 third_party/webkit/PerformanceTests/MotionMark/tests/resources/stage.css create mode 100644 third_party/webkit/PerformanceTests/MotionMark/tests/resources/star.svg create mode 100644 third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.png create mode 100644 third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.svg (limited to 'third_party/webkit/PerformanceTests/MotionMark/tests/resources') diff --git a/third_party/webkit/PerformanceTests/MotionMark/tests/resources/main.js b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/main.js new file mode 100644 index 0000000000..b9776d571e --- /dev/null +++ b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/main.js @@ -0,0 +1,934 @@ +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); + } +}); diff --git a/third_party/webkit/PerformanceTests/MotionMark/tests/resources/math.js b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/math.js new file mode 100644 index 0000000000..9c2706e2a3 --- /dev/null +++ b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/math.js @@ -0,0 +1,268 @@ +SimpleKalmanEstimator = Utilities.createSubclass(Experiment, + function(processError, measurementError) { + Experiment.call(this, false); + var error = .5 * (Math.sqrt(processError * processError + 4 * processError * measurementError) - processError); + this._gain = error / (error + measurementError); + }, { + + sample: function(newMeasurement) + { + if (!this._initialized) { + this._initialized = true; + this.estimate = newMeasurement; + return; + } + + this.estimate = this.estimate + this._gain * (newMeasurement - this.estimate); + }, + + reset: function() + { + Experiment.prototype.reset.call(this); + this._initialized = false; + this.estimate = 0; + } +}); + +PIDController = Utilities.createClass( + function(ysp) + { + this._ysp = ysp; + this._out = 0; + + this._Kp = 0; + this._stage = PIDController.stages.WARMING; + + this._eold = 0; + this._I = 0; + }, { + + // Determines whether the current y is + // before ysp => (below ysp if ysp > y0) || (above ysp if ysp < y0) + // after ysp => (above ysp if ysp > y0) || (below ysp if ysp < y0) + _yPosition: function(y) + { + return (y < this._ysp) == (this._y0 < this._ysp) + ? PIDController.yPositions.BEFORE_SETPOINT + : PIDController.yPositions.AFTER_SETPOINT; + }, + + // Calculate the ultimate distance from y0 after time t. We want to move very + // slowly at the beginning to see how adding few items to the test can affect + // its output. The complexity of a single item might be big enough to keep the + // proportional gain very small but achieves the desired progress. But if y does + // not change significantly after adding few items, that means we need a much + // bigger gain. So we need to move over a cubic curve which increases very + // slowly with small t values but moves very fast with larger t values. + // The basic formula is: y = t^3 + // Change the formula to reach y=1 after 1000 ms: y = (t/1000)^3 + // Change the formula to reach y=(ysp - y0) after 1000 ms: y = (ysp - y0) * (t/1000)^3 + _distanceUltimate: function(t) + { + return (this._ysp - this._y0) * Math.pow(t / 1000, 3); + }, + + // Calculates the distance of y relative to y0. It also ensures we do not return + // zero by returning a epsilon value in the same direction as ultimate distance. + _distance: function(y, du) + { + const epsilon = 0.0001; + var d = y - this._y0; + return du < 0 ? Math.min(d, -epsilon) : Math.max(d, epsilon); + }, + + // Decides how much the proportional gain should be increased during the manual + // gain stage. We choose to use the ratio of the ultimate distance to the current + // distance as an indication of how much the system is responsive. We want + // to keep the increment under control so it does not cause the system instability + // So we choose to take the natural logarithm of this ratio. + _gainIncrement: function(t, y, e) + { + var du = this._distanceUltimate(t); + var d = this._distance(y, du); + return Math.log(du / d) * 0.1; + }, + + // Update the stage of the controller based on its current stage and the system output + _updateStage: function(y) + { + var yPosition = this._yPosition(y); + + switch (this._stage) { + case PIDController.stages.WARMING: + if (yPosition == PIDController.yPositions.AFTER_SETPOINT) + this._stage = PIDController.stages.OVERSHOOT; + break; + + case PIDController.stages.OVERSHOOT: + if (yPosition == PIDController.yPositions.BEFORE_SETPOINT) + this._stage = PIDController.stages.UNDERSHOOT; + break; + + case PIDController.stages.UNDERSHOOT: + if (yPosition == PIDController.yPositions.AFTER_SETPOINT) + this._stage = PIDController.stages.SATURATE; + break; + } + }, + + // Manual tuning is used before calculating the PID controller gains. + _tuneP: function(e) + { + // The output is the proportional term only. + return this._Kp * e; + }, + + // PID tuning function. Kp, Ti and Td were already calculated + _tunePID: function(h, y, e) + { + // Proportional term. + var P = this._Kp * e; + + // Integral term is the area under the curve starting from the beginning + // till the current time. + this._I += (this._Kp / this._Ti) * ((e + this._eold) / 2) * h; + + // Derivative term is the slope of the curve at the current time. + var D = (this._Kp * this._Td) * (e - this._eold) / h; + + // The ouput is a PID function. + return P + this._I + D; + }, + + // Apply different strategies for the tuning based on the stage of the controller. + _tune: function(t, h, y, e) + { + switch (this._stage) { + case PIDController.stages.WARMING: + // This is the first stage of the Zieglerâ€Nichols method. It increments + // the proportional gain till the system output passes the set-point value. + if (typeof this._y0 == "undefined") { + // This is the first time a tuning value is required. We want the test + // to add only one item. So we need to return -1 which forces us to + // choose the initial value of Kp to be = -1 / e + this._y0 = y; + this._Kp = -1 / e; + } else { + // Keep incrementing the Kp as long as we have not reached the + // set-point yet + this._Kp += this._gainIncrement(t, y, e); + } + + return this._tuneP(e); + + case PIDController.stages.OVERSHOOT: + // This is the second stage of the Zieglerâ€Nichols method. It measures the + // oscillation period. + if (typeof this._t0 == "undefined") { + // t is the time of the begining of the first overshot + this._t0 = t; + this._Kp /= 2; + } + + return this._tuneP(e); + + case PIDController.stages.UNDERSHOOT: + // This is the end of the Zieglerâ€Nichols method. We need to calculate the + // integral and derivative periods. + if (typeof this._Ti == "undefined") { + // t is the time of the end of the first overshot + var Tu = t - this._t0; + + // Calculate the system parameters from Kp and Tu assuming + // a "some overshoot" control type. See: + // https://en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method + this._Ti = Tu / 2; + this._Td = Tu / 3; + this._Kp = 0.33 * this._Kp; + + // Calculate the tracking time. + this._Tt = Math.sqrt(this._Ti * this._Td); + } + + return this._tunePID(h, y, e); + + case PIDController.stages.SATURATE: + return this._tunePID(h, y, e); + } + + return 0; + }, + + // Ensures the system does not fluctuates. + _saturate: function(v, e) + { + var u = v; + + switch (this._stage) { + case PIDController.stages.OVERSHOOT: + case PIDController.stages.UNDERSHOOT: + // Calculate the min-max values of the saturation actuator. + if (typeof this._min == "undefined") + this._min = this._max = this._out; + else { + this._min = Math.min(this._min, this._out); + this._max = Math.max(this._max, this._out); + } + break; + + case PIDController.stages.SATURATE: + const limitPercentage = 0.90; + var min = this._min > 0 ? Math.min(this._min, this._max * limitPercentage) : this._min; + var max = this._max < 0 ? Math.max(this._max, this._min * limitPercentage) : this._max; + var out = this._out + u; + + // Clip the controller output to the min-max values + out = Math.max(Math.min(max, out), min); + u = out - this._out; + + // Apply the back-calculation and tracking + if (u != v) + u += (this._Kp * this._Tt / this._Ti) * e; + break; + } + + this._out += u; + return u; + }, + + // Called from the benchmark to tune its test. It uses Ziegler-Nichols method + // to calculate the controller parameters. It then returns a PID tuning value. + tune: function(t, h, y) + { + this._updateStage(y); + + // Current error. + var e = this._ysp - y; + var v = this._tune(t, h, y, e); + + // Save e for the next call. + this._eold = e; + + // Apply back-calculation and tracking to avoid integrator windup + return this._saturate(v, e); + } +}); + +Utilities.extendObject(PIDController, { + // This enum will be used to tell whether the system output (or the controller input) + // is moving towards the set-point or away from it. + yPositions: { + BEFORE_SETPOINT: 0, + AFTER_SETPOINT: 1 + }, + + // The Ziegler-Nichols method for is used tuning the PID controller. The workflow of + // the tuning is split into four stages. The first two stages determine the values + // of the PID controller gains. During these two stages we return the proportional + // term only. The third stage is used to determine the min-max values of the + // saturation actuator. In the last stage back-calculation and tracking are applied + // to avoid integrator windup. During the last two stages, we return a PID control + // value. + stages: { + WARMING: 0, // Increase the value of the Kp until the system output reaches ysp. + OVERSHOOT: 1, // Measure the oscillation period and the overshoot value + UNDERSHOOT: 2, // Return PID value and measure the undershoot value + SATURATE: 3 // Return PID value and apply back-calculation and tracking. + } +}); diff --git a/third_party/webkit/PerformanceTests/MotionMark/tests/resources/stage.css b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/stage.css new file mode 100644 index 0000000000..0b6ffdc4a2 --- /dev/null +++ b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/stage.css @@ -0,0 +1,27 @@ +html { + height: 100%; +} +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + background-color: rgb(241, 241, 241); + font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif; +} + +#stage { + position: relative; + width: 100%; + height: 100%; + background-color: rgb(241, 241, 241); + overflow: hidden; +} + +#center-text { + position: absolute; + z-index: 3; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/third_party/webkit/PerformanceTests/MotionMark/tests/resources/star.svg b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/star.svg new file mode 100644 index 0000000000..3c46ae0419 --- /dev/null +++ b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/star.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.png b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.png new file mode 100644 index 0000000000..3162f6ec00 Binary files /dev/null and b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.png differ diff --git a/third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.svg b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.svg new file mode 100644 index 0000000000..4412626774 --- /dev/null +++ b/third_party/webkit/PerformanceTests/MotionMark/tests/resources/yin-yang.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + -- cgit v1.2.3