New perf dashboard should compare results to baseline and target
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 5 Feb 2015 22:34:13 +0000 (22:34 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 5 Feb 2015 22:34:13 +0000 (22:34 +0000)
https://bugs.webkit.org/show_bug.cgi?id=141286

Reviewed by Chris Dumez.

Compare the selected value against baseline and target values as done in v1. e.g. "5% below target"
Also use d3.format to format the selected value to show four significant figures.

* public/v2/app.js:
(App.Pane.searchCommit):
(App.Pane._fetch): Create time series here via createChartData so that _computeStatus can use them
to compute the status text without having to recreate them.
(App.createChartData): Added.
(App.PaneController._updateDetails): Use 3d.format on current and old values.
(App.PaneController._computeStatus): Added. Computes the status text.
(App.PaneController._relativeDifferentToLaterPointInTimeSeries): Added.
(App.AnalysisTaskController._fetchedManifest): Use createChartData as done in App.Pane._fetch. Also
format the values using chartData.formatter.

* public/v2/chart-pane.css: Enlarge the status text. Show the status text in red if it's worse than
the baseline and in blue if it's better than the target.

* public/v2/data.js:
(TimeSeries.prototype.findPointAfterTime): Added.

* public/v2/index.html: Added a new tbody for the status text and the selected value. Also fixed
the bug that we were not showing the old value's unit.

* public/v2/interactive-chart.js:
(App.InteractiveChartComponent._constructGraphIfPossible): Use chartData.formatter. Also cleaned up
the code to show the baseline and the target lines.

* public/v2/manifest.js:
(App.Manifest.fetchRunsWithPlatformAndMetric): Added smallerIsBetter.

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/v2/app.js
Websites/perf.webkit.org/public/v2/chart-pane.css
Websites/perf.webkit.org/public/v2/data.js
Websites/perf.webkit.org/public/v2/index.html
Websites/perf.webkit.org/public/v2/interactive-chart.js
Websites/perf.webkit.org/public/v2/manifest.js

index da440ec..0cbb575 100644 (file)
@@ -1,5 +1,42 @@
 2015-02-05  Ryosuke Niwa  <rniwa@webkit.org>
 
+        New perf dashboard should compare results to baseline and target
+        https://bugs.webkit.org/show_bug.cgi?id=141286
+
+        Reviewed by Chris Dumez.
+
+        Compare the selected value against baseline and target values as done in v1. e.g. "5% below target"
+        Also use d3.format to format the selected value to show four significant figures.
+
+        * public/v2/app.js:
+        (App.Pane.searchCommit):
+        (App.Pane._fetch): Create time series here via createChartData so that _computeStatus can use them
+        to compute the status text without having to recreate them.
+        (App.createChartData): Added.
+        (App.PaneController._updateDetails): Use 3d.format on current and old values.
+        (App.PaneController._computeStatus): Added. Computes the status text.
+        (App.PaneController._relativeDifferentToLaterPointInTimeSeries): Added.
+        (App.AnalysisTaskController._fetchedManifest): Use createChartData as done in App.Pane._fetch. Also
+        format the values using chartData.formatter.
+
+        * public/v2/chart-pane.css: Enlarge the status text. Show the status text in red if it's worse than
+        the baseline and in blue if it's better than the target.
+
+        * public/v2/data.js:
+        (TimeSeries.prototype.findPointAfterTime): Added.
+
+        * public/v2/index.html: Added a new tbody for the status text and the selected value. Also fixed
+        the bug that we were not showing the old value's unit.
+
+        * public/v2/interactive-chart.js:
+        (App.InteractiveChartComponent._constructGraphIfPossible): Use chartData.formatter. Also cleaned up
+        the code to show the baseline and the target lines.
+
+        * public/v2/manifest.js:
+        (App.Manifest.fetchRunsWithPlatformAndMetric): Added smallerIsBetter.
+
+2015-02-05  Ryosuke Niwa  <rniwa@webkit.org>
+
         Unreviewed build fix.
 
         * public/v2/app.js:
index 5365b46..7bc230a 100755 (executable)
@@ -281,7 +281,7 @@ App.Pane = Ember.Object.extend({
         CommitLogs.fetchForTimeRange(repositoryId, null, null, keyword).then(function (commits) {
             if (self.isDestroyed || !self.get('chartData') || !commits.length)
                 return;
-            var currentRuns = self.get('chartData').current.timeSeriesByCommitTime().series();
+            var currentRuns = self.get('chartData').current.series();
             if (!currentRuns.length)
                 return;
 
@@ -329,7 +329,7 @@ App.Pane = Ember.Object.extend({
             App.Manifest.fetchRunsWithPlatformAndMetric(this.get('store'), platformId, metricId).then(function (result) {
                 self.set('platform', result.platform);
                 self.set('metric', result.metric);
-                self.set('chartData', result.runs);
+                self.set('chartData', App.createChartData(result));
             }, function (result) {
                 if (!result || typeof(result) === "string")
                     self.set('failure', 'Failed to fetch the JSON with an error: ' + result);
@@ -365,6 +365,19 @@ App.Pane = Ember.Object.extend({
     }
 });
 
+App.createChartData = function (data)
+{
+    var runs = data.runs;
+    return {
+        current: runs.current.timeSeriesByCommitTime(),
+        baseline: runs.baseline ? runs.baseline.timeSeriesByCommitTime() : null,
+        target: runs.target ? runs.target.timeSeriesByCommitTime() : null,
+        unit: data.unit,
+        formatter: data.useSI ? d3.format('.4s') : d3.format('.4g'),
+        smallerIsBetter: data.smallerIsBetter,
+    };
+}
+
 App.encodePrettifiedJSON = function (plain)
 {
     function numberIfPossible(string) {
@@ -749,9 +762,11 @@ App.PaneController = Ember.ObjectController.extend({
                 buildURL = builder.urlFromBuildNumber(buildNumber);
         }
 
+        var chartData = this.get('chartData');
         this.set('details', Ember.Object.create({
-            currentValue: currentMeasurement.mean().toFixed(2),
-            oldValue: oldMeasurement && selectedPoints ? oldMeasurement.mean().toFixed(2) : null,
+            status: this._computeStatus(currentPoint),
+            currentValue: chartData.formatter(currentMeasurement.mean()),
+            oldValue: oldMeasurement && selectedPoints ? chartData.formatter(oldMeasurement.mean()) : null,
             buildNumber: buildNumber,
             buildURL: buildURL,
             buildTime: currentMeasurement.formattedBuildTime(),
@@ -764,6 +779,40 @@ App.PaneController = Ember.ObjectController.extend({
         var points = this.get('selectedPoints');
         this.set('cannotAnalyze', !this.get('newAnalysisTaskName') || !points || points.length < 2);
     }.observes('newAnalysisTaskName'),
+    _computeStatus: function (currentPoint)
+    {
+        var chartData = this.get('chartData');
+
+        var diffFromBaseline = this._relativeDifferentToLaterPointInTimeSeries(currentPoint, chartData.baseline);
+        var diffFromTarget = this._relativeDifferentToLaterPointInTimeSeries(currentPoint, chartData.target);
+
+        var label = '';
+        var className = '';
+        var formatter = d3.format('.3p');
+
+        var smallerIsBetter = chartData.smallerIsBetter;
+        if (diffFromBaseline !== undefined && diffFromBaseline > 0 == smallerIsBetter) {
+            label = formatter(Math.abs(diffFromBaseline)) + ' ' + (smallerIsBetter ? 'above' : 'below') + ' baseline';
+            className = 'worse';
+        } else if (diffFromTarget !== undefined && diffFromTarget < 0 == smallerIsBetter) {
+            label = formatter(Math.abs(diffFromTarget)) + ' ' + (smallerIsBetter ? 'below' : 'above') + ' target';
+            className = 'better';
+        } else if (diffFromTarget !== undefined)
+            label = formatter(Math.abs(diffFromTarget)) + ' until target';
+
+        return {className: className, label: label};
+    },
+    _relativeDifferentToLaterPointInTimeSeries: function (currentPoint, timeSeries)
+    {
+        if (!currentPoint || !timeSeries)
+            return undefined;
+        
+        var referencePoint = timeSeries.findPointAfterTime(currentPoint.time);
+        if (!referencePoint)
+            return undefined;
+
+        return (currentPoint.value - referencePoint.value) / referencePoint.value;
+    }
 });
 
 
@@ -835,17 +884,18 @@ App.AnalysisTaskController = Ember.Controller.extend({
         highlightedItems[start.measurement.id()] = true;
         highlightedItems[end.measurement.id()] = true;
 
+        var chartData = App.createChartData(data);
         var formatedPoints = currentTimeSeries.seriesBetweenPoints(start, end).map(function (point, index) {
             return {
                 id: point.measurement.id(),
                 measurement: point.measurement,
                 label: 'Point ' + (index + 1),
-                value: point.value + (runs.unit ? ' ' + runs.unit : ''),
+                value: chartData.formatter(point.value) + (data.unit ? ' ' + data.unit : ''),
             };
         });
 
         var margin = (end.time - start.time) * 0.1;
-        this.set('chartData', runs);
+        this.set('chartData', chartData);
         this.set('chartDomain', [start.time - margin, +end.time + margin]);
         this.set('highlightedItems', highlightedItems);
         this.set('analysisPoints', formatedPoints);
index 9c680b0..d8e113e 100755 (executable)
 .chart-pane .details-table td table td {
     word-break: break-word;
     border-top: solid 1px #ccc;
+    border-bottom: solid 1px #ccc;
     padding: 0.2rem;
 }
 
     content: " : ";
 }
 
+.chart-pane .details-table .status th {
+    visibility: hidden;
+}
+
+.chart-pane .details-table .status td {
+    font-size: 1rem;
+}
+
 .chart-pane .chart {
     height: 100%;
 }
     stroke-width: 1.5px;
 }
 
-.chart .commit-time-line {
-    stroke: #999;
-}
-
 .chart .dot {
     fill: #666;
     stroke: none;
     opacity: 0.8;
 }
 
-.chart path.area.baseline {
+.chart path.current {
+    stroke: #999;
+}
+
+.chart path.baseline {
     stroke: #f66;
-    fill: #fdd;
-    opacity: 0.4;
+}
+.chart-pane .status .worse {
+    color: #c33;
 }
 
-.chart path.area.target {
+.chart path.target {
     stroke: #66f;
-    fill: #ddf;
-    opacity: 0.4;
+}
+.chart-pane .status .better {
+    color: #33c;
 }
 
 .chart .axis,
index 2da2fef..3338212 100755 (executable)
@@ -360,6 +360,11 @@ TimeSeries.prototype.findPointByMeasurementId = function (measurementId)
     return this._series.find(function (point) { return point.measurement.id() == measurementId; });
 }
 
+TimeSeries.prototype.findPointAfterTime = function (time)
+{
+    return this._series.find(function (point) { return point.time >= time; });
+}
+
 TimeSeries.prototype.seriesBetweenPoints = function (startPoint, endPoint)
 {
     if (!startPoint.seriesIndex || !endPoint.seriesIndex)
index 0147cf7..dd9b486 100755 (executable)
                 {{/if}}
             {{/each}}
             </tbody>
-            <tr><th>Current</th><td>{{details.currentValue}} {{chartData.unit}}
-            {{#if details.oldValue}}
-                (from {{details.oldValue}})
-            {{/if}}</td></tr>
+            <tbody class="status">
+                <tr>
+                    <th>Current</th>
+                    <td>
+                        {{details.currentValue}} {{chartData.unit}}
+                        {{#if details.oldValue}}
+                            (from {{details.oldValue}} {{chartData.unit}})
+                        {{/if}}
+                        {{#if details.status.label}}
+                            <br>
+                            <span {{bind-attr class=details.status.className}}>{{details.status.label}}</span>
+                        {{/if}}
+                    </td>
+                </tr>
+            </tbody>
             {{#if details.buildNumber}}
                 <tr>
                     <th>Build</th>
index ac17e18..f9e97af 100644 (file)
@@ -66,8 +66,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
         }
 
         if (this.get('showYAxis')) {
-            this._yAxis = d3.svg.axis().scale(this._y).orient("left").ticks(6).tickFormat(
-                chartData.useSI ? d3.format("s") : d3.format(".3g"));
+            this._yAxis = d3.svg.axis().scale(this._y).orient("left").ticks(6).tickFormat(chartData.formatter);
             this._yAxisLabels = svg.append("g")
                 .attr("class", "y axis");
         }
@@ -100,31 +99,24 @@ App.InteractiveChartComponent = Ember.Component.extend({
             this._highlights.remove();
         this._highlights = null;
 
-        this._currentTimeSeries = chartData.current.timeSeriesByCommitTime();
+        this._currentTimeSeries = chartData.current;
         this._currentTimeSeriesData = this._currentTimeSeries.series();
-        this._baselineTimeSeries = chartData.baseline ? chartData.baseline.timeSeriesByCommitTime() : null;
-        this._targetTimeSeries = chartData.target ? chartData.target.timeSeriesByCommitTime() : null;
+        this._baselineTimeSeries = chartData.baseline;
+        this._targetTimeSeries = chartData.target;
 
         this._yAxisUnit = chartData.unit;
 
-        var minMax = this._minMaxForAllTimeSeries();
-        var smallEnoughValue = minMax[0] - (minMax[1] - minMax[0]) * 10;
-        var largeEnoughValue = minMax[1] + (minMax[1] - minMax[0]) * 10;
-
-        // FIXME: Flip the sides based on smallerIsBetter-ness.
         if (this._baselineTimeSeries) {
-            var data = this._baselineTimeSeries.series();
-            this._areas.push(this._clippedContainer
+            this._paths.push(this._clippedContainer
                 .append("path")
-                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [point.value, largeEnoughValue]}; }))
-                .attr("class", "area baseline"));
+                .datum(this._baselineTimeSeries.series())
+                .attr("class", "baseline"));
         }
         if (this._targetTimeSeries) {
-            var data = this._targetTimeSeries.series();
-            this._areas.push(this._clippedContainer
+            this._paths.push(this._clippedContainer
                 .append("path")
-                .datum(data.map(function (point) { return {time: point.time, value: point.value, interval: point.interval ? point.interval : [smallEnoughValue, point.value]}; }))
-                .attr("class", "area target"));
+                .datum(this._targetTimeSeries.series())
+                .attr("class", "target"));
         }
 
         this._areas.push(this._clippedContainer
@@ -135,7 +127,7 @@ App.InteractiveChartComponent = Ember.Component.extend({
         this._paths.push(this._clippedContainer
             .append("path")
             .datum(this._currentTimeSeriesData)
-            .attr("class", "commit-time-line"));
+            .attr("class", "current"));
 
         this._dots.push(this._clippedContainer
             .selectAll(".dot")
index c04d180..6ef1d8e 100755 (executable)
@@ -281,12 +281,9 @@ App.Manifest = Ember.Controller.extend({
                 'Heap': 'bytes',
                 'Allocations': 'bytes'
             }[suffix];
+            var smallerIsBetter = unit != 'fps' && unit != '/s'; // Assume smaller is better for unit-less metrics.
 
-            // FIXME: Include this information in JSON and process it in RunsData.fetchRuns
-            runs.unit = unit;
-            runs.useSI = unit == 'bytes';
-
-            return {platform: platform, metric: metric, runs: runs};
+            return {platform: platform, metric: metric, runs: runs, unit: unit, useSI: unit == 'bytes', smallerIsBetter: smallerIsBetter};
         });
     },
 }).create();