| /* |
| * 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. |
| */ |
| Pseudo = |
| { |
| initialRandomSeed: 49734321, |
| randomSeed: 49734321, |
| |
| resetRandomSeed: function() |
| { |
| Pseudo.randomSeed = Pseudo.initialRandomSeed; |
| }, |
| |
| random: function() |
| { |
| var randomSeed = Pseudo.randomSeed; |
| randomSeed = ((randomSeed + 0x7ed55d16) + (randomSeed << 12)) & 0xffffffff; |
| randomSeed = ((randomSeed ^ 0xc761c23c) ^ (randomSeed >>> 19)) & 0xffffffff; |
| randomSeed = ((randomSeed + 0x165667b1) + (randomSeed << 5)) & 0xffffffff; |
| randomSeed = ((randomSeed + 0xd3a2646c) ^ (randomSeed << 9)) & 0xffffffff; |
| randomSeed = ((randomSeed + 0xfd7046c5) + (randomSeed << 3)) & 0xffffffff; |
| randomSeed = ((randomSeed ^ 0xb55a4f09) ^ (randomSeed >>> 16)) & 0xffffffff; |
| Pseudo.randomSeed = randomSeed; |
| return (randomSeed & 0xfffffff) / 0x10000000; |
| } |
| }; |
| |
| Statistics = |
| { |
| sampleMean: function(numberOfSamples, sum) |
| { |
| if (numberOfSamples < 1) |
| return 0; |
| return sum / numberOfSamples; |
| }, |
| |
| // With sum and sum of squares, we can compute the sample standard deviation in O(1). |
| // See https://rniwa.com/2012-11-10/sample-standard-deviation-in-terms-of-sum-and-square-sum-of-samples/ |
| unbiasedSampleStandardDeviation: function(numberOfSamples, sum, squareSum) |
| { |
| if (numberOfSamples < 2) |
| return 0; |
| return Math.sqrt((squareSum - sum * sum / numberOfSamples) / (numberOfSamples - 1)); |
| }, |
| |
| geometricMean: function(values) |
| { |
| if (!values.length) |
| return 0; |
| var roots = values.map(function(value) { return Math.pow(value, 1 / values.length); }) |
| return roots.reduce(function(a, b) { return a * b; }); |
| }, |
| |
| // Cumulative distribution function |
| cdf: function(value, mean, standardDeviation) |
| { |
| return 0.5 * (1 + Statistics.erf((value - mean) / (Math.sqrt(2 * standardDeviation * standardDeviation)))); |
| }, |
| |
| // Approximation of Gauss error function, Abramowitz and Stegun 7.1.26 |
| erf: function(value) |
| { |
| var sign = (value >= 0) ? 1 : -1; |
| value = Math.abs(value); |
| |
| var a1 = 0.254829592; |
| var a2 = -0.284496736; |
| var a3 = 1.421413741; |
| var a4 = -1.453152027; |
| var a5 = 1.061405429; |
| var p = 0.3275911; |
| |
| var t = 1.0 / (1.0 + p * value); |
| var y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-value * value); |
| return sign * y; |
| }, |
| |
| largestDeviationPercentage: function(low, mean, high) |
| { |
| return Math.max(Math.abs(low / mean - 1), (high / mean - 1)); |
| } |
| }; |
| |
| Experiment = Utilities.createClass( |
| function(includeConcern) |
| { |
| if (includeConcern) |
| this._maxHeap = Heap.createMaxHeap(Experiment.defaults.CONCERN_SIZE); |
| this.reset(); |
| }, { |
| |
| reset: function() |
| { |
| this._sum = 0; |
| this._squareSum = 0; |
| this._numberOfSamples = 0; |
| if (this._maxHeap) |
| this._maxHeap.init(); |
| }, |
| |
| get sampleCount() |
| { |
| return this._numberOfSamples; |
| }, |
| |
| sample: function(value) |
| { |
| this._sum += value; |
| this._squareSum += value * value; |
| if (this._maxHeap) |
| this._maxHeap.push(value); |
| ++this._numberOfSamples; |
| }, |
| |
| mean: function() |
| { |
| return Statistics.sampleMean(this._numberOfSamples, this._sum); |
| }, |
| |
| standardDeviation: function() |
| { |
| return Statistics.unbiasedSampleStandardDeviation(this._numberOfSamples, this._sum, this._squareSum); |
| }, |
| |
| cdf: function(value) |
| { |
| return Statistics.cdf(value, this.mean(), this.standardDeviation()); |
| }, |
| |
| percentage: function() |
| { |
| var mean = this.mean(); |
| return mean ? this.standardDeviation() * 100 / mean : 0; |
| }, |
| |
| concern: function(percentage) |
| { |
| if (!this._maxHeap) |
| return this.mean(); |
| |
| var size = Math.ceil(this._numberOfSamples * percentage / 100); |
| var values = this._maxHeap.values(size); |
| return values.length ? values.reduce(function(a, b) { return a + b; }) / values.length : 0; |
| }, |
| |
| score: function(percentage) |
| { |
| return Statistics.geometricMean([this.mean(), Math.max(this.concern(percentage), 1)]); |
| } |
| }); |
| |
| Experiment.defaults = |
| { |
| CONCERN: 5, |
| CONCERN_SIZE: 100, |
| }; |
| |
| Regression = Utilities.createClass( |
| function(samples, getComplexity, getFrameLength, startIndex, endIndex, options) |
| { |
| var desiredFrameLength = options.desiredFrameLength || 1000/60; |
| var bestProfile; |
| |
| if (!options.preferredProfile || options.preferredProfile == Strings.json.profiles.slope) { |
| var slope = this._calculateRegression(samples, getComplexity, getFrameLength, startIndex, endIndex, { |
| shouldClip: true, |
| s1: desiredFrameLength, |
| t1: 0 |
| }); |
| if (!bestProfile || slope.error < bestProfile.error) { |
| bestProfile = slope; |
| this.profile = Strings.json.profiles.slope; |
| } |
| } |
| if (!options.preferredProfile || options.preferredProfile == Strings.json.profiles.flat) { |
| var flat = this._calculateRegression(samples, getComplexity, getFrameLength, startIndex, endIndex, { |
| shouldClip: true, |
| s1: desiredFrameLength, |
| t1: 0, |
| t2: 0 |
| }); |
| |
| if (!bestProfile || flat.error < bestProfile.error) { |
| bestProfile = flat; |
| this.profile = Strings.json.profiles.flat; |
| } |
| } |
| |
| this.startIndex = Math.min(startIndex, endIndex); |
| this.endIndex = Math.max(startIndex, endIndex); |
| |
| this.complexity = bestProfile.complexity; |
| this.s1 = bestProfile.s1; |
| this.t1 = bestProfile.t1; |
| this.s2 = bestProfile.s2; |
| this.t2 = bestProfile.t2; |
| this.stdev1 = bestProfile.stdev1; |
| this.stdev2 = bestProfile.stdev2; |
| this.n1 = bestProfile.n1; |
| this.n2 = bestProfile.n2; |
| this.error = bestProfile.error; |
| }, { |
| |
| valueAt: function(complexity) |
| { |
| if (this.n1 == 1 || complexity > this.complexity) |
| return this.s2 + this.t2 * complexity; |
| return this.s1 + this.t1 * complexity; |
| }, |
| |
| // A generic two-segment piecewise regression calculator. Based on Kundu/Ubhaya |
| // |
| // Minimize sum of (y - y')^2 |
| // where y = s1 + t1*x |
| // y = s2 + t2*x |
| // y' = s1 + t1*x' = s2 + t2*x' if x_0 <= x' <= x_n |
| // |
| // Allows for fixing s1, t1, s2, t2 |
| // |
| // x is assumed to be complexity, y is frame length. Can be used for pure complexity-FPS |
| // analysis or for ramp controllers since complexity monotonically decreases with time. |
| _calculateRegression: function(samples, getComplexity, getFrameLength, startIndex, endIndex, options) |
| { |
| if (startIndex == endIndex) { |
| // Only one sample point; we can't calculate any regression. |
| var x = getComplexity(samples, startIndex); |
| return { |
| complexity: x, |
| s1: x, |
| t1: 0, |
| s2: x, |
| t2: 0, |
| error1: 0, |
| error2: 0 |
| }; |
| } |
| |
| // x is expected to increase in complexity |
| var iterationDirection = endIndex > startIndex ? 1 : -1; |
| var lowComplexity = getComplexity(samples, startIndex); |
| var highComplexity = getComplexity(samples, endIndex); |
| var a1 = 0, b1 = 0, c1 = 0, d1 = 0, h1 = 0, k1 = 0; |
| var a2 = 0, b2 = 0, c2 = 0, d2 = 0, h2 = 0, k2 = 0; |
| |
| // Iterate from low to high complexity |
| for (var i = startIndex; iterationDirection * (endIndex - i) > -1; i += iterationDirection) { |
| var x = getComplexity(samples, i); |
| var y = getFrameLength(samples, i); |
| a2 += 1; |
| b2 += x; |
| c2 += x * x; |
| d2 += y; |
| h2 += y * x; |
| k2 += y * y; |
| } |
| |
| var s1_best, t1_best, s2_best, t2_best, n1_best, n2_best, error1_best, error2_best, x_best, x_prime; |
| |
| function setBest(s1, t1, error1, s2, t2, error2, splitIndex, x_prime, x) |
| { |
| s1_best = s1; |
| t1_best = t1; |
| error1_best = error1; |
| s2_best = s2; |
| t2_best = t2; |
| error2_best = error2; |
| // Number of samples included in the first segment, inclusive of splitIndex |
| n1_best = iterationDirection * (splitIndex - startIndex) + 1; |
| // Number of samples included in the second segment |
| n2_best = iterationDirection * (endIndex - splitIndex); |
| if (!options.shouldClip || (x_prime >= lowComplexity && x_prime <= highComplexity)) |
| x_best = x_prime; |
| else { |
| // Discontinuous piecewise regression |
| x_best = x; |
| } |
| } |
| |
| // Iterate from startIndex to endIndex - 1, inclusive |
| for (var i = startIndex; iterationDirection * (endIndex - i) > 0; i += iterationDirection) { |
| var x = getComplexity(samples, i); |
| var y = getFrameLength(samples, i); |
| var xx = x * x; |
| var yx = y * x; |
| var yy = y * y; |
| // a1, b1, etc. is sum from startIndex to i, inclusive |
| a1 += 1; |
| b1 += x; |
| c1 += xx; |
| d1 += y; |
| h1 += yx; |
| k1 += yy; |
| // a2, b2, etc. is sum from i+1 to endIndex, inclusive |
| a2 -= 1; |
| b2 -= x; |
| c2 -= xx; |
| d2 -= y; |
| h2 -= yx; |
| k2 -= yy; |
| |
| var A = c1*d1 - b1*h1; |
| var B = a1*h1 - b1*d1; |
| var C = a1*c1 - b1*b1; |
| var D = c2*d2 - b2*h2; |
| var E = a2*h2 - b2*d2; |
| var F = a2*c2 - b2*b2; |
| var s1 = options.s1 !== undefined ? options.s1 : (A / C); |
| var t1 = options.t1 !== undefined ? options.t1 : (B / C); |
| var s2 = options.s2 !== undefined ? options.s2 : (D / F); |
| var t2 = options.t2 !== undefined ? options.t2 : (E / F); |
| // Assumes that the two segments meet |
| var x_prime = (s1 - s2) / (t2 - t1); |
| |
| var error1 = (k1 + a1*s1*s1 + c1*t1*t1 - 2*d1*s1 - 2*h1*t1 + 2*b1*s1*t1) || Number.MAX_VALUE; |
| var error2 = (k2 + a2*s2*s2 + c2*t2*t2 - 2*d2*s2 - 2*h2*t2 + 2*b2*s2*t2) || Number.MAX_VALUE; |
| |
| if (i == startIndex) { |
| setBest(s1, t1, error1, s2, t2, error2, i, x_prime, x); |
| continue; |
| } |
| |
| if (C == 0 || F == 0) |
| continue; |
| |
| // Projected point is not between this and the next sample |
| if (x_prime > getComplexity(samples, i + iterationDirection) || x_prime < x) { |
| // Calculate lambda, which divides the weight of this sample between the two lines |
| |
| // These values remove the influence of this sample |
| var I = c1 - 2*b1*x + a1*xx; |
| var H = C - I; |
| var G = A + B*x - C*y; |
| |
| var J = D + E*x - F*y; |
| var K = c2 - 2*b2*x + a2*xx; |
| |
| var lambda = (G*F + G*K - H*J) / (I*J + G*K); |
| if (lambda > 0 && lambda < 1) { |
| var lambda1 = 1 - lambda; |
| s1 = options.s1 !== undefined ? options.s1 : ((A - lambda1*(-h1*x + d1*xx + c1*y - b1*yx)) / (C - lambda1*I)); |
| t1 = options.t1 !== undefined ? options.t1 : ((B - lambda1*(h1 - d1*x - b1*y + a1*yx)) / (C - lambda1*I)); |
| s2 = options.s2 !== undefined ? options.s2 : ((D + lambda1*(-h2*x + d2*xx + c2*y - b2*yx)) / (F + lambda1*K)); |
| t2 = options.t2 !== undefined ? options.t2 : ((E + lambda1*(h2 - d2*x - b2*y + a2*yx)) / (F + lambda1*K)); |
| x_prime = (s1 - s2) / (t2 - t1); |
| |
| error1 = ((k1 + a1*s1*s1 + c1*t1*t1 - 2*d1*s1 - 2*h1*t1 + 2*b1*s1*t1) - lambda1 * Math.pow(y - (s1 + t1*x), 2)) || Number.MAX_VALUE; |
| error2 = ((k2 + a2*s2*s2 + c2*t2*t2 - 2*d2*s2 - 2*h2*t2 + 2*b2*s2*t2) + lambda1 * Math.pow(y - (s2 + t2*x), 2)) || Number.MAX_VALUE; |
| } else if (t1 != t2) |
| continue; |
| } |
| |
| if (error1 + error2 < error1_best + error2_best) |
| setBest(s1, t1, error1, s2, t2, error2, i, x_prime, x); |
| } |
| |
| return { |
| complexity: x_best, |
| s1: s1_best, |
| t1: t1_best, |
| stdev1: Math.sqrt(error1_best / n1_best), |
| s2: s2_best, |
| t2: t2_best, |
| stdev2: Math.sqrt(error2_best / n2_best), |
| error: error1_best + error2_best, |
| n1: n1_best, |
| n2: n2_best |
| }; |
| } |
| }); |
| |
| Utilities.extendObject(Regression, { |
| bootstrap: function(samples, iterationCount, processResample, confidencePercentage) |
| { |
| var sampleLength = samples.length; |
| var resample = new Array(sampleLength); |
| |
| var bootstrapEstimator = new Experiment; |
| var bootstrapData = new Array(iterationCount); |
| |
| Pseudo.resetRandomSeed(); |
| for (var i = 0; i < iterationCount; ++i) { |
| for (var j = 0; j < sampleLength; ++j) |
| resample[j] = samples[Math.floor(Pseudo.random() * sampleLength)]; |
| |
| var resampleResult = processResample(resample); |
| bootstrapEstimator.sample(resampleResult); |
| bootstrapData[i] = resampleResult; |
| } |
| |
| bootstrapData.sort(function(a, b) { return a - b; }); |
| return { |
| confidenceLow: bootstrapData[Math.round((iterationCount - 1) * (1 - confidencePercentage) / 2)], |
| confidenceHigh: bootstrapData[Math.round((iterationCount - 1) * (1 + confidencePercentage) / 2)], |
| median: bootstrapData[Math.round(iterationCount / 2)], |
| mean: bootstrapEstimator.mean(), |
| data: bootstrapData, |
| confidencePercentage: confidencePercentage |
| }; |
| } |
| }); |