Perf dashboard should extend baseline and target to the future
[WebKit.git] / Websites / perf.webkit.org / public / v2 / manifest.js
1 App.Model = DS.Model;
2
3 App.NameLabelModel = DS.Model.extend({
4     name: DS.attr('string'),
5     label: function ()
6     {
7         return this.get('name');
8     }.property('name'),
9 });
10
11 App.Test = App.NameLabelModel.extend({
12     url: DS.attr('string'),
13     parent: DS.belongsTo('test', {inverse: 'childTests'}),
14     childTests: DS.hasMany('test', {inverse: 'parent'}),
15     metrics: DS.hasMany('metrics'),
16 });
17
18 App.Metric = App.NameLabelModel.extend({
19     test: DS.belongsTo('test'),
20     aggregator: DS.attr('string'),
21     label: function ()
22     {
23         return this.get('name') + (this.get('aggregator') ? ' : ' + this.get('aggregator') : '');
24     }.property('name', 'aggregator'),
25     path: function ()
26     {
27         var path = [];
28         var parent = this.get('test');
29         while (parent) {
30             path.push(parent.get('name'));
31             parent = parent.get('parent');
32         }
33         return path.reverse();
34     }.property('name', 'test'),
35     fullName: function ()
36     {
37         return this.get('path').join(' \u220b ') /* ∋ */
38             + ' : ' + this.get('label');
39     }.property('path', 'label'),
40     childMetrics: function ()
41     {
42         var test = this.get('test');
43         var childMetrics = [];
44         test.get('childTests').forEach(function (childTest) {
45             childTest.get('metrics').forEach(function (metric) {
46                 childMetrics.push(metric);
47             });
48         });
49         return childMetrics;
50     }.property('test.childTests'),
51 });
52
53 App.Builder = App.NameLabelModel.extend({
54     buildUrl: DS.attr('string'),
55     urlFromBuildNumber: function (buildNumber)
56     {
57         var template = this.get('buildUrl');
58         return template ? template.replace(/\$builderName/g, this.get('name')).replace(/\$buildNumber/g, buildNumber) : null;
59     }
60 });
61
62 App.BugTracker = App.NameLabelModel.extend({
63     bugUrl: DS.attr('string'),
64     newBugUrl: DS.attr('string'),
65     repositories: DS.hasMany('repository'),
66     urlFromBugNumber: function (bugNumber) {
67         var template = this.get('bugUrl');
68         return template && bugNumber ? template.replace(/\$number/g, bugNumber) : null;
69     }
70 });
71
72 App.DateArrayTransform = DS.Transform.extend({
73     deserialize: function (serialized)
74     {
75         return serialized.map(function (time) { return new Date(time); });
76     }
77 });
78
79 App.Platform = App.NameLabelModel.extend({
80     _metricSet: null,
81     _testSet: null,
82     metrics: DS.hasMany('metric'),
83     lastModified: DS.attr('dateArray'),
84     containsMetric: function (metric)
85     {
86         if (!this._metricSet)
87             this._metricSet = new Ember.Set(this.get('metrics'));
88         return this._metricSet.contains(metric);
89     },
90     lastModifiedTimeForMetric: function (metric)
91     {
92         var index = this.get('metrics').indexOf(metric);
93         if (index < 0)
94             return null;
95         return this.get('lastModified').objectAt(index);
96     },
97     containsTest: function (test)
98     {
99         if (!this._testSet) {
100             var set = new Ember.Set();
101             this.get('metrics').forEach(function (metric) {
102                 for (var test = metric.get('test'); test; test = test.get('parent'))
103                     set.push(test);
104             });
105             this._testSet = set;
106         }
107         return this._testSet.contains(test);
108     },
109 });
110
111 App.Repository = App.NameLabelModel.extend({
112     url: DS.attr('string'),
113     blameUrl: DS.attr('string'),
114     hasReportedCommits: DS.attr('boolean'),
115     urlForRevision: function (currentRevision)
116     {
117         return (this.get('url') || '').replace(/\$1/g, currentRevision);
118     },
119     urlForRevisionRange: function (from, to)
120     {
121         return (this.get('blameUrl') || '').replace(/\$1/g, from).replace(/\$2/g, to);
122     },
123 });
124
125 App.Dashboard = App.NameLabelModel.extend({
126     serialized: DS.attr('string'),
127     table: function ()
128     {
129         var json = this.get('serialized');
130         try {
131             var parsed = JSON.parse(json);
132         } catch (error) {
133             console.log("Failed to parse the grid:", error, json);
134             return [];
135         }
136         if (!parsed)
137             return [];
138         return this._normalizeTable(parsed);
139     }.property('serialized'),
140
141     rows: function ()
142     {
143         return this.get('table').slice(1);
144     }.property('table'),
145
146     headerColumns: function ()
147     {
148         var table = this.get('table');
149         if (!table || !table.length)
150             return [];
151         return table[0].map(function (name, index) {
152             return {label:name, index: index};
153         });
154     }.property('table'),
155
156     _normalizeTable: function (table)
157     {
158         var maxColumnCount = Math.max(table.map(function (column) { return column.length; }));
159         for (var i = 1; i < table.length; i++) {
160             var row = table[i];
161             for (var j = 1; j < row.length; j++) {
162                 if (row[j] && !(row[j] instanceof Array)) {
163                     console.log('Unrecognized (platform, metric) pair at column ' + i + ' row ' + j + ':' + row[j]);
164                     row[j] = [];
165                 }
166             }
167         }
168         return table;
169     },
170 });
171
172 App.MetricSerializer = App.PlatformSerializer = DS.RESTSerializer.extend({
173     normalizePayload: function (payload)
174     {
175         var results = {
176             platforms: this._normalizeIdMap(payload['all']),
177             builders: this._normalizeIdMap(payload['builders']),
178             tests: this._normalizeIdMap(payload['tests']).map(function (test) {
179                 test['parent'] = test['parentId'];
180                 return test;
181             }),
182             metrics: this._normalizeIdMap(payload['metrics']),
183             repositories: this._normalizeIdMap(payload['repositories']),
184             bugTrackers: this._normalizeIdMap(payload['bugTrackers']),
185             dashboards: [],
186         };
187
188         for (var testId in payload['tests']) {
189             var test = payload['tests'][testId];
190             var parent = payload['tests'][test['parent']];
191             if (!parent)
192                 continue;
193             if (!parent['childTests'])
194                 parent['childTests'] = [];
195             parent['childTests'].push(testId);
196         }
197
198         for (var metricId in payload['metrics']) {
199             var metric = payload['metrics'][metricId];
200             var test = payload['tests'][metric['test']];
201             if (!test['metrics'])
202                 test['metrics'] = [];
203             test['metrics'].push(metricId);
204         }
205
206         var id = 1;
207         var dashboardsInPayload = payload['dashboards'];
208         for (var dashboardName in dashboardsInPayload) {
209             results['dashboards'].push({
210                 id: id,
211                 name: dashboardName,
212                 serialized: JSON.stringify(dashboardsInPayload[dashboardName])
213             });
214             id++;
215         }
216
217         var siteTitle = payload['siteTitle'];
218         if (siteTitle) {
219             App.Manifest.set('siteTitle', siteTitle);
220             document.title = siteTitle;
221         }
222
223         return results;
224     },
225     _normalizeIdMap: function (idMap)
226     {
227         var results = [];
228         for (var id in idMap) {
229             var definition = idMap[id];
230             definition['id'] = id;
231             results.push(definition);
232         }
233         return results;
234     }
235 });
236
237 App.PlatformAdapter = DS.RESTAdapter.extend({
238     buildURL: function (type, id)
239     {
240         return '../data/manifest.json';
241     },
242 });
243
244 App.MetricAdapter = DS.RESTAdapter.extend({
245     buildURL: function (type, id)
246     {
247         return '../data/manifest.json';
248     },
249 });
250
251 App.Manifest = Ember.Controller.extend({
252     platforms: null,
253     topLevelTests: null,
254     repositories: [],
255     repositoriesWithReportedCommits: [],
256     bugTrackers: [],
257     _platformById: {},
258     _metricById: {},
259     _builderById: {},
260     _repositoryById: {},
261     _dashboardByName: {},
262     _defaultDashboardName: null,
263     _fetchPromise: null,
264     fetch: function (store)
265     {
266         if (!this._fetchPromise)
267             this._fetchPromise = store.findAll('platform').then(this._fetchedManifest.bind(this, store));
268         return this._fetchPromise;
269     },
270     isFetched: function () { return !!this.get('platforms'); }.property('platforms'),
271     platform: function (id) { return this._platformById[id]; },
272     metric: function (id) { return this._metricById[id]; },
273     builder: function (id) { return this._builderById[id]; },
274     repository: function (id) { return this._repositoryById[id]; },
275     dashboardByName: function (name) { return this._dashboardByName[name]; },
276     defaultDashboardName: function () { return this._defaultDashboardName; },
277     _fetchedManifest: function (store)
278     {
279         var startTime = Date.now();
280         var self = this;
281
282         var topLevelTests = [];
283         store.all('test').forEach(function (test) {
284             var parent = test.get('parent');
285             if (!parent)
286                 topLevelTests.push(test);
287         });
288         this.set('topLevelTests', topLevelTests);
289
290         store.all('metric').forEach(function (metric) {
291             self._metricById[metric.get('id')] = metric;
292         });
293         var platforms = store.all('platform');
294         platforms.forEach(function (platform) {
295             self._platformById[platform.get('id')] = platform;
296         });
297         this.set('platforms', platforms);
298
299         store.all('builder').forEach(function (builder) {
300             self._builderById[builder.get('id')] = builder;
301         });
302
303         var repositories = store.all('repository');
304         repositories.forEach(function (repository) {
305             self._repositoryById[repository.get('id')] = repository;
306         });
307         this.set('repositories', repositories.sortBy('id'));
308         this.set('repositoriesWithReportedCommits',
309             repositories.filter(function (repository) { return repository.get('hasReportedCommits'); }));
310
311         this.set('bugTrackers', store.all('bugTracker').sortBy('name'));
312
313         var dashboards = store.all('dashboard').toArray();
314         this.set('dashboards', dashboards);
315         dashboards.forEach(function (dashboard) { self._dashboardByName[dashboard.get('name')] = dashboard; });
316         this._defaultDashboardName = dashboards.length ? dashboards[0].get('name') : null;
317     },
318     fetchRunsWithPlatformAndMetric: function (store, platformId, metricId, testGroupId, useCache)
319     {
320         Ember.assert("Can't cache results for test groups", !(testGroupId && useCache));
321         var self = this;
322         return Ember.RSVP.all([
323             RunsData.fetchRuns(platformId, metricId, testGroupId, useCache),
324             this.fetch(store),
325         ]).then(function (values) {
326             var response = values[0];
327
328             var platform = App.Manifest.platform(platformId);
329             var metric = App.Manifest.metric(metricId);
330
331             return {
332                 platform: platform,
333                 metric: metric,
334                 data: response ? self._formatFetchedData(metric.get('name'), response.configurations) : null,
335                 shouldRefetch: !response || +response.lastModified < +platform.lastModifiedTimeForMetric(metric),
336             };
337         });
338     },
339     _makeFormatter: function (unit, sigFig, alwaysShowSign)
340     {
341         var isMiliseconds = false;
342         if (unit == 'ms') {
343             isMiliseconds = true;
344             unit = 's';
345         }
346         var divisor = unit == 'B' ? 1024 : 1000;
347
348         var suffix = ['\u03BC', 'm', '', 'K', 'M', 'G', 'T', 'P', 'E'];
349         var threshold = sigFig >= 3 ? divisor : (divisor / 10);
350         return function (value) {
351             var i;
352             var sign = value >= 0 ? (alwaysShowSign ? '+' : '') : '-';
353             value = Math.abs(value);
354             for (i = isMiliseconds ? 1 : 2; value < 1 && i > 0; i--)
355                 value *= divisor;
356             for (; value >= threshold; i++)
357                 value /= divisor;
358             return sign + value.toPrecision(Math.max(2, sigFig)) + ' ' + suffix[i] + (unit || '');
359         }
360     },
361     _formatFetchedData: function (metricName, configurations)
362     {
363         var unit = RunsData.unitFromMetricName(metricName);
364         var smallerIsBetter = RunsData.isSmallerBetter(unit);
365         var deltaFormatterWithoutSign = this._makeFormatter(unit, 2, false);
366
367         var currentTimeSeries = configurations.current.timeSeriesByCommitTime(false);
368         var baselineTimeSeries = configurations.baseline ? configurations.baseline.timeSeriesByCommitTime(false, true) : null;
369         var targetTimeSeries = configurations.target ? configurations.target.timeSeriesByCommitTime(false, true) : null;
370
371         var unfilteredCurrentTimeSeries = configurations.current.timeSeriesByCommitTime(true);
372         var unfilteredBaselineTimeSeries = configurations.baseline ? configurations.baseline.timeSeriesByCommitTime(true, true) : null;
373         var unfilteredTargetTimeSeries = configurations.target ? configurations.target.timeSeriesByCommitTime(true, true) : null;
374
375         return {
376             current: currentTimeSeries,
377             baseline: baselineTimeSeries,
378             target: targetTimeSeries,
379
380             unfilteredCurrentTimeSeries: unfilteredCurrentTimeSeries,
381             unfilteredBaselineTimeSeries: unfilteredBaselineTimeSeries,
382             unfilteredTargetTimeSeries: unfilteredTargetTimeSeries,
383
384             formatWithDeltaAndUnit: function (value, delta)
385             {
386                 return this.formatter(value) + (delta && !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '');
387             },
388             formatter: this._makeFormatter(unit, 4, false),
389             deltaFormatter: this._makeFormatter(unit, 2, true),
390             smallerIsBetter: smallerIsBetter,
391             showOutlier: function (show)
392             {
393                 this.current = show ? unfilteredCurrentTimeSeries : currentTimeSeries;
394                 this.baseline = show ? unfilteredBaselineTimeSeries : baselineTimeSeries;
395                 this.target = show ? unfilteredTargetTimeSeries : targetTimeSeries;
396             },
397         };
398     }
399 }).create();