dbf86a9d97b998bb615be443eb93f6e63ff1dae7
[WebKit.git] / Websites / perf.webkit.org / public / v2 / data.js
1 // We don't use DS.Model for these object types because we can't afford to process millions of them.
2
3 var PrivilegedAPI = {
4     _token: null,
5     _expiration: null,
6     _maxNetworkLatency: 3 * 60 * 1000 /* 3 minutes */,
7 };
8
9 PrivilegedAPI.sendRequest = function (url, parameters)
10 {
11     return this._generateTokenInServerIfNeeded().then(function (token) {
12         return PrivilegedAPI._post(url, $.extend({token: token}, parameters));
13     });
14 }
15
16 PrivilegedAPI._generateTokenInServerIfNeeded = function ()
17 {
18     var self = this;
19     return new Ember.RSVP.Promise(function (resolve, reject) {
20         if (self._token && self._expiration > Date.now() + self._maxNetworkLatency)
21             resolve(self._token);
22
23         PrivilegedAPI._post('generate-csrf-token')
24             .then(function (result, reject) {
25                 self._token = result['token'];
26                 self._expiration = new Date(result['expiration']);
27                 resolve(self._token);
28             }).catch(reject);
29     });
30 }
31
32 PrivilegedAPI._post = function (url, parameters)
33 {
34     return new Ember.RSVP.Promise(function (resolve, reject) {
35         $.ajax({
36             url: '../privileged-api/' + url,
37             type: 'POST',
38             contentType: 'application/json',
39             data: parameters ? JSON.stringify(parameters) : '{}',
40             dataType: 'json',
41         }).done(function (data) {
42             if (data.status != 'OK')
43                 reject(data.status);
44             else
45                 resolve(data);
46         }).fail(function (xhr, status, error) {
47             reject(xhr.status + (error ? ', ' + error : '') + '\n\nWith response:\n' + xhr.responseText);
48         });
49     });
50 }
51
52 var CommitLogs = {
53     _cachedCommitsByRepository: {}
54 };
55
56 CommitLogs.fetchForTimeRange = function (repository, from, to, keyword)
57 {
58     var params = [];
59     if (from && to) {
60         params.push(['from', from]);
61         params.push(['to', to]);
62     }
63     if (keyword)
64         params.push(['keyword', keyword]);
65
66     // FIXME: We should be able to use the cache if all commits in the range have been cached.
67     var useCache = from && to && !keyword;
68
69     var url = '../api/commits/' + repository + '/?' + params.map(function (keyValue) {
70         return encodeURIComponent(keyValue[0]) + '=' + encodeURIComponent(keyValue[1]);
71     }).join('&');
72
73     if (useCache) {
74         var cachedCommitsForRange = CommitLogs._cachedCommitsBetween(repository, from, to);
75         if (cachedCommitsForRange)
76             return new Ember.RSVP.Promise(function (resolve) { resolve(cachedCommitsForRange); });
77     }
78
79     return new Ember.RSVP.Promise(function (resolve, reject) {
80         $.getJSON(url, function (data) {
81             if (data.status != 'OK') {
82                 reject(data.status);
83                 return;
84             }
85
86             var fetchedCommits = data.commits;
87             fetchedCommits.forEach(function (commit) { commit.time = new Date(commit.time); });
88
89             if (useCache)
90                 CommitLogs._cacheConsecutiveCommits(repository, from, to, fetchedCommits);
91
92             resolve(fetchedCommits);
93         }).fail(function (xhr, status, error) {
94             reject(xhr.status + (error ? ', ' + error : ''));
95         })
96     });
97 }
98
99 CommitLogs._cachedCommitsBetween = function (repository, from, to)
100 {
101     var cachedCommits = this._cachedCommitsByRepository[repository];
102     if (!cachedCommits)
103         return null;
104
105     var startCommit = cachedCommits.commitsByRevision[from];
106     var endCommit = cachedCommits.commitsByRevision[to];
107     if (!startCommit || !endCommit)
108         return null;
109
110     return cachedCommits.commitsByTime.slice(startCommit.cacheIndex, endCommit.cacheIndex + 1);
111 }
112
113 CommitLogs._cacheConsecutiveCommits = function (repository, from, to, consecutiveCommits)
114 {
115     var cachedCommits = this._cachedCommitsByRepository[repository];
116     if (!cachedCommits) {
117         cachedCommits = {commitsByRevision: {}, commitsByTime: []};
118         this._cachedCommitsByRepository[repository] = cachedCommits;
119     }
120
121     consecutiveCommits.forEach(function (commit) {
122         if (cachedCommits.commitsByRevision[commit.revision])
123             return;
124         cachedCommits.commitsByRevision[commit.revision] = commit;
125         cachedCommits.commitsByTime.push(commit);
126     });
127
128     cachedCommits.commitsByTime.sort(function (a, b) { return a.time - b.time; });
129     cachedCommits.commitsByTime.forEach(function (commit, index) { commit.cacheIndex = index; });
130 }
131
132
133 function Measurement(rawData)
134 {
135     this._raw = rawData;
136
137     var latestTime = -1;
138     var revisions = this._raw['revisions'];
139     // FIXME: Fix this in the server side.
140     if (Array.isArray(revisions))
141         revisions = {};
142     this._raw['revisions'] = revisions;
143
144     for (var repositoryId in revisions) {
145         var commitTimeOrUndefined = revisions[repositoryId][1]; // e.g. ["162190", 1389945046000]
146         if (latestTime < commitTimeOrUndefined)
147             latestTime = commitTimeOrUndefined;
148     }
149     this._latestCommitTime = latestTime !== -1 ? new Date(latestTime) : null;
150     this._buildTime = new Date(this._raw['buildTime']);
151     this._confidenceInterval = undefined;
152     this._formattedRevisions = undefined;
153 }
154
155 Measurement.prototype.revisionForRepository = function (repositoryId)
156 {
157     var revisions = this._raw['revisions'];
158     var rawData = revisions[repositoryId];
159     return rawData ? rawData[0] : null;
160 }
161
162 Measurement.prototype.commitTimeForRepository = function (repositoryId)
163 {
164     var revisions = this._raw['revisions'];
165     var rawData = revisions[repositoryId];
166     return rawData ? new Date(rawData[1]) : null;
167 }
168
169 Measurement.prototype.formattedRevisions = function (previousMeasurement)
170 {
171     var revisions = this._raw['revisions'];
172     var previousRevisions = previousMeasurement ? previousMeasurement._raw['revisions'] : null;
173     var formattedRevisions = {};
174     for (var repositoryId in revisions) {
175         var currentRevision = revisions[repositoryId][0];
176         var previousRevision = previousRevisions && previousRevisions[repositoryId] ? previousRevisions[repositoryId][0] : null;
177         var formatttedRevision = Measurement.formatRevisionRange(currentRevision, previousRevision);
178         formattedRevisions[repositoryId] = formatttedRevision;
179     }
180
181     return formattedRevisions;
182 }
183
184 Measurement.formatRevisionRange = function (currentRevision, previousRevision)
185 {
186     var revisionChanged = false;
187     if (previousRevision == currentRevision)
188         previousRevision = null;
189     else
190         revisionChanged = true;
191
192     var revisionPrefix = '';
193     var revisionDelimiter = '-';
194     var label = '';
195     if (parseInt(currentRevision) == currentRevision) { // e.g. r12345.
196         currentRevision = parseInt(currentRevision);
197         revisionPrefix = 'r';
198         if (previousRevision)
199             previousRevision = (parseInt(previousRevision) + 1);
200     } else if (currentRevision.indexOf(' ') >= 0) // e.g. 10.9 13C64.
201         revisionDelimiter = ' - ';
202     else if (currentRevision.length == 40) { // e.g. git hash
203         var formattedCurrentHash = currentRevision.substring(0, 8);
204         if (previousRevision)
205             label = previousRevision.substring(0, 8) + '..' + formattedCurrentHash;
206         else
207             label = formattedCurrentHash;
208     }
209
210     if (!label) {
211         if (previousRevision)
212             label = revisionPrefix + previousRevision + revisionDelimiter + revisionPrefix + currentRevision;
213         else
214             label = revisionPrefix + currentRevision;
215     }
216
217     return {
218         label: label,
219         previousRevision: previousRevision,
220         currentRevision: currentRevision,
221         revisionChanged: revisionChanged
222     };
223 }
224
225 Measurement.prototype.id = function ()
226 {
227     return this._raw['id'];
228 }
229
230 Measurement.prototype.mean = function()
231 {
232     return this._raw['mean'];
233 }
234
235 Measurement.prototype.confidenceInterval = function()
236 {
237     if (this._confidenceInterval === undefined) {
238         var delta = Statistics.confidenceIntervalDelta(0.95, this._raw["iterationCount"], this._raw["sum"], this._raw["squareSum"]);
239         var mean = this.mean();
240         this._confidenceInterval = isNaN(delta) ? null : [mean - delta, mean + delta];
241     }
242     return this._confidenceInterval;
243 }
244
245 Measurement.prototype.latestCommitTime = function()
246 {
247     return this._latestCommitTime || this._buildTime;
248 }
249
250 Measurement.prototype.buildId = function()
251 {
252     return this._raw['build'];
253 }
254
255 Measurement.prototype.buildNumber = function ()
256 {
257     return this._raw['buildNumber'];
258 }
259
260 Measurement.prototype.builderId = function ()
261 {
262     return this._raw['builder'];
263 }
264
265 Measurement.prototype.buildTime = function()
266 {
267     return this._buildTime;
268 }
269
270 Measurement.prototype.formattedBuildTime = function ()
271 {
272     return Measurement._formatDate(this.buildTime());
273 }
274
275 Measurement._formatDate = function (date)
276 {
277     return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
278 }
279
280 Measurement.prototype.bugs = function ()
281 {
282     return this._raw['bugs'];
283 }
284
285 Measurement.prototype.hasBugs = function ()
286 {
287     var bugs = this.bugs();
288     return bugs && Object.keys(bugs).length;
289 }
290
291 function RunsData(rawData)
292 {
293     this._measurements = rawData.map(function (run) { return new Measurement(run); });
294 }
295
296 RunsData.prototype.count = function ()
297 {
298     return this._measurements.length;
299 }
300
301 RunsData.prototype.timeSeriesByCommitTime = function ()
302 {
303     return new TimeSeries(this._measurements.map(function (measurement) {
304         var confidenceInterval = measurement.confidenceInterval();
305         return {
306             measurement: measurement,
307             time: measurement.latestCommitTime(),
308             value: measurement.mean(),
309             interval: measurement.confidenceInterval(),
310         };
311     }));
312 }
313
314 RunsData.prototype.timeSeriesByBuildTime = function ()
315 {
316     return new TimeSeries(this._measurements.map(function (measurement) {
317         return {
318             measurement: measurement,
319             time: measurement.buildTime(),
320             value: measurement.mean(),
321             interval: measurement.confidenceInterval(),
322         };
323     }));
324 }
325
326 // FIXME: We need to devise a way to fetch runs in multiple chunks so that
327 // we don't have to fetch the entire time series to just show the last 3 days.
328 RunsData.fetchRuns = function (platformId, metricId, testGroupId, useCache)
329 {
330     var url = useCache ? '../data/' : '../api/runs/';
331
332     url += platformId + '-' + metricId + '.json';
333     if (testGroupId)
334         url += '?testGroup=' + testGroupId;
335
336     return new Ember.RSVP.Promise(function (resolve, reject) {
337         $.getJSON(url, function (response) {
338             if (response.status != 'OK') {
339                 reject(response.status);
340                 return;
341             }
342             delete response.status;
343
344             var data = response.configurations;
345             for (var config in data)
346                 data[config] = new RunsData(data[config]);
347             
348             if (response.lastModified)
349                 response.lastModified = new Date(response.lastModified);
350
351             resolve(response);
352         }).fail(function (xhr, status, error) {
353             if (xhr.status == 404 && useCache)
354                 resolve(null);
355             else
356                 reject(xhr.status + (error ? ', ' + error : ''));
357         })
358     });
359 }
360
361 function TimeSeries(series)
362 {
363     this._series = series.sort(function (a, b) { return a.time - b.time; });
364     var self = this;
365     var min = undefined;
366     var max = undefined;
367     this._series.forEach(function (point, index) {
368         point.series = self;
369         point.seriesIndex = index;
370         if (min === undefined || min > point.value)
371             min = point.value;
372         if (max === undefined || max < point.value)
373             max = point.value;
374     });
375     this._min = min;
376     this._max = max;
377 }
378
379 TimeSeries.prototype.findPointByBuild = function (buildId)
380 {
381     return this._series.find(function (point) { return point.measurement.buildId() == buildId; })
382 }
383
384 TimeSeries.prototype.findPointByRevisions = function (revisions)
385 {
386     return this._series.find(function (point, index) {
387         for (var repositoryId in revisions) {
388             if (point.measurement.revisionForRepository(repositoryId) != revisions[repositoryId])
389                 return false;
390         }
391         return true;
392     });
393 }
394
395 TimeSeries.prototype.findPointByMeasurementId = function (measurementId)
396 {
397     return this._series.find(function (point) { return point.measurement.id() == measurementId; });
398 }
399
400 TimeSeries.prototype.findPointAfterTime = function (time)
401 {
402     return this._series.find(function (point) { return point.time >= time; });
403 }
404
405 TimeSeries.prototype.seriesBetweenPoints = function (startPoint, endPoint)
406 {
407     if (!startPoint.seriesIndex || !endPoint.seriesIndex)
408         return null;
409     return this._series.slice(startPoint.seriesIndex, endPoint.seriesIndex + 1);
410 }
411
412 TimeSeries.prototype.minMaxForTimeRange = function (startTime, endTime, ignoreOutlier)
413 {
414     var data = this._series;
415     var i = 0;
416     if (startTime !== undefined) {
417         for (i = 0; i < data.length; i++) {
418             var point = data[i];
419             if (point.time >= startTime)
420                 break;
421         }
422         if (i)
423             i--;
424     }
425     var min = Number.MAX_VALUE;
426     var max = Number.MIN_VALUE;
427     for (; i < data.length; i++) {
428         var point = data[i];
429         if (point.isOutlier && ignoreOutlier)
430             continue;
431         var currentMin = point.interval ? point.interval[0] : point.value;
432         var currentMax = point.interval ? point.interval[1] : point.value;
433
434         if (currentMin < min)
435             min = currentMin;
436         if (currentMax > max)
437             max = currentMax;
438
439         if (point.time >= endTime)
440             break;
441     }
442     return [min, max];
443 }
444
445 TimeSeries.prototype.series = function () { return this._series; }
446
447 TimeSeries.prototype.lastPoint = function ()
448 {
449     if (!this._series || !this._series.length)
450         return null;
451     return this._series[this._series.length - 1];
452 }
453
454 TimeSeries.prototype.previousPoint = function (point)
455 {
456     if (!point.seriesIndex)
457         return null;
458     return this._series[point.seriesIndex - 1];
459 }
460
461 TimeSeries.prototype.nextPoint = function (point)
462 {
463     if (!point.seriesIndex)
464         return null;
465     return this._series[point.seriesIndex + 1];
466 }