https://bugs.webkit.org/show_bug.cgi?id=141339
Reviewed by Chris Dumez.
Added the support for multiple dashboard pages. Also added the status of the latest data point.
e.g. "5% better than target"
* public/v2/app.css: Tweaked the styles to work around the fact Ember.js creates empty script elements.
Also hid the border lines around charts on the dashboard page for a cleaner look.
* public/v2/app.js:
(App.IndexRoute): Added. Navigate to /dashboard/<defaultDashboardName> once the manifest.json is loaded.
(App.IndexRoute.beforeModel): Added.
(App.DashboardRoute): Added.
(App.DashboardRoute.model): Added. Return the dashboard specified by the name.
(App.CustomDashboardRoute): Added. This route is used for a customized dashboard specified by "grid".
(App.CustomDashboardRoute.model): Create a dashboard model from "grid" query parameter.
(App.CustomDashboardRoute.renderTemplate): Use the dashboard template.
(App.DashboardController): Renamed from App.IndexController.
(App.DashboardController.modelChanged): Renamed from gridChanged. Removed the code to deal with "grid"
and "defaultDashboard" as these are taken care of by newly added routers.
(App.DashboardController.computeGrid): Renamed from updateGrid. No longer updates "grid" since this is
now done in actions.toggleEditMode.
(App.DashboardController.actions.toggleEditMode): Navigate to CustomDashboardRoute when start editing
an existing dashboard.
(App.Pane.computeStatus): Moved from App.PaneController so that to be called in App.Pane.latestStatus.
Also moved the code to compute the delta with respect to the previous data point from _updateDetails.
(App.Pane._relativeDifferentToLaterPointInTimeSeries): Ditto.
(App.Pane.latestStatus): Added. Used by the dashboard template to show the status of the latest result.
(App.createChartData): Added deltaFormatter to show less significant digits for differences.
(App.PaneController._updateDetails): Updated per changes to computeStatus.
* public/v2/chart-pane.css: Added style rules for the status labels on the dashboard.
* public/v2/data.js:
(TimeSeries.prototype.lastPoint): Added.
* public/v2/index.html: Prefetch manifest.json as soon as possible, show the latest data points' status
on the dashboard, and enumerate all predefined dashboards.
* public/v2/interactive-chart.js:
(App.InteractiveChartComponent._relayoutDataAndAxes): Slightly adjust the offset at which we show unit
for the dashboard page.
* public/v2/manifest.js:
(App.Dashboard): Inherit from App.NameLabelModel now that each predefined dashboard has a name.
(App.MetricSerializer.normalizePayload): Parse all predefined dashboards instead of a single dashboard.
IDs are generated for each dashboard for forward compatibility.
(App.Manifest):
(App.Manifest.dashboardByName): Added.
(App.Manifest.defaultDashboardName): Added.
(App.Manifest._fetchedManifest): Create dashboard model objects for all predefined ones.
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@179763
268f45cc-cd09-0410-ab3c-
d52691b4dbfc
+2015-02-06 Ryosuke Niwa <rniwa@webkit.org>
+
+ New perf dashboard should have multiple dashboard pages
+ https://bugs.webkit.org/show_bug.cgi?id=141339
+
+ Reviewed by Chris Dumez.
+
+ Added the support for multiple dashboard pages. Also added the status of the latest data point.
+ e.g. "5% better than target"
+
+ * public/v2/app.css: Tweaked the styles to work around the fact Ember.js creates empty script elements.
+ Also hid the border lines around charts on the dashboard page for a cleaner look.
+
+ * public/v2/app.js:
+ (App.IndexRoute): Added. Navigate to /dashboard/<defaultDashboardName> once the manifest.json is loaded.
+ (App.IndexRoute.beforeModel): Added.
+ (App.DashboardRoute): Added.
+ (App.DashboardRoute.model): Added. Return the dashboard specified by the name.
+ (App.CustomDashboardRoute): Added. This route is used for a customized dashboard specified by "grid".
+ (App.CustomDashboardRoute.model): Create a dashboard model from "grid" query parameter.
+ (App.CustomDashboardRoute.renderTemplate): Use the dashboard template.
+ (App.DashboardController): Renamed from App.IndexController.
+ (App.DashboardController.modelChanged): Renamed from gridChanged. Removed the code to deal with "grid"
+ and "defaultDashboard" as these are taken care of by newly added routers.
+ (App.DashboardController.computeGrid): Renamed from updateGrid. No longer updates "grid" since this is
+ now done in actions.toggleEditMode.
+ (App.DashboardController.actions.toggleEditMode): Navigate to CustomDashboardRoute when start editing
+ an existing dashboard.
+
+ (App.Pane.computeStatus): Moved from App.PaneController so that to be called in App.Pane.latestStatus.
+ Also moved the code to compute the delta with respect to the previous data point from _updateDetails.
+ (App.Pane._relativeDifferentToLaterPointInTimeSeries): Ditto.
+ (App.Pane.latestStatus): Added. Used by the dashboard template to show the status of the latest result.
+
+ (App.createChartData): Added deltaFormatter to show less significant digits for differences.
+
+ (App.PaneController._updateDetails): Updated per changes to computeStatus.
+
+ * public/v2/chart-pane.css: Added style rules for the status labels on the dashboard.
+
+ * public/v2/data.js:
+ (TimeSeries.prototype.lastPoint): Added.
+
+ * public/v2/index.html: Prefetch manifest.json as soon as possible, show the latest data points' status
+ on the dashboard, and enumerate all predefined dashboards.
+
+ * public/v2/interactive-chart.js:
+ (App.InteractiveChartComponent._relayoutDataAndAxes): Slightly adjust the offset at which we show unit
+ for the dashboard page.
+
+ * public/v2/manifest.js:
+ (App.Dashboard): Inherit from App.NameLabelModel now that each predefined dashboard has a name.
+ (App.MetricSerializer.normalizePayload): Parse all predefined dashboards instead of a single dashboard.
+ IDs are generated for each dashboard for forward compatibility.
+ (App.Manifest):
+ (App.Manifest.dashboardByName): Added.
+ (App.Manifest.defaultDashboardName): Added.
+ (App.Manifest._fetchedManifest): Create dashboard model objects for all predefined ones.
+
2015-02-05 Ryosuke Niwa <rniwa@webkit.org>
Move commits viewer to the end of details view
margin: 0;
line-height: 1rem;
}
-#navigation li:not(:last-child) a {
+#navigation li:not(:last-of-type) a {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
-#navigation li:not(:first-child) a {
+#navigation li:not(:first-of-type) a {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
table.dashboard tbody td .chart {
- border: solid 1px #ddd;
+ border: solid 0px #ddd;
border-radius: 0.5rem;
margin: 0.5rem 0.5rem;
}
window.App = Ember.Application.create();
App.Router.map(function () {
+ this.resource('customDashboard', {path: 'dashboard/custom'});
+ this.resource('dashboard', {path: 'dashboard/:name'});
this.resource('charts', {path: 'charts'});
this.resource('analysis', {path: 'analysis'});
this.resource('analysisTask', {path: 'analysis/task/:taskId'});
}.property('platformId', 'metricId'),
});
-App.IndexController = Ember.Controller.extend({
+App.IndexRoute = Ember.Route.extend({
+ beforeModel: function ()
+ {
+ var self = this;
+ App.Manifest.fetch(this.store).then(function () {
+ self.transitionTo('dashboard', App.Manifest.defaultDashboardName());
+ });
+ },
+});
+
+App.DashboardRoute = Ember.Route.extend({
+ model: function (param)
+ {
+ return App.Manifest.fetch(this.store).then(function () {
+ return App.Manifest.dashboardByName(param.name);
+ });
+ },
+});
+
+App.CustomDashboardRoute = Ember.Route.extend({
+ controllerName: 'dashboard',
+ model: function (param)
+ {
+ return this.store.createRecord('dashboard', {serialized: param.grid});
+ },
+ renderTemplate: function()
+ {
+ this.render('dashboard');
+ }
+});
+
+App.DashboardController = Ember.Controller.extend({
queryParams: ['grid', 'numberOfDays'],
- _previousGrid: {},
headerColumns: [],
rows: [],
numberOfDays: 7,
editMode: false,
- gridChanged: function ()
+ modelChanged: function ()
{
- var grid = this.get('grid');
- if (grid === this._previousGrid)
- return;
-
- var dashboard = null;
- if (grid) {
- dashboard = this.store.createRecord('dashboard', {serialized: grid});
- if (!dashboard.get('headerColumns').length)
- dashboard = null;
- }
- if (!dashboard)
- dashboard = App.Manifest.get('defaultDashboard');
+ var dashboard = this.get('model');
if (!dashboard)
return;
}));
this.set('emptyRow', new Array(columnCount));
- }.observes('grid', 'App.Manifest.defaultDashboard').on('init'),
+ }.observes('model').on('init'),
- updateGrid: function()
+ computeGrid: function()
{
var headers = this.get('headerColumns').map(function (header) { return header.label; });
var table = [headers].concat(this.get('rows').map(function (row) {
return platformAndMetric[0] || platformAndMetric[1] ? platformAndMetric : [];
}));
}));
- this._previousGrid = JSON.stringify(table);
- this.set('grid', this._previousGrid);
+ return JSON.stringify(table);
},
_sharedDomainChanged: function ()
toggleEditMode: function ()
{
this.toggleProperty('editMode');
- if (!this.get('editMode'))
- this.updateGrid();
+ if (this.get('editMode'))
+ this.transitionToRoute('dashboard', 'custom', {name: null, queryParams: {grid: this.computeGrid()}});
+ else
+ this.set('grid', this.computeGrid());
},
},
if (typeof(id) == "string")
return !!id.match(/^[A-Za-z0-9_]+$/);
return false;
- }
+ },
+ computeStatus: function (currentPoint, previousPoint)
+ {
+ 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';
+
+ var valueDelta = previousPoint ? chartData.deltaFormatter(currentPoint.value - previousPoint.value) : null;
+ if (valueDelta && valueDelta > 0)
+ valueDelta = '+' + valueDelta;
+
+ return {className: className, label: label, currentValue: chartData.formatter(currentPoint.value), valueDelta: valueDelta};
+ },
+ _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;
+ },
+ latestStatus: function ()
+ {
+ var chartData = this.get('chartData');
+ if (!chartData || !chartData.current)
+ return null;
+
+ var lastPoint = chartData.current.lastPoint();
+ if (!lastPoint)
+ return null;
+
+ return this.computeStatus(lastPoint, chartData.current.previousPoint(lastPoint));
+ }.property('chartData'),
});
App.createChartData = function (data)
target: runs.target ? runs.target.timeSeriesByCommitTime() : null,
unit: data.unit,
formatter: data.useSI ? d3.format('.4s') : d3.format('.4g'),
+ deltaFormatter: data.useSI ? d3.format('.2s') : d3.format('.2g'),
smallerIsBetter: data.smallerIsBetter,
};
}
}
var currentMeasurement;
- var oldMeasurement;
+ var previousPoint;
if (currentPoint) {
currentMeasurement = currentPoint.measurement;
- var previousPoint = currentPoint.series.previousPoint(currentPoint);
- oldMeasurement = previousPoint ? previousPoint.measurement : null;
+ previousPoint = currentPoint.series.previousPoint(currentPoint);
} else {
currentMeasurement = selectedPoints[selectedPoints.length - 1].measurement;
- oldMeasurement = selectedPoints[0].measurement;
+ previousPoint = selectedPoints[0];
}
+ var oldMeasurement = previousPoint ? previousPoint.measurement : null;
var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
var revisions = App.Manifest.get('repositories')
buildURL = builder.urlFromBuildNumber(buildNumber);
}
- var chartData = this.get('chartData');
- var valueDiff = oldMeasurement ? chartData.formatter(currentMeasurement.mean() - oldMeasurement.mean()) : null;
- if (valueDiff && valueDiff > 0)
- valueDiff = '+' + valueDiff;
-
this.set('details', Ember.Object.create({
- status: this._computeStatus(currentPoint),
- currentValue: chartData.formatter(currentMeasurement.mean()),
- valueDiff: valueDiff,
+ status: this.get('model').computeStatus(currentPoint, previousPoint),
buildNumber: buildNumber,
buildURL: buildURL,
buildTime: currentMeasurement.formattedBuildTime(),
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;
- }
});
.chart path.baseline {
stroke: #f66;
}
-.chart-pane .status .worse {
+.chart-pane .status .worse,
+.dashboard-status .worse {
color: #c33;
}
.chart path.target {
stroke: #66f;
}
-.chart-pane .status .better {
+.chart-pane .status .better,
+.dashboard-status .better {
color: #33c;
}
+.dashboard-status .status-label {
+ margin-left: 1rem;
+}
+
.chart .axis,
.chart .domain {
fill: none;
TimeSeries.prototype.series = function () { return this._series; }
+TimeSeries.prototype.lastPoint = function ()
+{
+ if (!this._series || !this._series.length)
+ return null;
+ return this._series[this._series.length - 1];
+}
+
TimeSeries.prototype.previousPoint = function (point)
{
if (!point.seriesIndex)
<head>
<meta charset="utf-8">
<title>WebKit Performance Monitor (Beta)</title>
+
+ <link rel="prefetch" href="../data/manifest.json">
+ <script type="application/json" src="../data/manifest.json"></script>
+
+ <link rel="stylesheet" href="app.css">
+ <link rel="stylesheet" href="chart-pane.css">
+
+ <script src="js/jquery.min.js" defer></script>
<script src="js/jquery.min.js" defer></script>
<script src="js/handlebars.js" defer></script>
<script src="js/ember.js" defer></script>
<script src="popup.js" defer></script>
<script src="interactive-chart.js" defer></script>
<script src="commits-viewer.js" defer></script>
- <link rel="stylesheet" href="app.css">
- <link rel="stylesheet" href="chart-pane.css">
- <script type="text/x-handlebars" data-template-name="index">
+ <script type="text/x-handlebars" data-template-name="dashboard">
<header id="header">
{{partial "navbar"}}
{{view App.NumberOfDaysControlView tagName="ul" numberOfDays=numberOfDays}}
{{/if}}
{{else}}
{{#if chartData}}
+ <div class="dashboard-status">
+ {{#if latestStatus}}
+ {{latestStatus.currentValue}} {{chartData.unit}}
+ {{#if latestStatus.label}}
+ <span {{bind-attr class=":status-label latestStatus.className"}}>{{latestStatus.label}}</span>
+ {{/if}}
+ {{/if}}
+ </div>
{{#link-to 'charts' (query-params paneList=paneList since=controller.since)}}
- {{interactive-chart
- chartData=chartData
- domain=controller.sharedDomain
- enableSelection=false}}
+ {{interactive-chart
+ chartData=chartData
+ domain=controller.sharedDomain
+ enableSelection=false}}
{{/link-to}}
{{else}}
{{#if failure}}
<tr>
<th>Current</th>
<td>
- {{details.currentValue}} {{chartData.unit}}
- {{#if details.valueDiff}}
- ({{details.valueDiff}} {{chartData.unit}})
+ {{details.status.currentValue}} {{chartData.unit}}
+ {{#if details.status.valueDelta}}
+ ({{details.status.valueDelta}} {{chartData.unit}})
{{/if}}
{{#if details.status.label}}
<br>
<nav id="navigation" role="navigation">
<h1><a href="#">WebKit Perf Monitor</a></h1>
<ul>
- {{#link-to 'index' tagName='li'}}
- {{#link-to 'index'}}Dashboard{{/link-to}}
- {{/link-to}}
+ {{#each App.Manifest.dashboards}}
+ {{#if name}}
+ {{#link-to 'dashboard' name tagName='li'}}
+ {{#link-to 'dashboard' name}}{{label}}{{/link-to}}
+ {{/link-to}}
+ {{/if}}
+ {{/each}}
{{#link-to 'charts' tagName='li'}}
{{#link-to 'charts'}}Charts{{/link-to}}
{{/link-to}}
this._yAxisLabels.call(this._yAxis);
if (this._yAxisUnitContainer)
this._yAxisUnitContainer.remove();
- var x = - 3 * this._rem;
+ var x = - 3.2 * this._rem;
var y = this._contentHeight / 2;
this._yAxisUnitContainer = this._yAxisLabels.append("text")
.attr("transform", "rotate(90 0 0) translate(" + y + ", " + (-x) + ")")
},
});
-App.Dashboard = App.Model.extend({
+App.Dashboard = App.NameLabelModel.extend({
serialized: DS.attr('string'),
table: function ()
{
metrics: this._normalizeIdMap(payload['metrics']),
repositories: this._normalizeIdMap(payload['repositories']),
bugTrackers: this._normalizeIdMap(payload['bugTrackers']),
- dashboards: [{id: 1, serialized: JSON.stringify(payload['defaultDashboard'])}],
+ dashboards: [],
};
for (var testId in payload['tests']) {
test['metrics'].push(metricId);
}
+ var id = 1;
+ var dashboardsInPayload = payload['dashboards'];
+ for (var dashboardName in dashboardsInPayload) {
+ results['dashboards'].push({
+ id: id,
+ name: dashboardName,
+ serialized: JSON.stringify(dashboardsInPayload[dashboardName])
+ });
+ id++;
+ }
+
return results;
},
_normalizeIdMap: function (idMap)
_metricById: {},
_builderById: {},
_repositoryById: {},
+ _dashboardByName: {},
+ _defaultDashboardName: null,
_fetchPromise: null,
fetch: function (store)
{
metric: function (id) { return this._metricById[id]; },
builder: function (id) { return this._builderById[id]; },
repository: function (id) { return this._repositoryById[id]; },
+ dashboardByName: function (name) { return this._dashboardByName[name]; },
+ defaultDashboardName: function () { return this._defaultDashboardName; },
_fetchedManifest: function (store)
{
var startTime = Date.now();
this.set('bugTrackers', store.all('bugTracker').sortBy('name'));
- this.set('defaultDashboard', store.all('dashboard').objectAt(0));
+ var dashboards = store.all('dashboard').sortBy('name');
+ this.set('dashboards', dashboards);
+ dashboards.forEach(function (dashboard) { self._dashboardByName[dashboard.get('name')] = dashboard; });
+ this._defaultDashboardName = dashboards.length ? dashboards[0].get('name') : null;
},
fetchRunsWithPlatformAndMetric: function (store, platformId, metricId)
{