Calculate the graphics benchmark test gain adaptively
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Nov 2015 01:20:48 +0000 (01:20 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 20 Nov 2015 01:20:48 +0000 (01:20 +0000)
https://bugs.webkit.org/show_bug.cgi?id=151208

Patch by Said Abou-Hallawa <sabouhallawa@apple.com> on 2015-11-19
Reviewed by Darin Adler.

We need to calculate the gain of the graphics benchmark tests adaptively
and get rid of the gain and limits parameters we have to choose manually
for every test. We are going to use the classic Ziegler–Nichols method for
calculating the gain and integral and derivative times. We are going to
try moving on a cubic curve during the manual stage from y0 to reach ysp.
We also going to use a saturation actuator to ensure the system does not
fluctuate.

* Animometer/resources/extensions.js:
(ResultsTable.prototype._isNoisyMeasurement): Fix a parameter name.
(ResultsTable.prototype._isNoisyTest): Since score is a member of testResults, we need to limit our search to frame rate and complexity.
(ResultsTable.prototype._showTest): Pass the correct parameter to _isNoisyMeasurement().

* Animometer/resources/strings.js: Fix the indentation and name and value of a string.

* Animometer/runner/resources/tests.js: Remove all the manual gains and limits parameters which had to be passed to every test.

* Animometer/tests/resources/main.js:
(BenchmarkState.prototype.currentStage): Fix an enum name.
(Benchmark): Get rid of manual gain and limits.
(Benchmark.prototype.update): Simplify the calculation by having all the times in ms.

* Animometer/tests/resources/math.js:
(PIDController): Get rid of the manual gain and limits and the magic numbers for Ti and Td.
(PIDController.prototype._yPosition): Tells whether the test current output is moving towards the set-point or away from it.
(PIDController.prototype._distanceUltimate): Calculates the ultimate distance from y0 after time t using a cubic formula.
(PIDController.prototype._distance): Calculates the distance of y relative to y0.
(PIDController.prototype._gainIncrement): Decides how much the proportional gain should be increased during the manual gain stage.
(PIDController.prototype._updateStage): Updates the stage of the controller based on its current stage and the system output.
(PIDController.prototype._tuneP): Tunes the system before calculating the PID controller gains.
(PIDController.prototype._tunePID): PID tuning function.
(PIDController.prototype._tune):
(PIDController.prototype._saturate):
(PIDController.prototype.tune): Manages calculating the controller parameters. It then returns a PID tuning value.
(PIDController.prototype._sat): Deleted. We may need to return it back but the limits have to be calculated adaptively not manually.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@192669 268f45cc-cd09-0410-ab3c-d52691b4dbfc

PerformanceTests/Animometer/resources/extensions.js
PerformanceTests/Animometer/resources/strings.js
PerformanceTests/Animometer/runner/resources/tests.js
PerformanceTests/Animometer/tests/resources/main.js
PerformanceTests/Animometer/tests/resources/math.js
PerformanceTests/ChangeLog

index 32782aa..566dc8d 100644 (file)
@@ -361,7 +361,7 @@ ResultsTable.prototype =
         button.textContent = Strings.text.results.json + "...";
     },
     
-    _isNoisyMeasurement: function(experiment, data, measurement, options)
+    _isNoisyMeasurement: function(jsonExperiment, data, measurement, options)
     {
         const percentThreshold = 10;
         const averageThreshold = 2;
@@ -369,7 +369,7 @@ ResultsTable.prototype =
         if (measurement == Strings.json.measurements.percent)
             return data[Strings.json.measurements.percent] >= percentThreshold;
             
-        if (experiment == Strings.json.experiments.frameRate && measurement == Strings.json.measurements.average)
+        if (jsonExperiment == Strings.json.experiments.frameRate && measurement == Strings.json.measurements.average)
             return Math.abs(data[Strings.json.measurements.average] - options["frame-rate"]) >= averageThreshold;
 
         return false;
@@ -377,10 +377,11 @@ ResultsTable.prototype =
 
     _isNoisyTest: function(testResults, options)
     {
-        for (var experiment in testResults) {
-            var data = testResults[experiment];
+        for (var index = 0; index < 2; ++index) {
+            var jsonExperiment = !index ? Strings.json.experiments.complexity : Strings.json.experiments.frameRate;
+            var data = testResults[jsonExperiment];
             for (var measurement in data) {
-                if (this._isNoisyMeasurement(experiment, data, measurement, options))
+                if (this._isNoisyMeasurement(jsonExperiment, data, measurement, options))
                     return true;
             }
         }
@@ -422,9 +423,10 @@ ResultsTable.prototype =
 
             case 2:
             case 3:
-                var data = testResults[index == 2 ? Strings.json.experiments.complexity : Strings.json.experiments.frameRate];
+                var jsonExperiment = index == 2 ? Strings.json.experiments.complexity : Strings.json.experiments.frameRate;
+                var data = testResults[jsonExperiment];
                 for (var measurement in data)
-                    this._showFixedNumber(row, data[measurement], 2, this._isNoisyMeasurement(index - 2, data, measurement, options) ? className : "");
+                    this._showFixedNumber(row, data[measurement], 2, this._isNoisyMeasurement(jsonExperiment, data, measurement, options) ? className : "");
                 break;
                 
             case 4:
index 43bd3a8..8293cbe 100644 (file)
@@ -1,12 +1,12 @@
 var Strings = {
     text: {
-       testName: "Test Name",
-       score: "Score",
-       samples: "Samples",
+        testName: "Test Name",
+        score: "Score",
+        samples: "Samples",
 
-       runningState: {
-           warmingup: "Warming up",
-           sampling: "Sampling",
+        runningState: {
+            warming: "Warming",
+            sampling: "Sampling",
             finished: "Finished"
         },
 
@@ -55,5 +55,4 @@ var Strings = {
             samplingTimeOffset: "samplingTimeOffset"
         }
     }
-
 };
index 76b994e..084b1f2 100644 (file)
@@ -62,27 +62,27 @@ var Suites = [];
 Suites.push(new Suite("HTML suite",
     [
         { 
-            url: "bouncing-particles/bouncing-css-shapes.html?gain=1&addLimit=100&removeLimit=5&particleWidth=12&particleHeight=12&shape=circle",
+            url: "bouncing-particles/bouncing-css-shapes.html?particleWidth=12&particleHeight=12&shape=circle",
             name: "CSS bouncing circles"
         },
         { 
-            url: "bouncing-particles/bouncing-css-shapes.html?gain=1&addLimit=100&removeLimit=5&particleWidth=40&particleHeight=40&shape=rect&clip=star",
+            url: "bouncing-particles/bouncing-css-shapes.html?particleWidth=40&particleHeight=40&shape=rect&clip=star",
             name: "CSS bouncing clipped rects"
         },
         { 
-            url: "bouncing-particles/bouncing-css-shapes.html?gain=1&addLimit=100&removeLimit=5&particleWidth=50&particleHeight=50&shape=circle&fill=gradient",
+            url: "bouncing-particles/bouncing-css-shapes.html?particleWidth=50&particleHeight=50&shape=circle&fill=gradient",
             name: "CSS bouncing gradient circles"
         },
         {
-            url: "bouncing-particles/bouncing-css-images.html?gain=0.4&addLimit=5&removeLimit=2&particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.svg",
+            url: "bouncing-particles/bouncing-css-images.html?particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.svg",
             name: "CSS bouncing SVG images"
         },
         {
-            url: "bouncing-particles/bouncing-css-images.html?gain=1&addLimit=100&removeLimit=5&particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.png",
+            url: "bouncing-particles/bouncing-css-images.html?particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.png",
             name: "CSS bouncing PNG images"
         },
         {
-            url: "text/layering-text.html?gain=4&addLimit=100&removeLimit=100",
+            url: "text/layering-text.html",
             name: "CSS layering text"
         },
     ]
@@ -91,23 +91,23 @@ Suites.push(new Suite("HTML suite",
 Suites.push(new Suite("Canvas suite",
     [
         { 
-            url: "bouncing-particles/bouncing-canvas-shapes.html?gain=4&addLimit=100&removeLimit=1000&particleWidth=12&particleHeight=12&shape=circle",
+            url: "bouncing-particles/bouncing-canvas-shapes.html?particleWidth=12&particleHeight=12&shape=circle",
             name: "canvas bouncing circles"
         },
         { 
-            url: "bouncing-particles/bouncing-canvas-shapes.html?gain=4&addLimit=100&removeLimit=1000&particleWidth=40&particleHeight=40&shape=rect&clip=star",
+            url: "bouncing-particles/bouncing-canvas-shapes.html?particleWidth=40&particleHeight=40&shape=rect&clip=star",
             name: "canvas bouncing clipped rects"
         },
         { 
-            url: "bouncing-particles/bouncing-canvas-shapes.html?gain=4&addLimit=100&removeLimit=1000&particleWidth=50&particleHeight=50&shape=circle&fill=gradient",
+            url: "bouncing-particles/bouncing-canvas-shapes.html?particleWidth=50&particleHeight=50&shape=circle&fill=gradient",
             name: "canvas bouncing gradient circles"
         },
         { 
-            url: "bouncing-particles/bouncing-canvas-images.html?gain=0.4&addLimit=5&removeLimit=1&particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.svg",
+            url: "bouncing-particles/bouncing-canvas-images.html?particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.svg",
             name: "canvas bouncing SVG images"
         },
         {
-            url: "bouncing-particles/bouncing-canvas-images.html?gain=4&addLimit=1000&removeLimit=1000&particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.png",
+            url: "bouncing-particles/bouncing-canvas-images.html?particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.png",
             name: "canvas bouncing PNG images"
         },
     ]
@@ -116,23 +116,23 @@ Suites.push(new Suite("Canvas suite",
 Suites.push(new Suite("SVG suite",
     [
         {
-            url: "bouncing-particles/bouncing-svg-shapes.html?gain=6&addLimit=100&removeLimit=1000&particleWidth=12&particleHeight=12&shape=circle",
+            url: "bouncing-particles/bouncing-svg-shapes.html?particleWidth=12&particleHeight=12&shape=circle",
             name: "SVG bouncing circles",
         },
         {
-            url: "bouncing-particles/bouncing-svg-shapes.html?gain=0.6&addLimit=10&removeLimit=1&particleWidth=40&particleHeight=40&shape=rect&clip=star",
+            url: "bouncing-particles/bouncing-svg-shapes.html?particleWidth=40&particleHeight=40&shape=rect&clip=star",
             name: "SVG bouncing clipped rects",
         },
         {
-            url: "bouncing-particles/bouncing-svg-shapes.html?gain=0.8&addLimit=10&removeLimit=4&particleWidth=50&particleHeight=50&shape=circle&fill=gradient",
+            url: "bouncing-particles/bouncing-svg-shapes.html?particleWidth=50&particleHeight=50&shape=circle&fill=gradient",
             name: "SVG bouncing gradient circles"
         },
         {
-            url: "bouncing-particles/bouncing-svg-images.html?gain=0.4&addLimit=5&removeLimit=2&particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.svg",
+            url: "bouncing-particles/bouncing-svg-images.html?particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.svg",
             name: "SVG bouncing SVG images"
         },
         {
-            url: "bouncing-particles/bouncing-svg-images.html?gain=4&addLimit=100&removeLimit=5&particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.png",
+            url: "bouncing-particles/bouncing-svg-images.html?particleWidth=80&particleHeight=80&imageSrc=../resources/yin-yang.png",
             name: "SVG bouncing PNG images"
         },
     ]
@@ -226,11 +226,11 @@ Suites.push(new Suite("Basic canvas path suite",
 Suites.push(new Suite("Complex examples",
     [
         {
-            url: "examples/canvas-electrons.html?gain=1&addLimit=100&removeLimit=10",
+            url: "examples/canvas-electrons.html",
             name: "canvas electrons"
         },
         {
-            url: "examples/canvas-stars.html?gain=4&addLimit=100&removeLimit=5",
+            url: "examples/canvas-stars.html",
             name: "canvas stars"
         },
     ]
@@ -239,15 +239,15 @@ Suites.push(new Suite("Complex examples",
 Suites.push(new Suite("Test Templates",
     [
         {
-            url: "template/template-css.html?gain=1&addLimit=100&removeLimit=5",
+            url: "template/template-css.html",
             name: "CSS template"
         },
         {
-            url: "template/template-canvas.html?gain=1&addLimit=100&removeLimit=1000",
+            url: "template/template-canvas.html",
             name: "canvas template"
         },
         {
-            url: "template/template-svg.html?gain=1&addLimit=100&removeLimit=5&<other_paramter>=<value>",
+            url: "template/template-svg.html",
             name: "SVG template"
         },
     ]
index cf3ce76..9851074 100644 (file)
@@ -6,11 +6,11 @@ function BenchmarkState(testInterval)
 
 // The enum values and the messages should be in the same order
 BenchmarkState.stages = {
-    WARMING_UP: 0,
+    WARMING: 0,
     SAMPLING: 1,
     FINISHED: 2,
     messages: [ 
-        Strings.text.runningState.warmingup,
+        Strings.text.runningState.warming,
         Strings.text.runningState.sampling,
         Strings.text.runningState.finished
     ]
@@ -45,7 +45,7 @@ BenchmarkState.prototype =
     
     currentStage: function()
     {
-        for (var stage = BenchmarkState.stages.WARMING_UP; stage < BenchmarkState.stages.FINISHED; ++stage) {
+        for (var stage = BenchmarkState.stages.WARMING; stage < BenchmarkState.stages.FINISHED; ++stage) {
             if (this._currentTimeOffset < this._timeOffset(stage + 1))
                 return stage;
         }
@@ -136,11 +136,7 @@ function Benchmark(options)
     this._recordInterval = 200;    
     this._isSampling = false;
 
-    var gain = parseInt(this._options["gain"]) || 1;
-    var lowValue = -parseInt(this._options["addLimit"]) || 1;
-    var highValue = parseInt(this._options["removeLimit"]) || 1;
-    
-    this._controller = new PIDController(gain, this._options["frame-rate"], lowValue, highValue);
+    this._controller = new PIDController(this._options["frame-rate"]);
     this._sampler = new Sampler(2);
     this._state = new BenchmarkState(this._options["test-interval"] * 1000);
 }
@@ -177,7 +173,7 @@ Benchmark.prototype =
         else if (!(this._isSampling && this._options["fix-test-complexity"])) {
             // The relationship between frameRate and test complexity is inverse-proportional so we
             // need to use the negative of PIDController.tune() to change the complexity of the test.
-            tuneValue = -this._controller.tune(currentFrameRate, timeDelta / 1000);
+            tuneValue = -this._controller.tune(currentTimeOffset, timeDelta, currentFrameRate);
             tuneValue = tuneValue > 0 ? Math.floor(tuneValue) : Math.ceil(tuneValue);
         }
 
index f26f186..1b3ec09 100644 (file)
@@ -4,17 +4,17 @@ var Matrix =
     {
         return Array(m * n).fill(v);
     },
-    
+
     zeros: function(m, n)
     {
         return Matrix.init(m, n, 0);
     },
-    
+
     ones: function(m, n)
     {
         return Matrix.init(m, n, 1);
     },
-    
+
     identity: function(n)
     {
         var out = new Matrix.zeros(n, n);
@@ -22,7 +22,7 @@ var Matrix =
             out[i * n + i] = 1;
         return out;
     },
-    
+
     str: function(A, n, m)
     {
         var out = (m > 1 && n > 1 ? "Matrix[" + n + ", " + m : "Vector[" + m * n) + "] = [";
@@ -33,12 +33,12 @@ var Matrix =
         }       
         return out + "]";
     },
-    
+
     pos: function(m, i, j)
     {
         return m * i + j;
     },
-    
+
     add: function(A, B, n, m)
     {
         var out = Matrix.zeros(n, m);
@@ -46,7 +46,7 @@ var Matrix =
             out[i] = A[i] + B[i];
         return out;
     },
-    
+
     subtract: function(A, B, n, m)
     {
         var out = Matrix.zeros(n, m);
@@ -54,7 +54,7 @@ var Matrix =
             out[i] = A[i] - B[i];
         return out;
     },
-    
+
     scale: function(s, A, n, m)
     {
         var out = Matrix.zeros(n, m);
@@ -62,17 +62,17 @@ var Matrix =
             out[i] = s * A[i];
         return out;
     },
-    
+
     transpose: function(A, n, m)
     {
         var out = Matrix.zeros(m, n);
         for (var i = 0; i < n; ++i) {
             for (var j = 0; j < m; ++j)
                 out[Matrix.pos(n, i, j)] = A[Matrix.pos(m, j, i)];
-        }       
+        }
         return out;
     },
-    
+
     multiply: function(A, B, n, m, p)
     {
         var out = Matrix.zeros(n, p);
@@ -93,37 +93,37 @@ var Vector3 =
     {
         return Matrix.zeros(1, 3);
     },
-    
+
     ones: function()
     {
         return Matrix.ones(1, 3);
     },
-    
+
     str: function(v)
     {
         return Matrix.str(v, 1, 3);
     },
-    
+
     add: function(v, w)
     {
         return Matrix.add(v, w, 1, 3);
     },
-    
+
     subtract: function(v, w)
     {
         return Matrix.subtract(v, w, 1, 3);
     },
-    
+
     scale: function(s, v)
     {
         return Matrix.scale(s, v, 1, 3);
     },
-    
+
     multiplyMatrix3: function(v, A)
     {
         return Matrix.multiply(v, A, 1, 3, 3);
     },
-    
+
     multiplyVector3: function(v, w)
     {
         var out = 0;
@@ -139,114 +139,308 @@ var Matrix3 =
     {
         return Matrix.zeros(3, 3);
     },
-    
+
     identity: function()
     {
         return Matrix.identity(3, 3);
     },
-    
+
     str: function(A)
     {
         return Matrix.str(A, 3, 3);
     },
-    
+
     pos: function(i, j)
     {
         return Matrix.pos(3, i, j);
     },
-    
+
     add: function(A, B)
     {
         return Matrix.add(A, B, 3, 3);
     },
-    
+
     subtract: function(A, B)
     {
         return Matrix.subtract(A, B, 3, 3);
     },
-    
+
     scale: function(s, A)
     {
         return Matrix.scale(s, A, 3, 3);
     },
-    
+
     transpose: function(A)
     {
         return Matrix.transpose(A, 3, 3);
     },
-    
+
     multiplyMatrix3: function(A, B)
     {
         return Matrix.multiply(A, B, 3, 3, 3);
     },
-    
+
     multiplyVector3: function(A, v)
     {
         return Matrix.multiply(A, v, 3, 3, 1);
     }
 }
 
-function PIDController(K, ysp, ulow, uhigh)
+function PIDController(ysp)
 {
     this._ysp = ysp;
+    this._out = 0;
 
-    this._Td = 5;
-    this._Ti = 15;
-
-    this._Kp = K;
-    this._Kd = K / this._Td;
-    this._Ki = K / this._Ti;
-
+    this._Kp = 0;
+    this._stage = PIDController.stages.WARMING;
+    
     this._eold = 0;
     this._I = 0;
 }
 
+// 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.
+PIDController.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.
+PIDController.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.
+}
+
 PIDController.prototype =
 {
-    _sat: function(v, low, high)
+    // 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)
     {
-        return v < low ? low : (v > high ? high : v);
+        const epsilon = 0.0001;
+        var d  = y - this._y0;
+        return du < 0 ? Math.min(d, -epsilon) : Math.max(d, epsilon);
     },
-    
-    tune: function(y, h)
+
+    // 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)
     {
-        // Current error.
-        var e = this._ysp - 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._Kd * (e - this._eold) / h;
+        var D = (this._Kp * this._Td) * (e - this._eold) / h;
 
-        // Integral term is the area under the curve starting from the begining till the current time.
-        this._I += this._Ki * ((e + this._eold) / 2) * 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â\80Nichols 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);
 
-        // pid controller value.
-        var v = P + D + this._I;
+        case PIDController.stages.OVERSHOOT:
+            // This is the second stage of the Zieglerâ\80Nichols 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â\80Nichols 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);
+        }
         
-        // Avoid spikes by applying actuator saturation.
-        var u = this._sat(v, this._ulow, this._uhigh);        
+        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:
+            var out = this._out + u;
+            var min = Math.min(this._min, this._max * 0.70);
+            var max = this._max;
 
-        this._eold = e;
+            // 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);
     }
 }
 
 function KalmanEstimator(initX)
 {
-    // Initialize state transition matrix
+    // Initialize state transition matrix.
     this._matA = Matrix3.identity();
     this._matA[Matrix3.pos(0, 2)] = 1;
-    
-    // Initialize measurement matrix
+
+    // Initialize measurement matrix.
     this._vecH = Vector3.zeros();
     this._vecH[0] = 1;
-    
+
     this._matQ = Matrix3.identity();
     this._R = 1000;
-    
-    // Initial state conditions
+
+    // Initial state conditions.
     this._vecX_est = Vector3.zeros();
     this._vecX_est[0] = initX;
     this._matP_est = Matrix3.zeros();
@@ -259,7 +453,7 @@ KalmanEstimator.prototype =
         // Project the state ahead
         //  X_prd(k) = A * X_est(k-1)
         var vecX_prd = Matrix3.multiplyVector3(this._matA, this._vecX_est);
-        
+
         // Project the error covariance ahead
         //  P_prd(k) = A * P_est(k-1) * A' + Q
         var matP_prd = Matrix3.add(Matrix3.multiplyMatrix3(Matrix3.multiplyMatrix3(this._matA, this._matP_est), Matrix3.transpose(this._matA)), this._matQ);
@@ -271,11 +465,11 @@ KalmanEstimator.prototype =
         var vecB = Vector3.multiplyMatrix3(this._vecH, Matrix3.transpose(matP_prd));
         var S = Vector3.multiplyVector3(vecB, this._vecH) + this._R;
         var vecGain = Vector3.scale(1/S, vecB);
-        
+
         // Update the estimate via z(k)
         //  X_est(k) = x_prd + K(k) * (z(k) - H * X_prd(k));
         this._vecX_est = Vector3.add(vecX_prd, Vector3.scale(current - Vector3.multiplyVector3(this._vecH, vecX_prd), vecGain));
-        
+
         // Update the error covariance
         //  P_est(k) = P_prd(k) - K(k) * H * P_prd(k);
         this._matP_est = Matrix3.subtract(matP_prd, Matrix3.scale(Vector3.multiplyVector3(vecGain, this._vecH), matP_prd));
index 7bf2a83..53e9db3 100644 (file)
@@ -1,3 +1,46 @@
+2015-11-19  Said Abou-Hallawa  <sabouhallawa@apple.com>
+
+        Calculate the graphics benchmark test gain adaptively
+        https://bugs.webkit.org/show_bug.cgi?id=151208
+
+        Reviewed by Darin Adler.
+
+        We need to calculate the gain of the graphics benchmark tests adaptively
+        and get rid of the gain and limits parameters we have to choose manually
+        for every test. We are going to use the classic Ziegler–Nichols method for
+        calculating the gain and integral and derivative times. We are going to
+        try moving on a cubic curve during the manual stage from y0 to reach ysp.
+        We also going to use a saturation actuator to ensure the system does not
+        fluctuate.
+
+        * Animometer/resources/extensions.js:
+        (ResultsTable.prototype._isNoisyMeasurement): Fix a parameter name.
+        (ResultsTable.prototype._isNoisyTest): Since score is a member of testResults, we need to limit our search to frame rate and complexity.
+        (ResultsTable.prototype._showTest): Pass the correct parameter to _isNoisyMeasurement(). 
+        
+        * Animometer/resources/strings.js: Fix the indentation and name and value of a string.
+
+        * Animometer/runner/resources/tests.js: Remove all the manual gains and limits parameters which had to be passed to every test.
+        
+        * Animometer/tests/resources/main.js:
+        (BenchmarkState.prototype.currentStage): Fix an enum name.
+        (Benchmark): Get rid of manual gain and limits.
+        (Benchmark.prototype.update): Simplify the calculation by having all the times in ms.
+        
+        * Animometer/tests/resources/math.js:
+        (PIDController): Get rid of the manual gain and limits and the magic numbers for Ti and Td.
+        (PIDController.prototype._yPosition): Tells whether the test current output is moving towards the set-point or away from it.
+        (PIDController.prototype._distanceUltimate): Calculates the ultimate distance from y0 after time t using a cubic formula.
+        (PIDController.prototype._distance): Calculates the distance of y relative to y0.
+        (PIDController.prototype._gainIncrement): Decides how much the proportional gain should be increased during the manual gain stage.
+        (PIDController.prototype._updateStage): Updates the stage of the controller based on its current stage and the system output.
+        (PIDController.prototype._tuneP): Tunes the system before calculating the PID controller gains.
+        (PIDController.prototype._tunePID): PID tuning function.
+        (PIDController.prototype._tune):
+        (PIDController.prototype._saturate):
+        (PIDController.prototype.tune): Manages calculating the controller parameters. It then returns a PID tuning value.
+        (PIDController.prototype._sat): Deleted. We may need to return it back but the limits have to be calculated adaptively not manually. 
+
 2015-11-17  Said Abou-Hallawa  <sabouhallawa@apple.com>
 
         Reorganize the graphics benchmark string table