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