Summary page should show warnings when current or baseline data is missing.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 5 May 2016 05:27:18 +0000 (05:27 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 5 May 2016 05:27:18 +0000 (05:27 +0000)
https://bugs.webkit.org/show_bug.cgi?id=157339

Reviewed by Ryosuke Niwa.

Set summary page to be the default page of v3 UI.
Show warning icon when either baseline or current data is missing.
Make fetchBetween returns a promise.
Update unit tests for MeasurementSet.fetchBetween since it returns a promise now.
Add a workaround to skip some platform and metric configurations.

* public/v3/components/ratio-bar-graph.js:
(RatioBarGraph):
(RatioBarGraph.prototype.update): Add showWarningIcon flag to indicate whether we should show warning icon.
(RatioBarGraph.prototype.render): Show warning icon when showWarningIcon is true.
(RatioBarGraph.cssTemplate): Add style for warning icon.
* public/v3/components/warning-icon.js: Add warning icon.
(WarningIcon):
(WarningIcon.cssTemplate):
* public/v3/index.html:
* public/v3/main.js:
(main): Set summary page to be the default page of v3 UI.
* public/v3/models/measurement-set.js:
(MeasurementSet):
(MeasurementSet.prototype.fetchBetween): Returns a promise. Fix the bug in previous implementation that we miss
some callbacks sometimes. Basically, we will fetch primary cluster first, then secondary clusters. For each
secondary cluster fetch, we will always invoke callback even when it fails.
(MeasurementSet.prototype._fetchSecondaryClusters): Deleted.
(MeasurementSet.prototype._fetch.else.url.api.measurement.set platform): Deleted.
* public/v3/pages/summary-page.js:
(SummaryPage): Add a variable for excluded configurations.
(SummaryPage.prototype._createConfigurationGroup): Pass excluded configurations while building config groups.
(SummaryPage.prototype._constructTable): Remove the logic for unified header since it breaks consistency of the table appearance.
(SummaryPage.prototype.this._renderQueue.push): Show warning message when baseline/current data is missing.
(SummaryPageConfigurationGroup): Add a variable to keep track of the warnings while computing summary.
(SummaryPageConfigurationGroup.prototype.warnings): A getter for warnings.
(SummaryPageConfigurationGroup._computeSummary): Fix a bug in calculating ratios. We should always use
current/baseline for ratio and present the difference between ratio and 1 in the summary page.
(SummaryPageConfigurationGroup.set then): Deleted.
(SummaryPageConfigurationGroup.set var): Deleted.
* unit-tests/measurement-set-tests.js: Add a helper function to wait for fetchBetween. Update unit tests since fetchBetween returns a promise now.
(promise.set fetchBetween):
(set MeasurementSet):
(set fetchBetween): Deleted.

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js
Websites/perf.webkit.org/public/v3/components/warning-icon.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/main.js
Websites/perf.webkit.org/public/v3/models/measurement-set.js
Websites/perf.webkit.org/public/v3/pages/summary-page.js
Websites/perf.webkit.org/unit-tests/measurement-set-tests.js

index 707b970..f9f0d62 100644 (file)
@@ -1,3 +1,50 @@
+2016-05-04  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Summary page should show warnings when current or baseline data is missing.
+        https://bugs.webkit.org/show_bug.cgi?id=157339
+
+        Reviewed by Ryosuke Niwa.
+
+        Set summary page to be the default page of v3 UI.
+        Show warning icon when either baseline or current data is missing.
+        Make fetchBetween returns a promise.
+        Update unit tests for MeasurementSet.fetchBetween since it returns a promise now.
+        Add a workaround to skip some platform and metric configurations.
+
+        * public/v3/components/ratio-bar-graph.js:
+        (RatioBarGraph):
+        (RatioBarGraph.prototype.update): Add showWarningIcon flag to indicate whether we should show warning icon.
+        (RatioBarGraph.prototype.render): Show warning icon when showWarningIcon is true.
+        (RatioBarGraph.cssTemplate): Add style for warning icon.
+        * public/v3/components/warning-icon.js: Add warning icon.
+        (WarningIcon):
+        (WarningIcon.cssTemplate):
+        * public/v3/index.html:
+        * public/v3/main.js:
+        (main): Set summary page to be the default page of v3 UI.
+        * public/v3/models/measurement-set.js:
+        (MeasurementSet):
+        (MeasurementSet.prototype.fetchBetween): Returns a promise. Fix the bug in previous implementation that we miss
+        some callbacks sometimes. Basically, we will fetch primary cluster first, then secondary clusters. For each
+        secondary cluster fetch, we will always invoke callback even when it fails.
+        (MeasurementSet.prototype._fetchSecondaryClusters): Deleted.
+        (MeasurementSet.prototype._fetch.else.url.api.measurement.set platform): Deleted.
+        * public/v3/pages/summary-page.js:
+        (SummaryPage): Add a variable for excluded configurations.
+        (SummaryPage.prototype._createConfigurationGroup): Pass excluded configurations while building config groups.
+        (SummaryPage.prototype._constructTable): Remove the logic for unified header since it breaks consistency of the table appearance.
+        (SummaryPage.prototype.this._renderQueue.push): Show warning message when baseline/current data is missing.
+        (SummaryPageConfigurationGroup): Add a variable to keep track of the warnings while computing summary.
+        (SummaryPageConfigurationGroup.prototype.warnings): A getter for warnings.
+        (SummaryPageConfigurationGroup._computeSummary): Fix a bug in calculating ratios. We should always use
+        current/baseline for ratio and present the difference between ratio and 1 in the summary page.
+        (SummaryPageConfigurationGroup.set then): Deleted.
+        (SummaryPageConfigurationGroup.set var): Deleted.
+        * unit-tests/measurement-set-tests.js: Add a helper function to wait for fetchBetween. Update unit tests since fetchBetween returns a promise now.
+        (promise.set fetchBetween):
+        (set MeasurementSet):
+        (set fetchBetween): Deleted.
+
 2016-04-26  Ryosuke Niwa  <rniwa@webkit.org>
 
         Chart status should always be computed against prior values
index 70f9238..f199c6e 100644 (file)
@@ -3,18 +3,18 @@ class RatioBarGraph extends ComponentBase {
     constructor()
     {
         super('ratio-bar-graph');
-        this._ratio = 0;
+        this._ratio = null;
         this._label = null;
         this._shouldRender = true;
-        this._bar = this.content().querySelector('.bar');
-        this._labelContainer = this.content().querySelector('.label');
+        this._ratioBarGraph = this.content().querySelector('.ratio-bar-graph');
     }
 
-    update(ratio, label)
+    update(ratio, label, showWarningIcon)
     {
-        console.assert(ratio >= -1 && ratio <= 1);
+        console.assert(isNaN(ratio) || (ratio >= -1 && ratio <= 1));
         this._ratio = ratio;
         this._label = label;
+        this._showWarningIcon = showWarningIcon;
         this._shouldRender = true;
     }
 
@@ -22,18 +22,35 @@ class RatioBarGraph extends ComponentBase {
     {
         if (!this._shouldRender)
             return;
+        this._shouldRender = false;
+        var element = ComponentBase.createElement;
 
-        var percent = Math.abs(this._ratio * 100);
-        this._labelContainer.textContent = this._label;
-        this._bar.style.width = Math.min(percent, 50) + '%';
-        this._bar.parentNode.className = 'ratio-bar-graph ' + (this._ratio > 0 ? 'better' : 'worse');
+        var children = [element('div', {class: 'separator'})];
+        if (this._showWarningIcon) {
+            if (this._ratio && this._ratio < -0.4)
+                this._ratioBarGraph.classList.add('warning-on-right');
+            else
+                this._ratioBarGraph.classList.remove('warning-on-right');
+            children.push(new WarningIcon);
+        }
 
-        this._shouldRender = false;
+        var barClassName = 'bar';
+        var labelClassName = 'label';
+        if (this._ratio) {
+            var ratioType = this._ratio > 0 ? 'better' : 'worse';
+            barClassName = [barClassName, ratioType].join(' ');
+            labelClassName = [labelClassName, ratioType].join(' ');
+            children.push(element('div', {class: barClassName, style: 'width:' + Math.min(Math.abs(this._ratio * 100), 50) + '%'}));
+        }
+        if (this._label)
+            children.push(element('div', {class: labelClassName}, this._label));
+
+        this.renderReplace(this._ratioBarGraph, children);
     }
 
     static htmlTemplate()
     {
-        return `<div class="ratio-bar-graph"><div class="seperator"></div><div class="bar"></div><div class="label"></div></div>`;
+        return `<div class="ratio-bar-graph"></div>`;
     }
 
     static cssTemplate()
@@ -50,7 +67,19 @@ class RatioBarGraph extends ComponentBase {
                 text-decoration: none;
                 color: black;
             }
-            .ratio-bar-graph .seperator {
+            .ratio-bar-graph warning-icon {
+                position: absolute;
+                display: block;
+                top: 0;
+            }
+            .ratio-bar-graph:not(.warning-on-right) warning-icon {
+                left: 0;
+            }
+            .ratio-bar-graph.warning-on-right warning-icon {
+                transform: scaleX(-1);
+                right: 0;
+            }
+            .ratio-bar-graph .separator {
                 position: absolute;
                 left: 50%;
                 width: 0px;
@@ -64,22 +93,22 @@ class RatioBarGraph extends ComponentBase {
                 height: calc(100% - 1rem);
                 background: #ccc;
             }
-            .ratio-bar-graph.worse .bar {
+            .ratio-bar-graph .bar.worse {
                 transform: translateX(-100%);
                 background: #c33;
             }
-            .ratio-bar-graph.better .bar {
+            .ratio-bar-graph .bar.better {
                 background: #3c3;
             }
             .ratio-bar-graph .label {
                 position: absolute;
                 line-height: 2.5rem;
             }
-            .ratio-bar-graph.worse .label {
+            .ratio-bar-graph .label.worse {
                 text-align: left;
                 left: calc(50% + 0.2rem);
             }
-            .ratio-bar-graph.better .label {
+            .ratio-bar-graph .label.better {
                 text-align: right;
                 right: calc(50% + 0.2rem);
             }
diff --git a/Websites/perf.webkit.org/public/v3/components/warning-icon.js b/Websites/perf.webkit.org/public/v3/components/warning-icon.js
new file mode 100644 (file)
index 0000000..d0b3889
--- /dev/null
@@ -0,0 +1,33 @@
+
+class WarningIcon extends ButtonBase {
+    constructor()
+    {
+        super('warning-icon');
+    }
+
+    static cssTemplate()
+    {
+        return super.cssTemplate() + `
+            .button {
+                display: block;
+                width: 0.7rem;
+                height: 0.7rem;
+            }
+            .button svg {
+                display: block;
+            }
+        `;
+    }
+
+    static htmlTemplate()
+    {
+        return `<a class="button" href="#"><svg viewBox="0 0 100 100">
+            <g stroke="#9f6000" fill="#9f6000" stroke-width="7">
+                <polygon points="0,0, 100,0, 0,100" />
+            </g>
+        </svg></a>`;
+    }
+
+}
+
+ComponentBase.defineElement('warning-icon', WarningIcon);
index 3db657b..3d11068 100644 (file)
@@ -66,6 +66,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="components/base.js"></script>
         <script src="components/spinner-icon.js"></script>
         <script src="components/button-base.js"></script>
+        <script src="components/warning-icon.js"></script>
         <script src="components/close-button.js"></script>
         <script src="components/commit-log-viewer.js"></script>
         <script src="components/editable-text.js"></script>
index a21b735..c073b38 100644 (file)
@@ -20,7 +20,7 @@ function main() {
         var router = new PageRouter();
         var chartsToolbar = new ChartsToolbar;
 
-        var summaryPage = new SummaryPage(manifest.summary);
+        var summaryPage = manifest.summary ? new SummaryPage(manifest.summary) : null;
         var chartsPage = new ChartsPage(chartsToolbar);
         var analysisCategoryPage = new AnalysisCategoryPage();
 
@@ -31,13 +31,14 @@ function main() {
         analysisTaskPage.setParentPage(analysisCategoryPage);
 
         var heading = new Heading(manifest.siteTitle);
-        heading.addPageGroup([summaryPage, chartsPage, analysisCategoryPage]);
+        heading.addPageGroup([summaryPage, chartsPage, analysisCategoryPage].filter(function (page) { return page; }));
 
         heading.setTitle(manifest.siteTitle);
         heading.addPageGroup(dashboardPages);
 
         var router = new PageRouter();
-        router.addPage(summaryPage);
+        if(summaryPage)
+            router.addPage(summaryPage);
         router.addPage(chartsPage);
         router.addPage(createAnalysisTaskPage);
         router.addPage(analysisTaskPage);
@@ -45,7 +46,9 @@ function main() {
         for (var page of dashboardPages)
             router.addPage(page);
 
-        if (dashboardPages)
+        if (summaryPage)
+            router.setDefaultPage(summaryPage);
+        else if (dashboardPages)
             router.setDefaultPage(dashboardPages[0]);
         else
             router.setDefaultPage(chartsPage);
index 97e9206..a8bdf66 100644 (file)
@@ -10,17 +10,17 @@ class MeasurementSet {
         this._metricId = metricId;
         this._lastModified = +lastModified;
 
-        this._waitingForPrimaryCluster = null;
-        this._fetchedPrimary = false;
-        this._endTimeToCallback = {};
-
         this._sortedClusters = [];
         this._primaryClusterEndTime = null;
         this._clusterCount = null;
         this._clusterStart = null;
         this._clusterSize = null;
+        this._allFetches = {};
+        this._primaryClusterPromise = null;
     }
 
+    platformId() { return this._platformId; }
+
     static findSet(platformId, metricId, lastModified)
     {
         if (!this._set)
@@ -35,7 +35,6 @@ class MeasurementSet {
     {
         var clusterStart = this._clusterStart;
         var clusterSize = this._clusterSize;
-        console.assert(clusterStart && clusterSize);
 
         function computeClusterStart(time) {
             var diff = time - clusterStart;
@@ -58,87 +57,61 @@ class MeasurementSet {
 
     fetchBetween(startTime, endTime, callback)
     {
-        if (!this._fetchedPrimary) {
-            var primaryFetchHadFailed = this._waitingForPrimaryCluster === false;
-            if (primaryFetchHadFailed) {
-                callback();
-                return;
-            }
-
-            var shouldStartPrimaryFetch = !this._waitingForPrimaryCluster;
-            if (shouldStartPrimaryFetch)
-                this._waitingForPrimaryCluster = [];
-
-            this._waitingForPrimaryCluster.push({startTime: startTime, endTime: endTime, callback: callback});
-
-            if (shouldStartPrimaryFetch)
-                this._fetch(null, true);
-
-            return;
-        }
-
-        this._fetchSecondaryClusters(startTime, endTime, callback);
+        if (!this._primaryClusterPromise)
+            this._primaryClusterPromise = this._fetchPrimaryCluster();
+        var self = this;
+        this._primaryClusterPromise.catch(callback);
+        return this._primaryClusterPromise.then(function () {
+            var promiseList = [];
+            self.findClusters(startTime, endTime).map(function (clusterEndTime) {
+                if(!self._allFetches[clusterEndTime])
+                    self._allFetches[clusterEndTime] = self._fetchSecondaryCluster(clusterEndTime);
+                self._allFetches[clusterEndTime].then(callback, callback);
+                promiseList.push(self._allFetches[clusterEndTime]);
+            });
+            return Promise.all(promiseList);
+        });
     }
 
-    _fetchSecondaryClusters(startTime, endTime, callback)
+    _constructUrl(useCache, clusterEndTime)
     {
-        console.assert(this._fetchedPrimary);
-        console.assert(this._clusterStart && this._clusterSize);
-        console.assert(this._sortedClusters.length);
-
-        var clusters = this.findClusters(startTime, endTime);
-        var shouldInvokeCallackNow = false;
-        for (var endTime of clusters) {
-            var isPrimaryCluster = endTime == this._primaryClusterEndTime;
-            var shouldStartFetch = !isPrimaryCluster && !(endTime in this._endTimeToCallback);
-            if (shouldStartFetch)
-                this._endTimeToCallback[endTime] = [];
-
-            var callbackList = this._endTimeToCallback[endTime];
-            if (isPrimaryCluster || callbackList === true)
-                shouldInvokeCallackNow = true;
-            else if (!callbackList.includes(callback))
-                callbackList.push(callback);
-
-            if (shouldStartFetch)
-                this._fetch(endTime, true);
+        if (!useCache) {
+            return `../api/measurement-set?platform=${this._platformId}&metric=${this._metricId}`;
         }
-
-        if (shouldInvokeCallackNow)
-            callback();
-    }
-
-    _fetch(clusterEndTime, useCache)
-    {
-        console.assert(!clusterEndTime || useCache);
-
         var url;
-        if (useCache) {
-            url = `../data/measurement-set-${this._platformId}-${this._metricId}`;
-            if (clusterEndTime)
-                url += '-' + +clusterEndTime;
-            url += '.json';
-        } else
-            url = `../api/measurement-set?platform=${this._platformId}&metric=${this._metricId}`;
+        url = `../data/measurement-set-${this._platformId}-${this._metricId}`;
+        if (clusterEndTime)
+            url += '-' + +clusterEndTime;
+        url += '.json';
+        return url;
+    }
 
+    _fetchPrimaryCluster() {
         var self = this;
+        return RemoteAPI.getJSONWithStatus(self._constructUrl(true, null)).then(function (data) {
+            if (+data['lastModified'] < self._lastModified)
+                return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
+            return data;
+        }).catch(function (error) {
+            if(error == 404)
+                return RemoteAPI.getJSONWithStatus(self._constructUrl(false, null));
+            return Promise.reject(error);
+        }).then(function (data) {
+            self._didFetchJSON(true, data);
+            self._allFetches[self._primaryClusterEndTime] = self._primaryClusterPromise;
+        });
+    }
 
-        return RemoteAPI.getJSONWithStatus(url).then(function (data) {
-            if (!clusterEndTime && useCache && +data['lastModified'] < self._lastModified)
-                self._fetch(clusterEndTime, false);
-            else
-                self._didFetchJSON(!clusterEndTime, data);
-        }, function (error, xhr) {
-            if (!clusterEndTime && error == 404 && useCache)
-                self._fetch(clusterEndTime, false);
-            else
-                self._failedToFetchJSON(clusterEndTime, error);
+    _fetchSecondaryCluster(endTime) {
+        var self = this;
+        return RemoteAPI.getJSONWithStatus(self._constructUrl(true, endTime)).then(function (data) {
+            self._didFetchJSON(false, data);
         });
     }
 
     _didFetchJSON(isPrimaryCluster, response, clusterEndTime)
     {
-        console.assert(isPrimaryCluster || this._fetchedPrimary);
+        console.assert(isPrimaryCluster);
 
         if (isPrimaryCluster) {
             this._primaryClusterEndTime = response['endTime'];
@@ -149,40 +122,6 @@ class MeasurementSet {
             console.assert(this._primaryClusterEndTime);
 
         this._addFetchedCluster(new MeasurementCluster(response));
-
-        console.assert(this._waitingForPrimaryCluster);
-        if (!isPrimaryCluster) {
-            this._invokeCallbacks(response.endTime);
-            return;
-        }
-        console.assert(this._waitingForPrimaryCluster instanceof Array);
-
-        this._fetchedPrimary = true;
-        for (var entry of this._waitingForPrimaryCluster)
-            this._fetchSecondaryClusters(entry.startTime, entry.endTime, entry.callback);
-        this._waitingForPrimaryCluster = true;
-    }
-
-    _failedToFetchJSON(clusterEndTime, error)
-    {
-        if (clusterEndTime) {
-            this._invokeCallbacks(clusterEndTime, error || true);
-            return;
-        }
-
-        console.assert(!this._fetchedPrimary);
-        console.assert(this._waitingForPrimaryCluster instanceof Array);
-        for (var entry of this._waitingForPrimaryCluster)
-            entry.callback(error || true);
-        this._waitingForPrimaryCluster = false;
-    }
-
-    _invokeCallbacks(clusterEndTime, error)
-    {
-        var callbackList = this._endTimeToCallback[clusterEndTime];
-        for (var callback of callbackList)
-            callback(error);
-        this._endTimeToCallback[clusterEndTime] = true;
     }
 
     _addFetchedCluster(cluster)
index 5e60b0f..55169e9 100644 (file)
@@ -12,6 +12,7 @@ class SummaryPage extends PageWithHeading {
         this._shouldConstructTable = true;
         this._renderQueue = [];
         this._configGroups = [];
+        this._excludedConfigurations = summarySettings.excludedConfigurations;
 
         for (var metricGroup of summarySettings.metricGroups) {
             var group = {name: metricGroup.name, rows: []};
@@ -52,7 +53,7 @@ class SummaryPage extends PageWithHeading {
     {
         var platforms = platformIdList.map(function (id) { return Platform.findById(id); }).filter(function (obj) { return !!obj; });
         var metrics = metricIdList.map(function (id) { return Metric.findById(id); }).filter(function (obj) { return !!obj; });
-        var configGroup = new SummaryPageConfigurationGroup(platforms, metrics);
+        var configGroup = new SummaryPageConfigurationGroup(platforms, metrics, this._excludedConfigurations);
         this._configGroups.push(configGroup);
         return configGroup;
     }
@@ -75,13 +76,9 @@ class SummaryPage extends PageWithHeading {
             this._table.groups.map(function (rowGroup) {
                 return rowGroup.rows.map(function (row, rowIndex) {
                     var headings;
-                    if (rowGroup.rows.length == 1)
-                        headings = [element('th', {class: 'unifiedHeader', colspan: 2}, row.name)];
-                    else {
-                        headings = [element('th', {class: 'minorHeader'}, row.name)];
-                        if (!rowIndex)
-                            headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name));
-                    }
+                    headings = [element('th', {class: 'minorHeader'}, row.name)];
+                    if (!rowIndex)
+                        headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name));
                     return element('tr', [headings, row.cells.map(self._constructRatioGraph.bind(self))]);
                 });
             }),
@@ -92,16 +89,28 @@ class SummaryPage extends PageWithHeading {
     {
         var element = ComponentBase.createElement;
         var link = ComponentBase.createLink;
+        var configurationList = configurationGroup.configurationList();
 
         var ratioGraph = new RatioBarGraph();
 
+        var state = ChartsPage.createStateForConfigurationList(configurationList);
+        var anchor = link(ratioGraph, this.router().url('charts', state));
         this._renderQueue.push(function () {
-            ratioGraph.update(configurationGroup.ratio(), configurationGroup.label());
+            var warnings = configurationGroup.warnings();
+            var warningText = '';
+            for (var type in warnings) {
+                var platformString = Array.from(warnings[type]).map(function (platform) { return platform.name(); }).join(', ');
+                warningText += `Missing ${type} for following platform(s): ${platformString}`;
+            }
+
+            anchor.title = warningText || 'Open charts';
+            ratioGraph.update(configurationGroup.ratio(), configurationGroup.label(), !!warningText);
             ratioGraph.render();
         });
+        if (configurationList.length == 0)
+            return element('td', ratioGraph);
 
-        var state = ChartsPage.createStateForConfigurationList(configurationGroup.configurationList());
-        return element('td', link(ratioGraph, 'Open charts', this.router().url('charts', state)));
+        return element('td', anchor);
     }
 
     static htmlTemplate()
@@ -167,14 +176,14 @@ class SummaryPage extends PageWithHeading {
 }
 
 class SummaryPageConfigurationGroup {
-    constructor(platforms, metrics)
+    constructor(platforms, metrics, excludedConfigurations)
     {
         this._measurementSets = [];
         this._configurationList = [];
         this._setToRatio = new Map;
         this._ratio = null;
         this._label = null;
-        this._changeType = null;
+        this._warnings = {};
         this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
 
         for (var platform of platforms) {
@@ -183,6 +192,9 @@ class SummaryPageConfigurationGroup {
                 console.assert(metric instanceof Metric);
                 console.assert(this._smallerIsBetter == metric.isSmallerBetter());
                 metric.isSmallerBetter();
+
+                if (excludedConfigurations && platform.id() in excludedConfigurations && excludedConfigurations[platform.id()].includes(+metric.id()))
+                    continue;
                 if (platform.hasMetric(metric)) {
                     this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric)));
                     this._configurationList.push([platform.id(), metric.id()]);
@@ -193,6 +205,7 @@ class SummaryPageConfigurationGroup {
 
     ratio() { return this._ratio; }
     label() { return this._label; }
+    warnings() { return this._warnings; }
     changeType() { return this._changeType; }
     configurationList() { return this._configurationList; }
 
@@ -219,37 +232,40 @@ class SummaryPageConfigurationGroup {
         }
 
         var averageRatio = Statistics.mean(ratios);
-        if (isNaN(averageRatio)) {
-            this._summary = '-';
-            this._changeType = null;
-            return;
-        }
-
-        if (Math.abs(averageRatio - 1) < 0.001) { // Less than 0.1% difference.
-            this._summary = 'No change';
-            this._changeType = null;
+        if (isNaN(averageRatio))
             return;
-        }
 
         var currentIsSmallerThanBaseline = averageRatio < 1;
         var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
-        if (currentIsSmallerThanBaseline)
-            averageRatio = 1 / averageRatio;
+        averageRatio = Math.abs(averageRatio - 1);
 
-        this._ratio = (averageRatio - 1) * (changeType == 'better' ? 1 : -1);
-        this._label = ((averageRatio - 1) * 100).toFixed(1) + '%';
+        this._ratio = averageRatio * (changeType == 'better' ? 1 : -1);
+        this._label = (averageRatio * 100).toFixed(1) + '%';
         this._changeType = changeType;
     }
 
     _fetchAndComputeRatio(set, timeRange)
     {
         var setToRatio = this._setToRatio;
-        return SummaryPageConfigurationGroup._fetchData(set, timeRange).then(function () {
+        var self = this;
+        return set.fetchBetween(timeRange[0], timeRange[1]).then(function () {
             var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false);
             var currentTimeSeries = set.fetchedTimeSeries('current', false, false);
 
             var baselineMedian = SummaryPageConfigurationGroup._medianForTimeRange(baselineTimeSeries, timeRange);
             var currentMedian = SummaryPageConfigurationGroup._medianForTimeRange(currentTimeSeries, timeRange);
+            var platform = Platform.findById(set.platformId());
+            if (!baselineMedian) {
+                if(!('baseline' in self._warnings))
+                    self._warnings['baseline'] = new Set;
+                self._warnings['baseline'].add(platform);
+            }
+            if (!currentMedian) {
+                if(!('current' in self._warnings))
+                    self._warnings['current'] = new Set;
+                self._warnings['current'].add(platform);
+            }
+
             setToRatio.set(set, currentMedian / baselineMedian);
         }).catch(function () {
             setToRatio.set(set, NaN);
@@ -270,21 +286,4 @@ class SummaryPageConfigurationGroup {
         var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
         return Statistics.median(points);
     }
-
-    static _fetchData(set, timeRange)
-    {
-        // FIXME: Make fetchBetween return a promise.
-        var done = false;
-        return new Promise(function (resolve, reject) {
-            set.fetchBetween(timeRange[0], timeRange[1], function (error) {
-                if (done)
-                    return;
-                if (error)
-                    reject(null);
-                else if (set.hasFetchedRange(timeRange[0], timeRange[1]))
-                    resolve();
-                done = true;
-            });
-        });
-    }
 }
index 05776ad..ffc16ac 100644 (file)
@@ -45,7 +45,7 @@ describe('MeasurementSet', function () {
         it('should invoke the callback when the up-to-date cached primary cluster is fetched and it matches the requested range', function (done) {
             var set = MeasurementSet.findSet(1, 1, 3000);
             var callCount = 0;
-            set.fetchBetween(2000, 3000, function () {
+            var promise = set.fetchBetween(2000, 3000, function () {
                 callCount++;
             });
             assert.equal(requests.length, 1);
@@ -62,7 +62,7 @@ describe('MeasurementSet', function () {
                 'clusterCount': 2,
                 'status': 'OK'});
 
-            requests[0].promise.then(function () {
+            promise.then(function () {
                 assert.equal(callCount, 1);
                 assert.equal(requests.length, 1);
                 done();
@@ -71,11 +71,19 @@ describe('MeasurementSet', function () {
             });
         });
 
-        it('should invoke the callback and fetch a secondary cluster'
-            + 'when the cached primary cluster is up-to-date and within in the requested range', function (done) {
+        function waitForMeasurementSet()
+        {
+            return Promise.resolve().then(function () {
+                return Promise.resolve();
+            }).then(function () {
+                return Promise.resolve();
+            });
+        }
+
+        it('should invoke the callback and fetch a secondary cluster when the cached primary cluster is up-to-date and within in the requested range', function (done) {
             var set = MeasurementSet.findSet(1, 1, 3000);
             var callCount = 0;
-            set.fetchBetween(1000, 3000, function () {
+            var promise = set.fetchBetween(1000, 3000, function () {
                 callCount++;
             });
             assert.equal(requests.length, 1);
@@ -92,7 +100,7 @@ describe('MeasurementSet', function () {
                 'clusterCount': 2,
                 'status': 'OK'});
 
-            Promise.resolve().then(function () {
+            waitForMeasurementSet().then(function () {
                 assert.equal(callCount, 1);
                 assert.equal(requests.length, 2);
                 assert.equal(requests[1].url, '../data/measurement-set-1-1-2000.json');
@@ -121,13 +129,15 @@ describe('MeasurementSet', function () {
                 'clusterCount': 3,
                 'status': 'OK'});
 
-            Promise.resolve().then(function () {
+            var callCount = 0;
+            waitForMeasurementSet().then(function () {
                 assert.equal(requests.length, 2);
                 assert.equal(requests[1].url, '../data/measurement-set-1-1-3000.json');
 
-                var callCount = 0;
                 set.fetchBetween(0, 7000, function () { callCount++; });
 
+                return waitForMeasurementSet();
+            }).then(function () {
                 assert.equal(callCount, 1);
                 assert.equal(requests.length, 4);
                 assert.equal(requests[2].url, '../data/measurement-set-1-1-2000.json');
@@ -158,7 +168,7 @@ describe('MeasurementSet', function () {
                 'clusterCount': 3,
                 'status': 'OK'});
 
-            Promise.resolve().then(function () {
+            waitForMeasurementSet().then(function () {
                 assert.equal(requests.length, 3);
                 assert.equal(requests[1].url, '../data/measurement-set-1-1-3000.json');
                 assert.equal(requests[2].url, '../data/measurement-set-1-1-4000.json');
@@ -187,23 +197,22 @@ describe('MeasurementSet', function () {
                 'clusterCount': 3,
                 'status': 'OK'});
 
-            Promise.resolve().then(function () {
+            var callCount = 0;
+            waitForMeasurementSet().then(function () {
                 assert.equal(requests.length, 2);
                 assert.equal(requests[1].url, '../data/measurement-set-1-1-4000.json');
-
-                var callCount = 0;
                 set.fetchBetween(1207, 1293, function () { callCount++; });
-
+                return waitForMeasurementSet();
+            }).then(function () {
                 assert.equal(callCount, 0);
                 assert.equal(requests.length, 3);
                 assert.equal(requests[2].url, '../data/measurement-set-1-1-2000.json');
-
                 set.fetchBetween(1964, 3401, function () { callCount++; });
-
+                return waitForMeasurementSet();
+            }).then(function () {
                 assert.equal(callCount, 0);
                 assert.equal(requests.length, 4);
                 assert.equal(requests[3].url, '../data/measurement-set-1-1-3000.json');
-
                 done();
             }).catch(function (error) {
                 done(error);
@@ -221,7 +230,7 @@ describe('MeasurementSet', function () {
 
             requests[0].reject(500);
 
-            Promise.resolve().then(function () {
+            waitForMeasurementSet().then(function () {
                 assert.equal(callCount, 1);
                 assert.equal(requests.length, 1);
                 done();
@@ -271,7 +280,7 @@ describe('MeasurementSet', function () {
 
             requests[0].reject(404);
 
-            Promise.resolve().then(function () {
+            waitForMeasurementSet().then(function () {
                 assert.equal(callCount, 0);
                 assert.equal(requests.length, 2);
                 assert.equal(requests[1].url, '../api/measurement-set?platform=1&metric=1');
@@ -306,7 +315,7 @@ describe('MeasurementSet', function () {
                 'clusterCount': 2,
                 'status': 'OK'});
 
-            Promise.resolve().then(function () {
+            waitForMeasurementSet().then(function () {
                 assert.equal(callCount, 1);
                 assert.equal(alternativeCallCount, 1);
                 assert.equal(requests.length, 1);
@@ -337,16 +346,20 @@ describe('MeasurementSet', function () {
             var callCountFor4000To5000 = 0;
             var callCountFor2000 = 0;
             var callCountFor2000To4000 = 0;
-            Promise.resolve().then(function () {
+            waitForMeasurementSet().then(function () {
                 assert.equal(callCountFor4000, 0);
                 assert.equal(requests.length, 2);
                 assert.equal(requests[1].url, '../data/measurement-set-1-1-4000.json');
 
                 set.fetchBetween(3708, 4800, function () { callCountFor4000To5000++; });
+                return waitForMeasurementSet();
+            }).then(function () {
                 assert.equal(callCountFor4000To5000, 1);
                 assert.equal(requests.length, 2);
 
                 set.fetchBetween(1207, 1293, function () { callCountFor2000++; });
+                return waitForMeasurementSet();
+            }).then(function () {
                 assert.equal(callCountFor2000, 0);
                 assert.equal(requests.length, 3);
                 assert.equal(requests[2].url, '../data/measurement-set-1-1-2000.json');
@@ -358,6 +371,7 @@ describe('MeasurementSet', function () {
                     'endTime': 2000,
                     'lastModified': 5000,
                     'status': 'OK'});
+                return waitForMeasurementSet();
             }).then(function () {
                 assert.equal(requests.length, 3);
                 assert.equal(callCountFor4000, 0);
@@ -365,7 +379,8 @@ describe('MeasurementSet', function () {
                 assert.equal(callCountFor2000, 1);
 
                 set.fetchBetween(1964, 3401, function () { callCountFor2000To4000++; });
-
+                return waitForMeasurementSet();
+            }).then(function () {
                 assert.equal(callCountFor2000To4000, 1);
                 assert.equal(requests.length, 4);
                 assert.equal(requests[3].url, '../data/measurement-set-1-1-3000.json');
@@ -377,6 +392,7 @@ describe('MeasurementSet', function () {
                     'endTime': 3000,
                     'lastModified': 5000,
                     'status': 'OK'});
+                return waitForMeasurementSet();
             }).then(function () {
                 assert.equal(callCountFor4000, 0);
                 assert.equal(callCountFor4000To5000, 1);
@@ -391,6 +407,7 @@ describe('MeasurementSet', function () {
                     'endTime': 4000,
                     'lastModified': 5000,
                     'status': 'OK'});
+                return waitForMeasurementSet();
             }).then(function () {
                 assert.equal(callCountFor4000, 1);
                 assert.equal(callCountFor4000To5000, 2);
@@ -406,4 +423,4 @@ describe('MeasurementSet', function () {
 
     });
 
-});
\ No newline at end of file
+});