Perf dashboard should automatically detect regressions
[WebKit-https.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 });
41
42 App.Builder = App.NameLabelModel.extend({
43     buildUrl: DS.attr('string'),
44     urlFromBuildNumber: function (buildNumber)
45     {
46         var template = this.get('buildUrl');
47         return template ? template.replace(/\$builderName/g, this.get('name')).replace(/\$buildNumber/g, buildNumber) : null;
48     }
49 });
50
51 App.BugTracker = App.NameLabelModel.extend({
52     bugUrl: DS.attr('string'),
53     newBugUrl: DS.attr('string'),
54     repositories: DS.hasMany('repository'),
55 });
56
57 App.DateArrayTransform = DS.Transform.extend({
58     deserialize: function (serialized)
59     {
60         return serialized.map(function (time) { return new Date(time); });
61     }
62 });
63
64 App.Platform = App.NameLabelModel.extend({
65     _metricSet: null,
66     _testSet: null,
67     metrics: DS.hasMany('metric'),
68     lastModified: DS.attr('dateArray'),
69     containsMetric: function (metric)
70     {
71         if (!this._metricSet)
72             this._metricSet = new Ember.Set(this.get('metrics'));
73         return this._metricSet.contains(metric);
74     },
75     lastModifiedTimeForMetric: function (metric)
76     {
77         var index = this.get('metrics').indexOf(metric);
78         if (index < 0)
79             return null;
80         return this.get('lastModified').objectAt(index);
81     },
82     containsTest: function (test)
83     {
84         if (!this._testSet) {
85             var set = new Ember.Set();
86             this.get('metrics').forEach(function (metric) {
87                 for (var test = metric.get('test'); test; test = test.get('parent'))
88                     set.push(test);
89             });
90             this._testSet = set;
91         }
92         return this._testSet.contains(test);
93     },
94 });
95
96 App.Repository = App.NameLabelModel.extend({
97     url: DS.attr('string'),
98     blameUrl: DS.attr('string'),
99     hasReportedCommits: DS.attr('boolean'),
100     urlForRevision: function (currentRevision)
101     {
102         return (this.get('url') || '').replace(/\$1/g, currentRevision);
103     },
104     urlForRevisionRange: function (from, to)
105     {
106         return (this.get('blameUrl') || '').replace(/\$1/g, from).replace(/\$2/g, to);
107     },
108 });
109
110 App.Dashboard = App.NameLabelModel.extend({
111     serialized: DS.attr('string'),
112     table: function ()
113     {
114         var json = this.get('serialized');
115         try {
116             var parsed = JSON.parse(json);
117         } catch (error) {
118             console.log("Failed to parse the grid:", error, json);
119             return [];
120         }
121         if (!parsed)
122             return [];
123         return this._normalizeTable(parsed);
124     }.property('serialized'),
125
126     rows: function ()
127     {
128         return this.get('table').slice(1);
129     }.property('table'),
130
131     headerColumns: function ()
132     {
133         var table = this.get('table');
134         if (!table || !table.length)
135             return [];
136         return table[0].map(function (name, index) {
137             return {label:name, index: index};
138         });
139     }.property('table'),
140
141     _normalizeTable: function (table)
142     {
143         var maxColumnCount = Math.max(table.map(function (column) { return column.length; }));
144         for (var i = 1; i < table.length; i++) {
145             var row = table[i];
146             for (var j = 1; j < row.length; j++) {
147                 if (row[j] && !(row[j] instanceof Array)) {
148                     console.log('Unrecognized (platform, metric) pair at column ' + i + ' row ' + j + ':' + row[j]);
149                     row[j] = [];
150                 }
151             }
152         }
153         return table;
154     },
155 });
156
157 App.MetricSerializer = App.PlatformSerializer = DS.RESTSerializer.extend({
158     normalizePayload: function (payload)
159     {
160         var results = {
161             platforms: this._normalizeIdMap(payload['all']),
162             builders: this._normalizeIdMap(payload['builders']),
163             tests: this._normalizeIdMap(payload['tests']).map(function (test) {
164                 test['parent'] = test['parentId'];
165                 return test;
166             }),
167             metrics: this._normalizeIdMap(payload['metrics']),
168             repositories: this._normalizeIdMap(payload['repositories']),
169             bugTrackers: this._normalizeIdMap(payload['bugTrackers']),
170             dashboards: [],
171         };
172
173         for (var testId in payload['tests']) {
174             var test = payload['tests'][testId];
175             var parent = payload['tests'][test['parent']];
176             if (!parent)
177                 continue;
178             if (!parent['childTests'])
179                 parent['childTests'] = [];
180             parent['childTests'].push(testId);
181         }
182
183         for (var metricId in payload['metrics']) {
184             var metric = payload['metrics'][metricId];
185             var test = payload['tests'][metric['test']];
186             if (!test['metrics'])
187                 test['metrics'] = [];
188             test['metrics'].push(metricId);
189         }
190
191         var id = 1;
192         var dashboardsInPayload = payload['dashboards'];
193         for (var dashboardName in dashboardsInPayload) {
194             results['dashboards'].push({
195                 id: id,
196                 name: dashboardName,
197                 serialized: JSON.stringify(dashboardsInPayload[dashboardName])
198             });
199             id++;
200         }
201
202         return results;
203     },
204     _normalizeIdMap: function (idMap)
205     {
206         var results = [];
207         for (var id in idMap) {
208             var definition = idMap[id];
209             definition['id'] = id;
210             results.push(definition);
211         }
212         return results;
213     }
214 });
215
216 App.PlatformAdapter = DS.RESTAdapter.extend({
217     buildURL: function (type, id)
218     {
219         return '../data/manifest.json';
220     },
221 });
222
223 App.MetricAdapter = DS.RESTAdapter.extend({
224     buildURL: function (type, id)
225     {
226         return '../data/manifest.json';
227     },
228 });
229
230 App.Manifest = Ember.Controller.extend({
231     platforms: null,
232     topLevelTests: null,
233     repositories: [],
234     repositoriesWithReportedCommits: [],
235     bugTrackers: [],
236     _platformById: {},
237     _metricById: {},
238     _builderById: {},
239     _repositoryById: {},
240     _dashboardByName: {},
241     _defaultDashboardName: null,
242     _fetchPromise: null,
243     fetch: function (store)
244     {
245         if (!this._fetchPromise)
246             this._fetchPromise = store.findAll('platform').then(this._fetchedManifest.bind(this, store));
247         return this._fetchPromise;
248     },
249     isFetched: function () { return !!this.get('platforms'); }.property('platforms'),
250     platform: function (id) { return this._platformById[id]; },
251     metric: function (id) { return this._metricById[id]; },
252     builder: function (id) { return this._builderById[id]; },
253     repository: function (id) { return this._repositoryById[id]; },
254     dashboardByName: function (name) { return this._dashboardByName[name]; },
255     defaultDashboardName: function () { return this._defaultDashboardName; },
256     _fetchedManifest: function (store)
257     {
258         var startTime = Date.now();
259         var self = this;
260
261         var topLevelTests = [];
262         store.all('test').forEach(function (test) {
263             var parent = test.get('parent');
264             if (!parent)
265                 topLevelTests.push(test);
266         });
267         this.set('topLevelTests', topLevelTests);
268
269         store.all('metric').forEach(function (metric) {
270             self._metricById[metric.get('id')] = metric;
271         });
272         var platforms = store.all('platform');
273         platforms.forEach(function (platform) {
274             self._platformById[platform.get('id')] = platform;
275         });
276         this.set('platforms', platforms);
277
278         store.all('builder').forEach(function (builder) {
279             self._builderById[builder.get('id')] = builder;
280         });
281
282         var repositories = store.all('repository');
283         repositories.forEach(function (repository) {
284             self._repositoryById[repository.get('id')] = repository;
285         });
286         this.set('repositories', repositories.sortBy('id'));
287         this.set('repositoriesWithReportedCommits',
288             repositories.filter(function (repository) { return repository.get('hasReportedCommits'); }));
289
290         this.set('bugTrackers', store.all('bugTracker').sortBy('name'));
291
292         var dashboards = store.all('dashboard').sortBy('name');
293         this.set('dashboards', dashboards);
294         dashboards.forEach(function (dashboard) { self._dashboardByName[dashboard.get('name')] = dashboard; });
295         this._defaultDashboardName = dashboards.length ? dashboards[0].get('name') : null;
296     },
297     fetchRunsWithPlatformAndMetric: function (store, platformId, metricId, testGroupId, useCache)
298     {
299         Ember.assert("Can't cache results for test groups", !(testGroupId && useCache));
300         var self = this;
301         return Ember.RSVP.all([
302             RunsData.fetchRuns(platformId, metricId, testGroupId, useCache),
303             this.fetch(store),
304         ]).then(function (values) {
305             var response = values[0];
306
307             var platform = App.Manifest.platform(platformId);
308             var metric = App.Manifest.metric(metricId);
309
310             return {
311                 platform: platform,
312                 metric: metric,
313                 data: response ? self._formatFetchedData(metric.get('name'), response.configurations) : null,
314                 shouldRefetch: !response || +response.lastModified < +platform.lastModifiedTimeForMetric(metric),
315             };
316         });
317     },
318     _formatFetchedData: function (metricName, configurations)
319     {
320         var unit = RunsData.unitFromMetricName(metricName);
321         var smallerIsBetter = RunsData.isSmallerBetter(unit);
322
323         var useSI = unit == 'bytes';
324         var unitSuffix = unit ? ' ' + unit : '';
325         var deltaFormatterWithoutSign = useSI ? d3.format('.2s') : d3.format('.2g');
326
327         var currentTimeSeries = configurations.current.timeSeriesByCommitTime(false);
328         var baselineTimeSeries = configurations.baseline ? configurations.baseline.timeSeriesByCommitTime(false) : null;
329         var targetTimeSeries = configurations.target ? configurations.target.timeSeriesByCommitTime(false) : null;
330         var unfilteredCurrentTimeSeries, unfilteredBaselineTimeSeries, unfilteredTargetTimeSeries;
331
332         return {
333             current: currentTimeSeries,
334             baseline: baselineTimeSeries,
335             target: targetTimeSeries,
336             unit: unit,
337             formatWithUnit: function (value) { return this.formatter(value) + unitSuffix; },
338             formatWithDeltaAndUnit: function (value, delta)
339             {
340                 return this.formatter(value) + (delta && !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
341             },
342             formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
343             deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
344             smallerIsBetter: smallerIsBetter,
345             showOutlier: function (show)
346             {
347                 if (!unfilteredCurrentTimeSeries) {
348                     unfilteredCurrentTimeSeries = configurations.current.timeSeriesByCommitTime(true);
349                     unfilteredBaselineTimeSeries = configurations.baseline ? configurations.baseline.timeSeriesByCommitTime(true) : null;
350                     unfilteredTargetTimeSeries = configurations.target ? configurations.target.timeSeriesByCommitTime(true) : null;
351                 }
352                 this.current = show ? unfilteredCurrentTimeSeries : currentTimeSeries;
353                 this.baseline = show ? unfilteredBaselineTimeSeries : baselineTimeSeries;
354                 this.target = show ? unfilteredTargetTimeSeries : targetTimeSeries;
355             },
356         };
357     }
358 }).create();