<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[198386] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/198386">198386</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-03-18 00:15:54 -0700 (Fri, 18 Mar 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add unit tests for config.json and statistics.js
https://bugs.webkit.org/show_bug.cgi?id=155626

Reviewed by Darin Adler.

Added mocha unit tests for statistics.js and validating config.json. For segmentations, I've extracted
real data from our internal perf dashboard.

Also fixed some bugs covered by these new tests.

* public/shared/statistics.js:
(Statistics.movingAverage): Fixed a bug that forwardWindowSize was never used.
(Statistics.exponentialMovingAverage): Fixed the bug that the moving average starts at 0. It should
start at the first value instead.
(.splitIntoSegmentsUntilGoodEnough): Fixed the bug that we may try to segment a time series into
more parts than there are data points. Clearly, that doesn't make any sense.
(.findOptimalSegmentation): Renamed local variables so that they're more descriptive, and rewrote
the debugging code was the old code was emitting some useless data. Also fixed the bug that the length
of &quot;segmentation&quot; was off by one (we need segmentCount + 1 elements in the array sine we always
include the start of the first segment = 0 and the end of the last segment = values.length).
(.SampleVarianceUpperTriangularMatrix):
(Statistics): Modernized the export code.
* tools/js: Added.
* tools/js/config.js: Added.
(Config): Added.
(Config.prototype.configFilePath): Added.
(Config.prototype.value): Added.
(Config.prototype.path): Added.
* tools/js/database.js: Added.
(Database): Added.
(Database.prototype.connect): Added.
(Database.prototype.disconnect): Added.
* unit-tests: Added.
* unit-tests/checkconfig.js: Added. Validates config.json. This is useful while setting up
a local instance of the perf dashboard.
* unit-tests/statistics-tests.js: Added.
(assert.almostEqual): Added. Asserts that two floating values are within a given significant digits.
(.stdev):
(.delta):
(.computeWelchsT):</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicsharedstatisticsjs">trunk/Websites/perf.webkit.org/public/shared/statistics.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>trunk/Websites/perf.webkit.org/tools/js/</li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsconfigjs">trunk/Websites/perf.webkit.org/tools/js/config.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsdatabasejs">trunk/Websites/perf.webkit.org/tools/js/database.js</a></li>
<li>trunk/Websites/perf.webkit.org/unit-tests/</li>
<li><a href="#trunkWebsitesperfwebkitorgunittestscheckconfigjs">trunk/Websites/perf.webkit.org/unit-tests/checkconfig.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsstatisticstestsjs">trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (198385 => 198386)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-03-18 07:09:46 UTC (rev 198385)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-03-18 07:15:54 UTC (rev 198386)
</span><span class="lines">@@ -1,5 +1,48 @@
</span><span class="cx"> 2016-03-17  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><ins>+        Add unit tests for config.json and statistics.js
+        https://bugs.webkit.org/show_bug.cgi?id=155626
+
+        Reviewed by Darin Adler.
+
+        Added mocha unit tests for statistics.js and validating config.json. For segmentations, I've extracted
+        real data from our internal perf dashboard.
+
+        Also fixed some bugs covered by these new tests.
+
+        * public/shared/statistics.js:
+        (Statistics.movingAverage): Fixed a bug that forwardWindowSize was never used.
+        (Statistics.exponentialMovingAverage): Fixed the bug that the moving average starts at 0. It should
+        start at the first value instead.
+        (.splitIntoSegmentsUntilGoodEnough): Fixed the bug that we may try to segment a time series into
+        more parts than there are data points. Clearly, that doesn't make any sense.
+        (.findOptimalSegmentation): Renamed local variables so that they're more descriptive, and rewrote
+        the debugging code was the old code was emitting some useless data. Also fixed the bug that the length
+        of &quot;segmentation&quot; was off by one (we need segmentCount + 1 elements in the array sine we always
+        include the start of the first segment = 0 and the end of the last segment = values.length).
+        (.SampleVarianceUpperTriangularMatrix):
+        (Statistics): Modernized the export code.
+        * tools/js: Added.
+        * tools/js/config.js: Added.
+        (Config): Added.
+        (Config.prototype.configFilePath): Added.
+        (Config.prototype.value): Added.
+        (Config.prototype.path): Added.
+        * tools/js/database.js: Added.
+        (Database): Added.
+        (Database.prototype.connect): Added.
+        (Database.prototype.disconnect): Added.
+        * unit-tests: Added.
+        * unit-tests/checkconfig.js: Added. Validates config.json. This is useful while setting up
+        a local instance of the perf dashboard.
+        * unit-tests/statistics-tests.js: Added.
+        (assert.almostEqual): Added. Asserts that two floating values are within a given significant digits.
+        (.stdev):
+        (.delta):
+        (.computeWelchsT):
+
+2016-03-17  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
</ins><span class="cx">         Fix a typo which was supposed to be fixed in r198351.
</span><span class="cx"> 
</span><span class="cx">         * public/v3/pages/analysis-task-page.js:
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicsharedstatisticsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/shared/statistics.js (198385 => 198386)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/shared/statistics.js        2016-03-18 07:09:46 UTC (rev 198385)
+++ trunk/Websites/perf.webkit.org/public/shared/statistics.js        2016-03-18 07:15:54 UTC (rev 198386)
</span><span class="lines">@@ -122,7 +122,7 @@
</span><span class="cx">         for (var i = 0; i &lt; values.length; i++) {
</span><span class="cx">             var sum = 0;
</span><span class="cx">             var count = 0;
</span><del>-            for (var j = i - backwardWindowSize; j &lt; i + backwardWindowSize; j++) {
</del><ins>+            for (var j = i - backwardWindowSize; j &lt;= i + forwardWindowSize; j++) {
</ins><span class="cx">                 if (j &gt;= 0 &amp;&amp; j &lt; values.length) {
</span><span class="cx">                     sum += values[j];
</span><span class="cx">                     count++;
</span><span class="lines">@@ -145,8 +145,9 @@
</span><span class="cx"> 
</span><span class="cx">     this.exponentialMovingAverage = function (values, smoothingFactor) {
</span><span class="cx">         var averages = new Array(values.length);
</span><del>-        var movingAverage = 0;
-        for (var i = 0; i &lt; values.length; i++) {
</del><ins>+        var movingAverage = values[0];
+        averages[0] = movingAverage;
+        for (var i = 1; i &lt; values.length; i++) {
</ins><span class="cx">             movingAverage = smoothingFactor * values[i] + (1 - smoothingFactor) * movingAverage;
</span><span class="cx">             averages[i] = movingAverage;
</span><span class="cx">         }
</span><span class="lines">@@ -280,7 +281,7 @@
</span><span class="cx"> 
</span><span class="cx">         var segmentation;
</span><span class="cx">         var minTotalCost = Infinity;
</span><del>-        var maxK = 50;
</del><ins>+        var maxK = Math.min(50, values.length);
</ins><span class="cx"> 
</span><span class="cx">         for (var k = 1; k &lt; maxK; k++) {
</span><span class="cx">             var start = Date.now();
</span><span class="lines">@@ -302,10 +303,10 @@
</span><span class="cx">     function findOptimalSegmentation(values, costMatrix, segmentCount) {
</span><span class="cx">         // Dynamic programming. cost[i][k] = The cost to segmenting values up to i into k segments.
</span><span class="cx">         var cost = new Array(values.length);
</span><del>-        for (var i = 0; i &lt; values.length; i++) {
-            cost[i] = new Float32Array(segmentCount + 1);
-        }
</del><ins>+        for (var segmentEnd = 0; segmentEnd &lt; values.length; segmentEnd++)
+            cost[segmentEnd] = new Float32Array(segmentCount + 1);
</ins><span class="cx"> 
</span><ins>+        // previousNode[i][k] = The start of the last segment in an optimal segmentation that ends at i with k segments.
</ins><span class="cx">         var previousNode = new Array(values.length);
</span><span class="cx">         for (var i = 0; i &lt; values.length; i++)
</span><span class="cx">             previousNode[i] = new Array(segmentCount + 1);
</span><span class="lines">@@ -313,45 +314,48 @@
</span><span class="cx">         cost[0] = [0]; // The cost of segmenting single value is always 0.
</span><span class="cx">         previousNode[0] = [-1];
</span><span class="cx">         for (var segmentStart = 0; segmentStart &lt; values.length; segmentStart++) {
</span><del>-            var costBySegment = cost[segmentStart];
-            for (var count = 0; count &lt; segmentCount; count++) {
-                if (previousNode[segmentStart][count] === undefined)
</del><ins>+            var costOfOptimalSegmentationThatEndAtCurrentStart = cost[segmentStart];
+            for (var k = 0; k &lt; segmentCount; k++) {
+                var noSegmentationOfLenghtKEndsAtCurrentStart = previousNode[segmentStart][k] === undefined;
+                if (noSegmentationOfLenghtKEndsAtCurrentStart)
</ins><span class="cx">                     continue;
</span><span class="cx">                 for (var segmentEnd = segmentStart + 1; segmentEnd &lt; values.length; segmentEnd++) {
</span><del>-                    var newCost = costBySegment[count] + costMatrix.costBetween(segmentStart, segmentEnd);
-                    if (previousNode[segmentEnd][count + 1] === undefined || newCost &lt; cost[segmentEnd][count + 1]) {
-                        cost[segmentEnd][count + 1] = newCost;
-                        previousNode[segmentEnd][count + 1] = segmentStart;
</del><ins>+                    var costOfOptimalSegmentationOfLengthK = costOfOptimalSegmentationThatEndAtCurrentStart[k];
+                    var costOfCurrentSegment = costMatrix.costBetween(segmentStart, segmentEnd);
+                    var totalCost = costOfOptimalSegmentationOfLengthK + costOfCurrentSegment;
+                    if (previousNode[segmentEnd][k + 1] === undefined || totalCost &lt; cost[segmentEnd][k + 1]) {
+                        cost[segmentEnd][k + 1] = totalCost;
+                        previousNode[segmentEnd][k + 1] = segmentStart;
</ins><span class="cx">                     }
</span><span class="cx">                 }
</span><span class="cx">             }
</span><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         if (Statistics.debuggingSegmentation) {
</span><del>-            console.log('findOptimalSegmentation with k=', segmentCount);
-            for (var i = 0; i &lt; cost.length; i++) {
-                var t = cost[i];
-                var s = '';
-                for (var j = 0; j &lt; t.length; j++) {
-                    var p = previousNode[i][j];
-                    s += '(k=' + j;
-                    if (p !== undefined)
-                        s += ' c=' + t[j] + ' p=' + p
-                    s += ')';
</del><ins>+            console.log('findOptimalSegmentation with', segmentCount, 'segments');
+            for (var end = 0; end &lt; values.length; end++) {
+                for (var k = 0; k &lt;= segmentCount; k++) {
+                    var start = previousNode[end][k];
+                    if (start === undefined)
+                        continue;
+                    console.log(`C(segment=[${start}, ${end + 1}], segmentCount=${k})=${cost[end][k]}`);
</ins><span class="cx">                 }
</span><del>-                console.log(i, values[i], s);
</del><span class="cx">             }
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        var currentIndex = values.length - 1;
-        var segmentation = new Array(segmentCount);
-        segmentation[0] = values.length;
-        for (var i = 0; i &lt; segmentCount; i++) {
-            currentIndex = previousNode[currentIndex][segmentCount - i];
-            segmentation[i + 1] = currentIndex;
</del><ins>+        var segmentEnd = values.length - 1;
+        var segmentation = new Array(segmentCount + 1);
+        segmentation[segmentCount] = values.length;
+        for (var k = segmentCount; k &gt; 0; k--) {
+            segmentEnd = previousNode[segmentEnd][k];
+            segmentation[k - 1] = segmentEnd;
</ins><span class="cx">         }
</span><ins>+        var costOfOptimalSegmentation = cost[values.length - 1][segmentCount];
</ins><span class="cx"> 
</span><del>-        return {segmentation: segmentation.reverse(), cost: cost[values.length - 1][segmentCount]};
</del><ins>+        if (Statistics.debuggingSegmentation)
+            console.log('Optimal segmentation:', segmentation, 'with cost =', costOfOptimalSegmentation);
+
+        return {segmentation: segmentation, cost: costOfOptimalSegmentation};
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     function SampleVarianceUpperTriangularMatrix(values) {
</span><span class="lines">@@ -385,7 +389,5 @@
</span><span class="cx"> 
</span><span class="cx"> })();
</span><span class="cx"> 
</span><del>-if (typeof module != 'undefined') {
-    for (var key in Statistics)
-        module.exports[key] = Statistics[key];
-}
</del><ins>+if (typeof module != 'undefined')
+    module.exports = Statistics;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsconfigjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/tools/js/config.js (0 => 198386)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/config.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/tools/js/config.js        2016-03-18 07:15:54 UTC (rev 198386)
</span><span class="lines">@@ -0,0 +1,38 @@
</span><ins>+&quot;use strict&quot;;
+
+var fs = require('fs');
+var path = require('path');
+
+var Config = new (class Config {
+    constructor()
+    {
+        this._rootDirectory =  path.resolve(__dirname, '../../'); 
+        this._configPath = path.resolve(this._rootDirectory, 'config.json');
+        this._content = null;
+    }
+
+    configFilePath() { return this._configPath; }
+
+    value(key)
+    {
+        if (!this._content)
+            this._content = JSON.parse(fs.readFileSync(this._configPath));
+
+        let content = this._content;
+        for (var key of key.split('.')) {
+            if (!(key in content))
+                return null;
+            content = content[key];
+        }
+
+        return content;
+    }
+
+    path(key)
+    {
+        return path.resolve(this._rootDirectory, this.value(key));
+    }
+});
+
+if (typeof module != 'undefined')
+    module.exports = Config;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsdatabasejs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/tools/js/database.js (0 => 198386)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/database.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/tools/js/database.js        2016-03-18 07:15:54 UTC (rev 198386)
</span><span class="lines">@@ -0,0 +1,53 @@
</span><ins>+&quot;use strict&quot;;
+
+var pg = require('pg');
+var config = require('./config.js');
+
+class Database {
+    constructor()
+    {
+        this._client = null;
+    }
+
+    connect(options)
+    {
+        console.assert(this._client === null);
+
+        let username = config.value('database.username');
+        let password = config.value('database.password');
+        let host = config.value('database.host');
+        let port = config.value('database.port');
+        let name = config.value('database.name');
+
+        // No need to worry about escaping strings since they are only set by someone who can write to config.json.
+        let connectionString = `tcp://${username}:${password}@${host}:${port}/${name}`;
+
+        let client = new pg.Client(connectionString);
+        if (!options || !options.keepAlive) {
+            client.on('drain', function () {
+                client.end();
+            });
+        }
+
+        this._client = client;
+
+        return new Promise(function (resolve, reject) {
+            client.connect(function (error) {
+                if (error)
+                    reject(error);
+                resolve();
+            });
+        });
+    }
+
+    disconnect()
+    {
+        if (this._client) {
+            this._client.end();
+            this._client = null;
+        }
+    }
+}
+
+if (typeof module != 'undefined')
+    module.exports = Database;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestscheckconfigjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/checkconfig.js (0 => 198386)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/checkconfig.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/checkconfig.js        2016-03-18 07:15:54 UTC (rev 198386)
</span><span class="lines">@@ -0,0 +1,169 @@
</span><ins>+&quot;use strict&quot;;
+
+var assert = require('assert');
+var fs = require('fs');
+var pg = require('pg');
+
+var Config = require('../tools/js/config.js');
+var Database = require('../tools/js/database.js');
+
+describe('config.json', function () {
+    it('should be a valid file', function () {
+        assert.doesNotThrow(function () {
+            fs.readFileSync(Config.configFilePath())
+        });
+    });
+
+    it('should be a valid JSON', function () {
+        assert.doesNotThrow(function () {
+            JSON.parse(fs.readFileSync(Config.configFilePath()));
+        });
+    });
+
+    it('should define `siteTitle`', function () {
+        assert.equal(typeof Config.value('siteTitle'), 'string');
+    });
+
+    it('should define `dataDirectory`', function () {
+        assert.ok(Config.value('dataDirectory'));
+        assert.ok(fs.existsSync(Config.path('dataDirectory')));
+        assert.ok(fs.statSync(Config.path('dataDirectory')).isDirectory());
+    });
+
+    it('should define `jsonCacheMaxAge`', function () {
+        assert.equal(typeof Config.value('jsonCacheMaxAge'), 'number');
+    });
+
+    it('should define `jsonCacheMaxAge`', function () {
+        assert.equal(typeof Config.value('jsonCacheMaxAge'), 'number');
+    });
+
+    it('should define `clusterStart`', function () {
+        var clusterStart = Config.value('clusterStart');
+        assert.ok(clusterStart instanceof Array);
+        assert.equal(clusterStart.length, [2000, 1, 1, 0, 0].length,
+            'Must specify year, month, date, hour, and minute');
+        var maxYear = (new Date).getFullYear() + 1;
+        assert.ok(clusterStart[0] &gt;= 1970 &amp;&amp; clusterStart[0] &lt;= maxYear, `year must be between 1970 and ${maxYear}`);
+        assert.ok(clusterStart[1] &gt;= 1 &amp;&amp; clusterStart[1] &lt;= 12, 'month must be between 1 and 12');
+        assert.ok(clusterStart[2] &gt;= 1 &amp;&amp; clusterStart[2] &lt;= 31, 'date must be between 1 and 31');
+        assert.ok(clusterStart[3] &gt;= 0 &amp;&amp; clusterStart[3] &lt;= 60, 'minute must be between 0 and 60');
+        assert.ok(clusterStart[4] &gt;= 0 &amp;&amp; clusterStart[4] &lt;= 60, 'minute must be between 0 and 60');
+    });
+
+    it('should define `clusterSize`', function () {
+        var clusterSize = Config.value('clusterSize');
+        assert.ok(clusterSize instanceof Array);
+        assert.equal(clusterSize.length, [0, 2, 0].length,
+            'Must specify the number of years, months, and days');
+        assert.equal(typeof clusterSize[0], 'number', 'the number of year must be a number');
+        assert.equal(typeof clusterSize[1], 'number', 'the number of month must be a number');
+        assert.equal(typeof clusterSize[2], 'number', 'the number of days must be a number');
+    });
+
+    describe('`dashboards`', function () {
+        var dashboards = Config.value('dashboards');
+
+        it('should exist for v2 and v3 UI', function () {
+            assert.equal(typeof dashboards, 'object');
+        });
+
+        it('dashboard names that do not contain /', function () {
+            for (var name in dashboards)
+                assert.ok(name.indexOf('/') &lt; 0, 'Dashboard name &quot;${name}&quot; should not contain &quot;/&quot;');
+        });
+
+        it('each dashboard must be an array', function () {
+            for (var name in dashboards)
+                assert.ok(dashboards[name] instanceof Array);
+        });
+
+        it('each row in a dashboard must be an array', function () {
+            for (var name in dashboards) {
+                for (var row of dashboards[name]) {
+                    console.assert(row instanceof Array);
+                }
+            }
+        });
+
+        it('each cell in a dashboard must be an array or a string', function () {
+            for (var name in dashboards) {
+                for (var row of dashboards[name]) {
+                    for (var cell of row) {
+                        if (cell instanceof Array)
+                            assert.ok(cell.length == 0 || cell.length == 2,
+                                'Each cell must be empty or specify [platform, metric] pair');
+                        else
+                            assert.equal(typeof cell, 'string');
+                    }
+                }
+            }
+        });
+
+    });
+
+    describe('`database`', function () {
+        it('should exist', function () {
+            assert.ok(Config.value('database'));
+        });
+
+        it('should define `database.host`', function () {
+            assert.equal(typeof Config.value('database.host'), 'string');
+        });
+
+        it('should define `database.port`', function () {
+            assert.equal(typeof Config.value('database.port'), 'string');
+        });
+
+        it('should define `database.username`', function () {
+            assert.equal(typeof Config.value('database.username'), 'string');
+        });
+
+        it('should define `database.password`', function () {
+            assert.equal(typeof Config.value('database.password'), 'string');
+        });
+
+        it('should define `database.name`', function () {
+            assert.equal(typeof Config.value('database.name'), 'string');
+        });
+
+        it('should be able to connect to the database', function (done) {
+            let database = new Database;
+            return database.connect().then(function () {
+                database.disconnect();
+                done();
+            }, function (error) {
+                database.disconnect();
+                done(error);
+            });
+        });
+    });
+
+    describe('optional configurations', function () {
+        function assertNullOrType(value, type) {
+            if (value !== null)
+                assert.equal(typeof value, type);
+        }
+
+        it('`debug` should be `null` or a boolean', function () {
+            assertNullOrType(Config.value('debug'), 'boolean');
+        });
+
+        it('`maintenanceMode` should be `null` or a boolean', function () {
+            assertNullOrType(Config.value('maintenanceMode'), 'boolean');
+        });
+
+        it('`maintenanceDirectory` should be `null` or a string', function () {
+            assertNullOrType(Config.value('maintenanceDirectory'), 'string');
+        });
+
+        it('`maintenanceDirectory` should be a string if `maintenanceMode` is true', function () {
+            if (Config.value('maintenanceMode'))
+                assert.equal(Config.value('maintenanceDirectory'), 'string');
+        });
+
+        it('`universalSlavePassword` should be `null` or a string', function () {
+            assertNullOrType(Config.value('universalSlavePassword'), 'string');
+        });
+    });
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsstatisticstestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js (0 => 198386)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/statistics-tests.js        2016-03-18 07:15:54 UTC (rev 198386)
</span><span class="lines">@@ -0,0 +1,399 @@
</span><ins>+&quot;use strict&quot;;
+
+var assert = require('assert');
+var Statistics = require('../public/shared/statistics.js');
+
+if (!assert.almostEqual) {
+    assert.almostEqual = function (actual, expected, precision, message) {
+        var suffiedMessage = (message ? message + ' ' : '');
+        if (isNaN(expected)) {
+            assert(isNaN(actual), `${suffiedMessage}expected NaN but got ${actual}`);
+            return;
+        }
+
+        if (expected == 0) {
+            assert.equal(actual, expected, message);
+            return;
+        }
+
+        if (!precision)
+            precision = 6;
+        var tolerance = 1 / Math.pow(10, precision);
+        var relativeDifference = Math.abs((actual - expected) / expected);
+        var percentDifference = (relativeDifference * 100).toFixed(2);
+        assert(relativeDifference &lt; tolerance,
+            `${suffiedMessage}expected ${expected} but got ${actual} (${percentDifference}% difference)`);
+    }
+}
+
+describe('assert.almostEqual', function () {
+    it('should not throw when values are identical', function () {
+        assert.doesNotThrow(function () { assert.almostEqual(1, 1); });
+    });
+
+    it('should not throw when values are close', function () {
+        assert.doesNotThrow(function () { assert.almostEqual(1.10, 1.107, 2); });
+        assert.doesNotThrow(function () { assert.almostEqual(1256.7, 1256.72, 4); });
+    });
+
+    it('should throw when values are not close', function () {
+        assert.throws(function () { assert.almostEqual(1.10, 1.27, 2); });
+        assert.throws(function () { assert.almostEqual(735.4, 735.6, 4); });
+    });
+});
+
+describe('Statistics', function () {
+    describe('min', function () {
+        it('should find the mininum value', function () {
+            assert.equal(Statistics.min([1, 2, 3, 4]), 1);
+            assert.equal(Statistics.min([4, 3, 2, 1]), 1);
+            assert.equal(Statistics.min([2000, 20, 200]), 20);
+            assert.equal(Statistics.min([0.3, 0.06, 0.5]), 0.06);
+            assert.equal(Statistics.min([-0.3, 0.06, 0.5]), -0.3);
+            assert.equal(Statistics.min([-0.3, 0.06, 0.5, Infinity]), -0.3);
+            assert.equal(Statistics.min([-0.3, 0.06, 0.5, -Infinity]), -Infinity);
+            assert.equal(Statistics.min([]), Infinity);
+        });
+    });
+
+    describe('max', function () {
+        it('should find the mininum value', function () {
+            assert.equal(Statistics.max([1, 2, 3, 4]), 4);
+            assert.equal(Statistics.max([4, 3, 2, 1]), 4);
+            assert.equal(Statistics.max([2000, 20, 200]), 2000);
+            assert.equal(Statistics.max([0.3, 0.06, 0.5]), 0.5);
+            assert.equal(Statistics.max([-0.3, 0.06, 0.5]), 0.5);
+            assert.equal(Statistics.max([-0.3, 0.06, 0.5, Infinity]), Infinity);
+            assert.equal(Statistics.max([-0.3, 0.06, 0.5, -Infinity]), 0.5);
+            assert.equal(Statistics.max([]), -Infinity);
+        });
+    });
+
+    describe('sum', function () {
+        it('should find the sum of values', function () {
+            assert.equal(Statistics.sum([1, 2, 3, 4]), 10);
+            assert.equal(Statistics.sum([4, 3, 2, 1]), 10);
+            assert.equal(Statistics.sum([2000, 20, 200]), 2220);
+            assert.equal(Statistics.sum([0.3, 0.06, 0.5]), 0.86);
+            assert.equal(Statistics.sum([-0.3, 0.06, 0.5]), 0.26);
+            assert.equal(Statistics.sum([-0.3, 0.06, 0.5, Infinity]), Infinity);
+            assert.equal(Statistics.sum([-0.3, 0.06, 0.5, -Infinity]), -Infinity);
+            assert.equal(Statistics.sum([]), 0);
+        });
+    });
+
+    describe('squareSum', function () {
+        it('should find the square sum of values', function () {
+            assert.equal(Statistics.squareSum([1, 2, 3, 4]), 30);
+            assert.equal(Statistics.squareSum([4, 3, 2, 1]), 30);
+            assert.equal(Statistics.squareSum([2000, 20, 200]), 2000 * 2000 + 20 * 20 + 200* 200);
+            assert.equal(Statistics.squareSum([0.3, 0.06, 0.5]), 0.09 + 0.0036 + 0.25);
+            assert.equal(Statistics.squareSum([-0.3, 0.06, 0.5]), 0.09 + 0.0036 + 0.25);
+            assert.equal(Statistics.squareSum([-0.3, 0.06, 0.5, Infinity]), Infinity);
+            assert.equal(Statistics.squareSum([-0.3, 0.06, 0.5, -Infinity]), Infinity);
+            assert.equal(Statistics.squareSum([]), 0);
+        });
+    });
+
+    describe('sampleStandardDeviation', function () {
+        function stdev(values) {
+            return Statistics.sampleStandardDeviation(values.length,
+                Statistics.sum(values), Statistics.squareSum(values));
+        }
+
+        it('should find the standard deviation of values', function () {
+            assert.almostEqual(stdev([1, 2, 3, 4]), 1.2909944);
+            assert.almostEqual(stdev([4, 3, 2, 1]), 1.2909944);
+            assert.almostEqual(stdev([2000, 20, 200]), 1094.89726);
+            assert.almostEqual(stdev([0.3, 0.06, 0.5]), 0.220302822);
+            assert.almostEqual(stdev([-0.3, 0.06, 0.5]), 0.40066611203);
+            assert.almostEqual(stdev([-0.3, 0.06, 0.5, Infinity]), NaN);
+            assert.almostEqual(stdev([-0.3, 0.06, 0.5, -Infinity]), NaN);
+            assert.almostEqual(stdev([]), 0);
+        });
+    });
+
+    describe('confidenceIntervalDelta', function () {
+        it('should find the p-value of values using Student\'s t distribution', function () {
+            function delta(values, probabilty) {
+                return Statistics.confidenceIntervalDelta(probabilty, values.length,
+                    Statistics.sum(values), Statistics.squareSum(values));
+            }
+
+            // https://onlinecourses.science.psu.edu/stat414/node/199
+            var values = [118, 115, 125, 110, 112, 130, 117, 112, 115, 120, 113, 118, 119, 122, 123, 126];
+            assert.almostEqual(delta(values, 0.95), 3.015, 3);
+
+            // Following values are computed using Excel Online's STDEV and CONFIDENCE.T
+            assert.almostEqual(delta([1, 2, 3, 4], 0.8), 1.057159);
+            assert.almostEqual(delta([1, 2, 3, 4], 0.9), 1.519090);
+            assert.almostEqual(delta([1, 2, 3, 4], 0.95), 2.054260);
+
+            assert.almostEqual(delta([0.3, 0.06, 0.5], 0.8), 0.2398353);
+            assert.almostEqual(delta([0.3, 0.06, 0.5], 0.9), 0.3713985);
+            assert.almostEqual(delta([0.3, 0.06, 0.5], 0.95), 0.5472625);
+
+            assert.almostEqual(delta([-0.3, 0.06, 0.5], 0.8), 0.4361900);
+            assert.almostEqual(delta([-0.3, 0.06, 0.5], 0.9), 0.6754647);
+            assert.almostEqual(delta([-0.3, 0.06, 0.5], 0.95), 0.9953098);
+
+            assert.almostEqual(delta([123, 107, 109, 104, 111], 0.8), 5.001167);
+            assert.almostEqual(delta([123, 107, 109, 104, 111], 0.9), 6.953874);
+            assert.almostEqual(delta([123, 107, 109, 104, 111], 0.95), 9.056490);
+
+            assert.almostEqual(delta([6785, 7812, 6904, 7503, 6943, 7207, 6812], 0.8), 212.6155);
+            assert.almostEqual(delta([6785, 7812, 6904, 7503, 6943, 7207, 6812], 0.9), 286.9585);
+            assert.almostEqual(delta([6785, 7812, 6904, 7503, 6943, 7207, 6812], 0.95), 361.3469);
+
+        });
+    });
+
+    // https://en.wikipedia.org/wiki/Welch%27s_t_test
+
+    var example1 = {
+        A1: [27.5, 21.0, 19.0, 23.6, 17.0, 17.9, 16.9, 20.1, 21.9, 22.6, 23.1, 19.6, 19.0, 21.7, 21.4],
+        A2: [27.1, 22.0, 20.8, 23.4, 23.4, 23.5, 25.8, 22.0, 24.8, 20.2, 21.9, 22.1, 22.9, 20.5, 24.4],
+        expectedT: 2.46,
+        expectedDegreesOfFreedom: 25.0,
+        expectedRange: [0.95, 0.98] // P = 0.021 so 1 - P = 0.979 is between 0.95 and 0.98
+    };
+
+    var example2 = {
+        A1: [17.2, 20.9, 22.6, 18.1, 21.7, 21.4, 23.5, 24.2, 14.7, 21.8],
+        A2: [21.5, 22.8, 21.0, 23.0, 21.6, 23.6, 22.5, 20.7, 23.4, 21.8, 20.7, 21.7, 21.5, 22.5, 23.6, 21.5, 22.5, 23.5, 21.5, 21.8],
+        expectedT: 1.57,
+        expectedDegreesOfFreedom: 9.9,
+        expectedRange: [0.8, 0.9] // P = 0.149 so 1 - P = 0.851 is between 0.8 and 0.9
+    };
+
+    var example3 = {
+        A1: [19.8, 20.4, 19.6, 17.8, 18.5, 18.9, 18.3, 18.9, 19.5, 22.0],
+        A2: [28.2, 26.6, 20.1, 23.3, 25.2, 22.1, 17.7, 27.6, 20.6, 13.7, 23.2, 17.5, 20.6, 18.0, 23.9, 21.6, 24.3, 20.4, 24.0, 13.2],
+        expectedT: 2.22,
+        expectedDegreesOfFreedom: 24.5,
+        expectedRange: [0.95, 0.98] // P = 0.036 so 1 - P = 0.964 is beteween 0.95 and 0.98
+    };
+
+    describe('computeWelchsT', function () {
+        function computeWelchsT(values1, values2, probability) {
+            return Statistics.computeWelchsT(values1, 0, values1.length, values2, 0, values2.length, probability);
+        }
+
+        it('should detect the statistically significant difference using Welch\'s t-test', function () {
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.8).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.9).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.95).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example1.A1, example1.A2, 0.98).significantlyDifferent, false);
+
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.8).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.9).significantlyDifferent, false);
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.95).significantlyDifferent, false);
+            assert.equal(computeWelchsT(example2.A1, example2.A2, 0.98).significantlyDifferent, false);
+
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.8).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.9).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.95).significantlyDifferent, true);
+            assert.equal(computeWelchsT(example3.A1, example3.A2, 0.98).significantlyDifferent, false);
+        });
+
+        it('should find the t-value of values using Welch\'s t-test', function () {
+            assert.almostEqual(computeWelchsT(example1.A1, example1.A2).t, example1.expectedT, 2);
+            assert.almostEqual(computeWelchsT(example2.A1, example2.A2).t, example2.expectedT, 2);
+            assert.almostEqual(computeWelchsT(example3.A1, example3.A2).t, example3.expectedT, 2);
+        });
+
+        it('should find the degreees of freedom using Welch–Satterthwaite equation', function () {
+            assert.almostEqual(computeWelchsT(example1.A1, example1.A2).degreesOfFreedom, example1.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(computeWelchsT(example2.A1, example2.A2).degreesOfFreedom, example2.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(computeWelchsT(example3.A1, example3.A2).degreesOfFreedom, example3.expectedDegreesOfFreedom, 2);
+        });
+
+        it('should respect the start and the end indices', function () {
+            var A1 = example2.A1.slice();
+            var A2 = example2.A2.slice();
+
+            var expectedT = Statistics.computeWelchsT(A1, 0, A1.length, A2, 0, A2.length).t;
+
+            A1.unshift(21);
+            A1.push(15);
+            A1.push(24);
+            assert.almostEqual(Statistics.computeWelchsT(A1, 1, A1.length - 3, A2, 0, A2.length).t, expectedT);
+
+            A2.unshift(24.3);
+            A2.unshift(25.8);
+            A2.push(23);
+            A2.push(24);
+            A2 = A2.reverse();
+            assert.almostEqual(Statistics.computeWelchsT(A1, 1, A1.length - 3, A2, 2, A2.length - 4).t, expectedT);
+        });
+    });
+
+    describe('probabilityRangeForWelchsT', function () {
+        it('should find the t-value of values using Welch\'s t-test', function () {
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).t, example1.expectedT, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).t, example2.expectedT, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).t, example3.expectedT, 2);
+        });
+
+        it('should find the degreees of freedom using Welch–Satterthwaite equation', function () {
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).degreesOfFreedom,
+                example1.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).degreesOfFreedom,
+                example2.expectedDegreesOfFreedom, 2);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).degreesOfFreedom,
+                example3.expectedDegreesOfFreedom, 2);
+        });
+
+        it('should compute the range of probabilites using the p-value of Welch\'s t-test', function () {
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).range[0], example1.expectedRange[0]);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example1.A1, example1.A2).range[1], example1.expectedRange[1]);
+
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).range[0], example2.expectedRange[0]);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example2.A1, example2.A2).range[1], example2.expectedRange[1]);
+
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).range[0], example3.expectedRange[0]);
+            assert.almostEqual(Statistics.probabilityRangeForWelchsT(example3.A1, example3.A2).range[1], example3.expectedRange[1]);
+        });
+    });
+
+    describe('movingAverage', function () {
+        it('should return the origian values when both forward and backward window size is 0', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 0), [1, 2, 3, 4, 5]);
+        });
+
+        it('should find the moving average with a positive backward window', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 1, 0),
+                [1, (1 + 2) / 2, (2 + 3) / 2, (3 + 4) / 2, (4 + 5) / 2]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (2 + 3 + 4) / 3, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 3, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 4, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 5, 0),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5]);
+        });
+
+        it('should find the moving average with a positive forward window', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 1),
+                [(1 + 2) / 2, (2 + 3) / 2, (3 + 4) / 2, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 2),
+                [(1 + 2 + 3) / 3, (2 + 3 + 4) / 3, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 3),
+                [(1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 4),
+                [(1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 0, 5),
+                [(1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2, 5]);
+        });
+
+        it('should find the moving average when both backward and forward window sizes are specified', function () {
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 1, 1),
+                [(1 + 2) / 2, (1 + 2 + 3) / 3, (2 + 3 + 4) / 3, (3 + 4 + 5) / 3, (4 + 5) / 2]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 1, 2),
+                [(1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3, (4 + 5) / 2]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 1),
+                [(1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 2),
+                [(1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 2, 3),
+                [(1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4, (3 + 4 + 5) / 3]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 3, 2),
+                [(1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4]);
+
+            assert.deepEqual(Statistics.movingAverage([1, 2, 3, 4, 5], 3, 3),
+                [(1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (1 + 2 + 3 + 4 + 5) / 5, (2 + 3 + 4 + 5) / 4]);
+        });
+    });
+
+    describe('cumulativeMovingAverage', function () {
+        it('should find the cumulative moving average', function () {
+            assert.deepEqual(Statistics.cumulativeMovingAverage([1, 2, 3, 4, 5]),
+                [1, (1 + 2) / 2, (1 + 2 + 3) / 3, (1 + 2 + 3 + 4) / 4, (1 + 2 + 3 + 4 + 5) / 5]);
+
+            assert.deepEqual(Statistics.cumulativeMovingAverage([-1, 7, 0, 8.5, 2]),
+                [-1, (-1 + 7) / 2, (-1 + 7 + 0) / 3, (-1 + 7 + 0 + 8.5) / 4, (-1 + 7 + 0 + 8.5 + 2) / 5]);
+        });
+    });
+
+    describe('exponentialMovingAverage', function () {
+        it('should find the exponential moving average', function () {
+            var averages = Statistics.exponentialMovingAverage([1, 2, 3, 4, 5], 0.2);
+            assert.equal(averages[0], 1);
+            assert.almostEqual(averages[1], 0.2 * 2 + 0.8 * averages[0]);
+            assert.almostEqual(averages[2], 0.2 * 3 + 0.8 * averages[1]);
+            assert.almostEqual(averages[3], 0.2 * 4 + 0.8 * averages[2]);
+            assert.almostEqual(averages[4], 0.2 * 5 + 0.8 * averages[3]);
+
+            averages = Statistics.exponentialMovingAverage([0.8, -0.2, 0.4, -0.3, 0.5], 0.1);
+            assert.almostEqual(averages[0], 0.8);
+            assert.almostEqual(averages[1], 0.1 * -0.2 + 0.9 * averages[0]);
+            assert.almostEqual(averages[2], 0.1 * 0.4 + 0.9 * averages[1]);
+            assert.almostEqual(averages[3], 0.1 * -0.3 + 0.9 * averages[2]);
+            assert.almostEqual(averages[4], 0.1 * 0.5 + 0.9 * averages[3]);
+        });
+    });
+
+    describe('segmentTimeSeriesGreedyWithStudentsTTest', function () {
+        it('should segment time series', function () {
+            assert.deepEqual(Statistics.segmentTimeSeriesGreedyWithStudentsTTest([1, 1, 1, 3, 3, 3], 1), [0, 2, 6]);
+            assert.deepEqual(Statistics.segmentTimeSeriesGreedyWithStudentsTTest([1, 1.2, 0.9, 1.1, 1.5, 1.7, 1.8], 1), [0, 4, 7]);
+        });
+    });
+
+    describe('segmentTimeSeriesByMaximizingSchwarzCriterion', function () {
+        it('should not segment time series of length two into two pieces', function () {
+            var values = [1, 2];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 2]);
+        });
+
+        it('should segment time series [1, 2, 3] into three pieces', function () {
+            var values = [1, 2, 3];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 1, 3]);
+        });
+
+        it('should segment time series for platform=47 metric=4875 between 1453938553772 and 1454630903100 into two parts', function () {
+            var values = [
+                1546.5603, 1548.1536, 1563.5452, 1539.7823, 1546.4184, 1548.9299, 1532.5444, 1546.2800, 1547.1760, 1551.3507,
+                1548.3277, 1544.7673, 1542.7157, 1538.1700, 1538.0948, 1543.0364, 1537.9737, 1542.2611, 1543.9685, 1546.4901,
+                1544.4080, 1540.8671, 1537.3353, 1549.4331, 1541.4436, 1544.1299, 1550.1770, 1553.1872, 1549.3417, 1542.3788,
+                1543.5094, 1541.7905, 1537.6625, 1547.3840, 1538.5185, 1549.6764, 1556.6138, 1552.0476, 1541.7629, 1544.7006,
+                /* segments changes here */
+                1587.1390, 1594.5451, 1586.2430, 1596.7310, 1548.1423];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 39, values.length]);
+        });
+
+        it('should segment time series for platform=51 metric=4565 betweeen 1452191332230 and 1454628206453 into two parts', function () {
+            var values = [
+                147243216, 147736350, 146670090, 146629723, 142749220, 148234161, 147303822, 145112097, 145852468, 147094741,
+                147568897, 145160531, 148028242, 141272279, 144323236, 147492567, 146219156, 144895726, 144418925, 145455873,
+                141924694, 141025833, 142082139, 144154698, 145312939, 148282554, 151852126, 149303740, 149431703, 150300257,
+                148752468, 150449779, 150030118, 150553542, 151775421, 146666762, 149492535, 147143284, 150356837, 147799616,
+                149889520,
+                258634751, 147397840, 256106147, 261100534, 255903392, 259658019, 259501433, 257685682, 258460322, 255563633,
+                259050663, 255567490, 253274911];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, 40, values.length]);
+        });
+
+        it('should not segment time series for platform=51 metric=4817 betweeen 1453926047749 and 1454635479052 into multiple parts', function () {
+            var values = [
+                5761.3, 5729.4, 5733.49, 5727.4, 5726.56, 5727.48, 5716.79, 5721.23, 5682.5, 5735.71,
+                5750.99, 5755.51, 5756.02, 5725.76, 5710.14, 5776.17, 5774.29, 5769.99, 5739.65, 5756.05,
+                5722.87, 5726.8, 5779.23, 5772.2, 5763.1, 5807.05];
+            assert.deepEqual(Statistics.segmentTimeSeriesByMaximizingSchwarzCriterion(values), [0, values.length]);
+        });
+    });
+});
</ins></span></pre>
</div>
</div>

</body>
</html>