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