Add unit tests for measurement-set.js and measurement-adapter.js
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 19 Mar 2016 02:17:48 +0000 (02:17 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 19 Mar 2016 02:17:48 +0000 (02:17 +0000)
https://bugs.webkit.org/show_bug.cgi?id=155673

Reviewed by Darin Adler.

Added mocha unit tests for MeasurementSet and MeasurementAdapter classes along with the necessary
refactoring to run these tests in node.

getJSON and getJSONStatus are now under RemoteAPI so that unit tests can mock them.

Removed the dependency on v2 UI's TimeSeries and Measurement class by adding a new implementation
of TimeSeries in v3 and removing the dependency on Measurement in chart-pane-status-view.js with
new helper methods on Build and CommitLog.

Many js files now use 'use strict' (node doesn't support class syntax in non-strict mode) and
module.exports to export symbols in node's require function.

* public/v3/index.html:
* public/v3/main.js:
* public/v3/models/analysis-results.js:
(AnalysisResults.fetch):
* public/v3/models/analysis-task.js:
(AnalysisTask.fetchAll):
* public/v3/models/builder.js:
(Build):
(Build.prototype.builder): Added. Used by ChartPaneStatusView.
(Build.prototype.buildNumber): Ditto.
(Build.prototype.buildTime): Ditto.
* public/v3/models/commit-log.js:
(CommitLog.prototype.diff): Ditto.
(CommitLog.fetchBetweenRevisions):
* public/v3/models/data-model.js:
(DataModelObject.cachedFetch):
* public/v3/models/measurement-adaptor.js:
(MeasurementAdaptor.prototype.applyToAnalysisResults): Renamed from adoptToAnalysisResults.
(MeasurementAdaptor.prototype.applyTo): Renamed from adoptToSeries. Now shares a lot more
code with applyToAnalysisResults. The code to set 'series' and 'seriesIndex' has been moved
to TimeSeries.append. 'measurement' is no longer needed as this patch removes its only use
in ChartPaneStatusView.
* public/v3/models/measurement-cluster.js:
(MeasurementCluster.prototype.addToSeries): Use TimeSeries.append instead of directly mutating
series._series.
* public/v3/models/measurement-set.js:
(Array.prototype.includes): Added a polyfill for node.
(MeasurementSet.prototype._fetchSecondaryClusters): Removed a bogus assertion. When fetchBetween
is called with a mixture of clusters that have been fetched and not fetched, this assertion fails.
(MeasurementSet.prototype._fetch):
(TimeSeries.prototype.findById): Moved to time-series.js.
(TimeSeries.prototype.dataBetweenPoints): Ditto.
(TimeSeries.prototype.firstPoint): Ditto.
(TimeSeries.prototype.fetchedTimeSeries): Moved the code to extend the last point to TimeSeries'
extendToFuture.
* public/v3/models/repository.js:
* public/v3/models/root-set.js:
(MeasurementRootSet): Ignore repositories that had not been defined (e.g. it could be added after
manifest.json had been downloaded but before a given root set is created for an A/B testing).
* public/v3/models/time-series.js:
(TimeSeries): Added.
(TimeSeries.prototype.append): Added.
(TimeSeries.prototype.extendToFuture): Added.
(TimeSeries.prototype.firstPoint): Moved from measurement-set.js.
(TimeSeries.prototype.lastPoint): Added.
(TimeSeries.prototype.previousPoint): Added.
(TimeSeries.prototype.nextPoint): Added.
(TimeSeries.prototype.findPointByIndex): Added.
(TimeSeries.prototype.findById): Moved from measurement-set.js.
(TimeSeries.prototype.findPointAfterTime): Added.
(TimeSeries.prototype.dataBetweenPoints): Moved from measurement-set.js.
* public/v3/pages/chart-pane-status-view.js:
(ChartPaneStatusView.prototype.render): Use newly added helper functions on Build.
(ChartPaneStatusView.prototype._formatTime): Added.
(ChartPaneStatusView.prototype.setCurrentRepository):
(ChartPaneStatusView.prototype.computeChartStatusLabels): Rewrote the code using RootSet object on
currentPoint and previousPoint instead of Measurement class from v2 UI. Also sort the results using
sortByNamePreferringOnesWithURL.
* public/v3/remote.js:
(RemoteAPI.getJSON): Moved under RemoteAPI.
(RemoteAPI.getJSONWithStatus): Ditto.
(PrivilegedAPI):
(PrivilegedAPI.requestCSRFToken):

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

16 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/main.js
Websites/perf.webkit.org/public/v3/models/analysis-results.js
Websites/perf.webkit.org/public/v3/models/analysis-task.js
Websites/perf.webkit.org/public/v3/models/builder.js
Websites/perf.webkit.org/public/v3/models/commit-log.js
Websites/perf.webkit.org/public/v3/models/data-model.js
Websites/perf.webkit.org/public/v3/models/measurement-adaptor.js
Websites/perf.webkit.org/public/v3/models/measurement-cluster.js
Websites/perf.webkit.org/public/v3/models/measurement-set.js
Websites/perf.webkit.org/public/v3/models/repository.js
Websites/perf.webkit.org/public/v3/models/root-set.js
Websites/perf.webkit.org/public/v3/models/time-series.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/pages/chart-pane-status-view.js
Websites/perf.webkit.org/public/v3/remote.js

index 4eca5de..ed1b54a 100644 (file)
@@ -1,3 +1,86 @@
+2016-03-18  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add unit tests for measurement-set.js and measurement-adapter.js
+        https://bugs.webkit.org/show_bug.cgi?id=155673
+
+        Reviewed by Darin Adler.
+
+        Added mocha unit tests for MeasurementSet and MeasurementAdapter classes along with the necessary
+        refactoring to run these tests in node.
+
+        getJSON and getJSONStatus are now under RemoteAPI so that unit tests can mock them.
+
+        Removed the dependency on v2 UI's TimeSeries and Measurement class by adding a new implementation
+        of TimeSeries in v3 and removing the dependency on Measurement in chart-pane-status-view.js with
+        new helper methods on Build and CommitLog.
+
+        Many js files now use 'use strict' (node doesn't support class syntax in non-strict mode) and
+        module.exports to export symbols in node's require function.
+
+        * public/v3/index.html:
+        * public/v3/main.js:
+        * public/v3/models/analysis-results.js:
+        (AnalysisResults.fetch):
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask.fetchAll):
+        * public/v3/models/builder.js:
+        (Build):
+        (Build.prototype.builder): Added. Used by ChartPaneStatusView.
+        (Build.prototype.buildNumber): Ditto.
+        (Build.prototype.buildTime): Ditto.
+        * public/v3/models/commit-log.js:
+        (CommitLog.prototype.diff): Ditto.
+        (CommitLog.fetchBetweenRevisions):
+        * public/v3/models/data-model.js:
+        (DataModelObject.cachedFetch):
+        * public/v3/models/measurement-adaptor.js:
+        (MeasurementAdaptor.prototype.applyToAnalysisResults): Renamed from adoptToAnalysisResults.
+        (MeasurementAdaptor.prototype.applyTo): Renamed from adoptToSeries. Now shares a lot more
+        code with applyToAnalysisResults. The code to set 'series' and 'seriesIndex' has been moved
+        to TimeSeries.append. 'measurement' is no longer needed as this patch removes its only use
+        in ChartPaneStatusView.
+        * public/v3/models/measurement-cluster.js:
+        (MeasurementCluster.prototype.addToSeries): Use TimeSeries.append instead of directly mutating
+        series._series.
+        * public/v3/models/measurement-set.js:
+        (Array.prototype.includes): Added a polyfill for node.
+        (MeasurementSet.prototype._fetchSecondaryClusters): Removed a bogus assertion. When fetchBetween
+        is called with a mixture of clusters that have been fetched and not fetched, this assertion fails.
+        (MeasurementSet.prototype._fetch):
+        (TimeSeries.prototype.findById): Moved to time-series.js.
+        (TimeSeries.prototype.dataBetweenPoints): Ditto.
+        (TimeSeries.prototype.firstPoint): Ditto.
+        (TimeSeries.prototype.fetchedTimeSeries): Moved the code to extend the last point to TimeSeries'
+        extendToFuture.
+        * public/v3/models/repository.js:
+        * public/v3/models/root-set.js:
+        (MeasurementRootSet): Ignore repositories that had not been defined (e.g. it could be added after
+        manifest.json had been downloaded but before a given root set is created for an A/B testing).
+        * public/v3/models/time-series.js:
+        (TimeSeries): Added.
+        (TimeSeries.prototype.append): Added.
+        (TimeSeries.prototype.extendToFuture): Added.
+        (TimeSeries.prototype.firstPoint): Moved from measurement-set.js.
+        (TimeSeries.prototype.lastPoint): Added.
+        (TimeSeries.prototype.previousPoint): Added.
+        (TimeSeries.prototype.nextPoint): Added.
+        (TimeSeries.prototype.findPointByIndex): Added.
+        (TimeSeries.prototype.findById): Moved from measurement-set.js.
+        (TimeSeries.prototype.findPointAfterTime): Added.
+        (TimeSeries.prototype.dataBetweenPoints): Moved from measurement-set.js.
+        * public/v3/pages/chart-pane-status-view.js:
+        (ChartPaneStatusView.prototype.render): Use newly added helper functions on Build.
+        (ChartPaneStatusView.prototype._formatTime): Added.
+        (ChartPaneStatusView.prototype.setCurrentRepository):
+        (ChartPaneStatusView.prototype.computeChartStatusLabels): Rewrote the code using RootSet object on
+        currentPoint and previousPoint instead of Measurement class from v2 UI. Also sort the results using
+        sortByNamePreferringOnesWithURL.
+        * public/v3/remote.js:
+        (RemoteAPI.getJSON): Moved under RemoteAPI.
+        (RemoteAPI.getJSONWithStatus): Ditto.
+        (PrivilegedAPI):
+        (PrivilegedAPI.requestCSRFToken):
+
 2016-03-17  Ryosuke Niwa  <rniwa@webkit.org>
 
         Add unit tests for config.json and statistics.js
index f64287f..6753c64 100644 (file)
@@ -43,6 +43,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="instrumentation.js"></script>
         <script src="remote.js"></script>
 
+        <script src="models/time-series.js"></script>
         <script src="models/measurement-adaptor.js"></script>
         <script src="models/measurement-cluster.js"></script>
         <script src="models/measurement-set.js"></script>
index 7329e42..bc23f4b 100644 (file)
@@ -56,8 +56,8 @@ function main() {
 
 function fetchManifest()
 {
-    return getJSON('../data/manifest.json').then(didFetchManifest, function () {
-        return getJSON('../api/manifest/').then(didFetchManifest, function (error) {
+    return RemoteAPI.getJSON('../data/manifest.json').then(didFetchManifest, function () {
+        return RemoteAPI.getJSON('../api/manifest/').then(didFetchManifest, function (error) {
             alert('Failed to load the site manifest: ' + error);
         });
     });
index b364cb8..234ce36 100644 (file)
@@ -26,14 +26,14 @@ class AnalysisResults {
     static fetch(taskId)
     {
         taskId = parseInt(taskId);
-        return getJSONWithStatus(`../api/measurement-set?analysisTask=${taskId}`).then(function (response) {
+        return RemoteAPI.getJSONWithStatus(`../api/measurement-set?analysisTask=${taskId}`).then(function (response) {
 
             Instrumentation.startMeasuringTime('AnalysisResults', 'fetch');
 
             var adaptor = new MeasurementAdaptor(response['formatMap']);
             var results = new AnalysisResults;
             for (var rawMeasurement of response['measurements'])
-                results.add(adaptor.adoptToAnalysisResults(rawMeasurement));
+                results.add(adaptor.applyToAnalysisResults(rawMeasurement));
 
             Instrumentation.endMeasuringTime('AnalysisResults', 'fetch');
 
index 4228860..f4f8e27 100644 (file)
@@ -207,7 +207,7 @@ class AnalysisTask extends LabeledObject {
     static fetchAll()
     {
         if (!this._fetchAllPromise)
-            this._fetchAllPromise = getJSONWithStatus('../api/analysis-tasks').then(this._constructAnalysisTasksFromRawData.bind(this));
+            this._fetchAllPromise = RemoteAPI.getJSONWithStatus('../api/analysis-tasks').then(this._constructAnalysisTasksFromRawData.bind(this));
         return this._fetchAllPromise;
     }
 
index 1fe66a9..fb4d7fd 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class Builder extends LabeledObject {
     constructor(id, object)
@@ -15,14 +16,23 @@ class Builder extends LabeledObject {
 }
 
 class Build extends DataModelObject {
-    constructor(id, builder, buildNumber)
+    constructor(id, builder, buildNumber, buildTime)
     {
         console.assert(builder instanceof Builder);
         super(id);
         this._builder = builder;
         this._buildNumber = buildNumber;
+        this._buildTime = new Date(buildTime);
     }
 
+    builder() { return this._builder; }
+    buildNumber() { return this._buildNumber; }
+    buildTime() { return this._buildTime; }
     label() { return `Build ${this._buildNumber} on ${this._builder.label()}`; }
     url() { return this._builder.urlForBuild(this._buildNumber); }
 }
+
+if (typeof module != 'undefined') {
+    module.exports.Builder = Builder;
+    module.exports.Build = Build;
+}
index 33a22ab..c7f266b 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class CommitLog extends DataModelObject {
     constructor(id, rawData)
@@ -56,6 +57,29 @@ class CommitLog extends DataModelObject {
     }
     title() { return this._repository.name() + ' at ' + this.label(); }
 
+    diff(previousCommit)
+    {
+        if (this == previousCommit)
+            previousCommit = null;
+
+        var repository = this._repository;
+        if (!previousCommit)
+            return {from: null, to: this.revision(), repository: repository, label: this.label(), url: this.url()};
+
+        var to = this.revision();
+        var from = previousCommit.revision();
+        var label = null;
+        if (parseInt(to) == to) { // e.g. r12345.
+            from = parseInt(from) + 1;
+            label = `r${from}-r${this.revision()}`;
+        } else if (to.length == 40) { // e.g. git hash
+            label = `${from}..${to}`;
+        } else
+            label = `${from} - ${to}`;
+
+        return {from: from, to: to, repository: repository, label: label, url: repository.urlForRevisionRange(from, to)};
+    }
+
     static fetchBetweenRevisions(repository, from, to)
     {
         var params = [];
@@ -74,7 +98,7 @@ class CommitLog extends DataModelObject {
             return new Promise(function (resolve) { resolve(cachedLogs); });
 
         var self = this;
-        return getJSONWithStatus(url).then(function (data) {
+        return RemoteAPI.getJSONWithStatus(url).then(function (data) {
             var commits = data['commits'].map(function (rawData) { return CommitLog.ensureSingleton(repository, rawData); });
             self._cacheCommitLogs(repository, from, to, commits);
             return commits;
@@ -101,3 +125,6 @@ class CommitLog extends DataModelObject {
         this._caches[repository.id()][from + '|' + to] = logs;
     }
 }
+
+if (typeof module != 'undefined')
+    module.exports.CommitLog = CommitLog;
index 7829a82..6c5ec80 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class DataModelObject {
     constructor(id)
@@ -66,11 +67,11 @@ class DataModelObject {
             path += '?' + query.join('&');
 
         if (noCache)
-            return getJSONWithStatus(path);
+            return RemoteAPI.getJSONWithStatus(path);
 
         var cacheMap = this.ensureNamedStaticMap(DataModelObject.CacheMapSymbol);
         if (!cacheMap[path])
-            cacheMap[path] = getJSONWithStatus(path);
+            cacheMap[path] = RemoteAPI.getJSONWithStatus(path);
 
         return cacheMap[path];
     }
@@ -103,3 +104,8 @@ class LabeledObject extends DataModelObject {
     name() { return this._name; }
     label() { return this.name(); }
 }
+
+if (typeof module != 'undefined') {
+    module.exports.DataModelObject = DataModelObject;
+    module.exports.LabeledObject = LabeledObject;
+}
index 3eb489f..4cdb437 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class MeasurementAdaptor {
     constructor(formatMap)
@@ -27,32 +28,38 @@ class MeasurementAdaptor {
         return row[this._idIndex];
     }
 
-    adoptToAnalysisResults(row)
+    applyToAnalysisResults(row)
+    {
+        var adaptedRow = this.applyTo(row);
+        adaptedRow.metricId = row[this._metricIndex];
+        adaptedRow.configType = row[this._configTypeIndex];
+        return adaptedRow;
+    }
+
+    applyTo(row)
     {
         var id = row[this._idIndex];
         var mean = row[this._meanIndex];
         var sum = row[this._sumIndex];
         var squareSum = row[this._squareSumIndex];
-        var iterationCount = row[this._countIndex];
         var revisionList = row[this._revisionsIndex];
         var buildId = row[this._buildIndex];
-        var metricId = row[this._metricIndex];
-        var configType = row[this._configTypeIndex];
+        var builderId = row[this._builderIndex];
+        var buildNumber = row[this._buildNumberIndex];
+        var buildTime = row[this._buildTimeIndex];
         var self = this;
         return {
             id: id,
             buildId: buildId,
-            metricId: metricId,
-            configType: configType,
+            metricId: null,
+            configType: null,
             rootSet: function () { return MeasurementRootSet.ensureSingleton(id, revisionList); },
-            build: function () {
-                var builder = Builder.findById(row[self._builderIndex]);
-                return new Build(id, builder, row[self._buildNumberIndex]);
-            },
+            build: function () { return new Build(buildId, Builder.findById(builderId), buildNumber, buildTime); },
+            time: row[this._commitTimeIndex],
             value: mean,
             sum: sum,
-            squareSum,
-            iterationCount: iterationCount,
+            squareSum: squareSum,
+            iterationCount: row[this._countIndex],
             interval: MeasurementAdaptor.computeConfidenceInterval(row[this._countIndex], mean, sum, squareSum)
         };
     }
@@ -79,45 +86,12 @@ class MeasurementAdaptor {
         return { value: mean, interval: interval };
     }
 
-    adoptToSeries(row, series, seriesIndex)
-    {
-        var id = row[this._idIndex];
-        var mean = row[this._meanIndex];
-        var sum = row[this._sumIndex];
-        var squareSum = row[this._squareSumIndex];
-        var revisionList = row[this._revisionsIndex];
-        var self = this;
-        return {
-            id: id,
-            measurement: function () {
-                // Create a new Measurement class that doesn't require mimicking what runs.php generates.
-                var revisionsMap = {};
-                for (var revisionRow of revisionList)
-                    revisionsMap[revisionRow[0]] = revisionRow.slice(1);
-                return new Measurement({
-                    id: id,
-                    mean: mean,
-                    sum: sum,
-                    squareSum: squareSum,
-                    revisions: revisionsMap,
-                    build: row[self._buildIndex],
-                    buildTime: row[self._buildTimeIndex],
-                    buildNumber: row[self._buildNumberIndex],
-                    builder: row[self._builderIndex],
-                });
-            },
-            rootSet: function () { return MeasurementRootSet.ensureSingleton(id, revisionList); },
-            series: series,
-            seriesIndex: seriesIndex,
-            time: row[this._commitTimeIndex],
-            value: mean,
-            interval: MeasurementAdaptor.computeConfidenceInterval(row[this._countIndex], mean, sum, squareSum)
-        };
-    }
-
     static computeConfidenceInterval(iterationCount, mean, sum, squareSum)
     {
         var delta = Statistics.confidenceIntervalDelta(0.95, iterationCount, sum, squareSum);
         return isNaN(delta) ? null : [mean - delta, mean + delta];
     }
 }
+
+if (typeof module != 'undefined')
+    module.exports.MeasurementAdaptor = MeasurementAdaptor;
index d438933..aa8bbd0 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class MeasurementCluster {
     constructor(response)
@@ -25,7 +26,10 @@ class MeasurementCluster {
 
             idMap[id] = true;
 
-            series._series.push(self._adaptor.adoptToSeries(row, series, series._series.length));
+            series.append(self._adaptor.applyTo(row));
         });
     }
 }
+
+if (typeof module != 'undefined')
+    module.exports.MeasurementCluster = MeasurementCluster;
index d6c99cc..45d1dec 100644 (file)
@@ -1,3 +1,7 @@
+'use strict';
+
+if (!Array.prototype.includes)
+    Array.prototype.includes = function (value) { return this.indexOf(value) >= 0; }
 
 class MeasurementSet {
     constructor(platformId, metricId, lastModified)
@@ -96,10 +100,8 @@ class MeasurementSet {
             else if (!callbackList.includes(callback))
                 callbackList.push(callback);
 
-            if (shouldStartFetch) {
-                console.assert(!shouldInvokeCallackNow);
+            if (shouldStartFetch)
                 this._fetch(endTime, true);
-            }
         }
 
         if (shouldInvokeCallackNow)
@@ -121,7 +123,7 @@ class MeasurementSet {
 
         var self = this;
 
-        return getJSONWithStatus(url).then(function (data) {
+        return RemoteAPI.getJSONWithStatus(url).then(function (data) {
             if (!clusterEndTime && useCache && +data['lastModified'] < self._lastModified)
                 self._fetch(clusterEndTime, false);
             else
@@ -218,17 +220,8 @@ class MeasurementSet {
         for (var cluster of this._sortedClusters)
             cluster.addToSeries(series, configType, includeOutliers, idMap);
 
-        if (extendToFuture && series._series.length) {
-            var lastPoint = series._series[series._series.length - 1];
-            series._series.push({
-                series: series,
-                seriesIndex: series._series.length,
-                measurement: null,
-                time: Date.now() + 365 * 24 * 3600 * 1000,
-                value: lastPoint.value,
-                interval: lastPoint.interval,
-            });
-        }
+        if (extendToFuture)
+            series.extendToFuture();
 
         Instrumentation.endMeasuringTime('MeasurementSet', 'fetchedTimeSeries');
 
@@ -236,25 +229,5 @@ class MeasurementSet {
     }
 }
 
-TimeSeries.prototype.findById = function (id)
-{
-    return this._series.find(function (point) { return point.id == id });
-}
-
-TimeSeries.prototype.dataBetweenPoints = function (firstPoint, lastPoint)
-{
-    var data = this._series;
-    var filteredData = [];
-    for (var i = firstPoint.seriesIndex; i <= lastPoint.seriesIndex; i++) {
-        if (!data[i].markedOutlier)
-            filteredData.push(data[i]);
-    }
-    return filteredData;
-}
-
-TimeSeries.prototype.firstPoint = function ()
-{
-    if (!this._series || !this._series.length)
-        return null;
-    return this._series[0];
-}
+if (typeof module != 'undefined')
+    module.exports.MeasurementSet = MeasurementSet;
index d09b11d..5192fd0 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class Repository extends LabeledObject {
     constructor(id, object)
@@ -36,3 +37,6 @@ class Repository extends LabeledObject {
     }
 
 }
+
+if (typeof module != 'undefined')
+    module.exports.Repository = Repository;
index fc31cdd..dae7845 100644 (file)
@@ -1,3 +1,4 @@
+'use strict';
 
 class RootSet extends DataModelObject {
 
@@ -75,6 +76,8 @@ class MeasurementRootSet extends RootSet {
         for (var values of revisionList) {
             var repositoryId = values[0];
             var repository = Repository.findById(repositoryId);
+            if (!repository)
+                continue;
 
             this._repositoryToCommitMap[repositoryId] = CommitLog.ensureSingleton(repository, {revision: values[1], time: values[2]});
             this._repositories.push(repository);
@@ -106,3 +109,8 @@ class CustomRootSet {
 
 }
 
+if (typeof module != 'undefined') {
+    module.exports.RootSet = RootSet;
+    module.exports.MeasurementRootSet = MeasurementRootSet;
+    module.exports.CustomRootSet = CustomRootSet;
+}
diff --git a/Websites/perf.webkit.org/public/v3/models/time-series.js b/Websites/perf.webkit.org/public/v3/models/time-series.js
new file mode 100644 (file)
index 0000000..43ce113
--- /dev/null
@@ -0,0 +1,73 @@
+
+// v3 UI still relies on RunsData for associating metrics with units.
+// Use declartive syntax once that dependency has been removed.
+TimeSeries = class {
+    constructor()
+    {
+        this._data = [];
+    }
+
+    append(item)
+    {
+        console.assert(item.series === undefined);
+        item.series = this;
+        item.seriesIndex = this._data.length;
+        this._data.push(item);
+    }
+
+    extendToFuture()
+    {
+        if (!this._data.length)
+            return;
+        var lastPoint = this._data[this._data.length - 1];
+        this._data.push({
+            series: this,
+            seriesIndex: this._data.length,
+            time: Date.now() + 365 * 24 * 3600 * 1000,
+            value: lastPoint.value,
+            interval: lastPoint.interval,
+        });
+    }
+
+    firstPoint() { return this._data.length ? this._data[0] : null; }
+    lastPoint() { return this._data.length ? this._data[this._data.length - 1] : null; }
+
+    previousPoint(point)
+    {
+        console.assert(point.series == this);
+        if (!point.seriesIndex)
+            return null;
+        return this._data[point.seriesIndex - 1];
+    }
+
+    nextPoint(point)
+    {
+        console.assert(point.series == this);
+        if (point.seriesIndex + 1 >= this._data.length)
+            return null;
+        return this._data[point.seriesIndex + 1];
+    }
+
+    findPointByIndex(index)
+    {
+        if (!this._data || index < 0 || index >= this._data.length)
+            return null;
+        return this._data[index];
+    }
+
+    findById(id) { return this._data.find(function (point) { return point.id == id }); }
+
+    findPointAfterTime(time) { return this._data.find(function (point) { return point.time >= time; }); }
+
+    dataBetweenPoints(firstPoint, lastPoint)
+    {
+        var data = this._data;
+        var filteredData = [];
+        for (var i = firstPoint.seriesIndex; i <= lastPoint.seriesIndex; i++) {
+            if (!data[i].markedOutlier)
+                filteredData.push(data[i]);
+        }
+        return filteredData;
+    }
+
+};
index 73566d5..0df932d 100644 (file)
@@ -44,31 +44,33 @@ class ChartPaneStatusView extends ChartStatusView {
             };
 
             return element('tr', {class: selected ? 'selected' : '', onclick: action}, [
-                element('td', info.name),
+                element('td', info.repository.name()),
                 element('td', info.url ? link(info.label, info.label, info.url, true) : info.label),
                 element('td', {class: 'commit-viewer-opener'}, link('\u00BB', action)),
             ]);
         });
 
         if (this._buildInfo) {
-            var number = this._buildInfo.buildNumber();
-            var builder = Builder.findById(this._buildInfo.builderId());
-            var url = null;
-            if (builder)
-                url = builder.urlForBuild(number);
-            var buildTime = this._buildInfo.formattedBuildTime();
+            var build = this._buildInfo;
+            var number = build.buildNumber();
+            var buildTime = this._formatTime(build.buildTime());
+            var url = build.url();
 
             tableContent.unshift(element('tr', [
                 element('td', 'Build'),
-                element('td', {colspan: 2}, [
-                    url ? link(number, `Build ${number} on "${builder.name()}"`, url, true) : number,
-                    ` (${buildTime})`]),
+                element('td', {colspan: 2}, [url ? link(number, build.label(), url, true) : number, ` (${buildTime})`]),
             ]));
         }
 
         this.renderReplace(this.content().querySelector('.chart-pane-revisions'), tableContent);
     }
 
+    _formatTime(date)
+    {
+        console.assert(date instanceof Date);
+        return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
+    }
+
     setCurrentRepository(repository)
     {
         this._currentRepository = repository;
@@ -130,12 +132,8 @@ class ChartPaneStatusView extends ChartStatusView {
         if (!currentPoint)
             return;
 
-        var currentMeasurement = currentPoint.measurement();
-        if (!currentMeasurement)
-            return;
-
-        if (!this._chart.currentSelection() && currentMeasurement)
-            this._buildInfo = currentMeasurement;
+        if (!this._chart.currentSelection())
+            this._buildInfo = currentPoint.build();
 
         if (currentPoint && previousPoint && this._chart.currentSelection()) {
             this._pointsRangeForAnalysis = {
@@ -145,40 +143,17 @@ class ChartPaneStatusView extends ChartStatusView {
         }
 
         // FIXME: Rewrite the interface to obtain the list of revision changes.
-        var previousMeasurement = previousPoint ? previousPoint.measurement() : null;
+        var currentRootSet = currentPoint.rootSet();
+        var previousRootSet = previousPoint ? previousPoint.rootSet() : null;
 
-        var revisions = currentMeasurement.formattedRevisions(previousMeasurement);
+        var repositoriesInCurrentRootSet = Repository.sortByNamePreferringOnesWithURL(currentRootSet.repositories());
         var revisionList = [];
-        for (var repositoryId in revisions) {
-            var repository = Repository.findById(repositoryId);
-            var revision = revisions[repositoryId];
-            var url = revision.previousRevision ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision) : '';
-            if (!url)
-                url = repository.urlForRevision(revision.currentRevision);
-
-            revisionList.push({
-                from: revision.previousRevision,
-                to: revision.currentRevision,
-                repository: repository,
-                name: repository.name(),
-                label: revision.label,
-                url: url,
-            });
+        for (var repository of repositoriesInCurrentRootSet) {
+            var currentCommit = currentRootSet.commitForRepository(repository);
+            var previousCommit = previousRootSet ? previousRootSet.commitForRepository(repository) : null;
+            revisionList.push(currentCommit.diff(previousCommit));
         }
 
-        // Sort by repository names preferring ones with URL.
-        revisionList = revisionList.sort(function (a, b) {
-            if (!!a.url == !!b.url) {
-                if (a.name > b.name)
-                    return 1;
-                else if (a.name < b.name)
-                    return -1;
-                return 0;
-            } else if (b.url) // a > b
-                return 1;
-            return -1;
-        });
-
         this._revisionList = revisionList;
     }
 
index 59f01dd..02e416b 100644 (file)
@@ -1,5 +1,7 @@
 
-function getJSON(path, data)
+var RemoteAPI = {};
+
+RemoteAPI.getJSON = function(path, data)
 {
     console.assert(!path.startsWith('http:') && !path.startsWith('https:') && !path.startsWith('file:'));
 
@@ -43,9 +45,9 @@ function getJSON(path, data)
     });
 }
 
-function getJSONWithStatus(path, data)
+RemoteAPI.getJSONWithStatus = function(path, data)
 {
-    return getJSON(path, data).then(function (content) {
+    return this.getJSON(path, data).then(function (content) {
         if (content['status'] != 'OK')
             return Promise.reject(content['status']);
         return content;
@@ -62,7 +64,7 @@ PrivilegedAPI = class {
             for (var key in data)
                 clonedData[key] = data[key];
             clonedData['token'] = token;
-            return getJSONWithStatus('../privileged-api/' + path, clonedData);
+            return RemoteAPI.getJSONWithStatus('../privileged-api/' + path, clonedData);
         });
     }
 
@@ -72,7 +74,7 @@ PrivilegedAPI = class {
         if (this._token && this._expiration > Date.now() + maxNetworkLatency)
             return Promise.resolve(this._token);
 
-        return getJSONWithStatus('../privileged-api/generate-csrf-token', {}).then(function (result) {
+        return RemoteAPI.getJSONWithStatus('../privileged-api/generate-csrf-token', {}).then(function (result) {
             PrivilegedAPI._token = result['token'];
             PrivilegedAPI._expiration = new Date(result['expiration']);
             return PrivilegedAPI._token;