Perf dashboard treats Speedometer and JetStream as smaller is better
[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         'Score': 'pt',
413     }[suffix];
414     return unit;
415 }
416
417 RunsData.isSmallerBetter = function (unit)
418 {
419     return unit != 'fps' && unit != '/s' && unit != 'pt';
420 }
421
422 function TimeSeries(series)
423 {
424     this._series = series.sort(function (a, b) {
425         var diff = a.time - b.time;
426         return diff ? diff : a.secondaryTime - b.secondaryTime;
427     });
428
429     var self = this;
430     var min = undefined;
431     var max = undefined;
432     this._series.forEach(function (point, index) {
433         point.series = self;
434         point.seriesIndex = index;
435         if (min === undefined || min > point.value)
436             min = point.value;
437         if (max === undefined || max < point.value)
438             max = point.value;
439     });
440     this._min = min;
441     this._max = max;
442 }
443
444 TimeSeries.prototype.findPointByIndex = function (index)
445 {
446     if (!this._series || index < 0 || index >= this._series.length)
447         return null;
448     return this._series[index];
449 }
450
451 TimeSeries.prototype.findPointByBuild = function (buildId)
452 {
453     return this._series.find(function (point) { return point.measurement.buildId() == buildId; })
454 }
455
456 TimeSeries.prototype.findPointByRevisions = function (revisions)
457 {
458     return this._series.find(function (point, index) {
459         for (var repositoryId in revisions) {
460             if (point.measurement.revisionForRepository(repositoryId) != revisions[repositoryId])
461                 return false;
462         }
463         return true;
464     });
465 }
466
467 TimeSeries.prototype.findPointByMeasurementId = function (measurementId)
468 {
469     return this._series.find(function (point) { return point.measurement.id() == measurementId; });
470 }
471
472 TimeSeries.prototype.findPointAfterTime = function (time)
473 {
474     return this._series.find(function (point) { return point.time >= time; });
475 }
476
477 TimeSeries.prototype.seriesBetweenPoints = function (startPoint, endPoint)
478 {
479     if (!startPoint.seriesIndex || !endPoint.seriesIndex)
480         return null;
481     return this._series.slice(startPoint.seriesIndex, endPoint.seriesIndex + 1);
482 }
483
484 TimeSeries.prototype.minMaxForTimeRange = function (startTime, endTime, ignoreOutlier)
485 {
486     var data = this._series;
487     var i = 0;
488     if (startTime !== undefined) {
489         for (i = 0; i < data.length; i++) {
490             var point = data[i];
491             if (point.time >= startTime)
492                 break;
493         }
494         if (i)
495             i--;
496     }
497     var min = Number.MAX_VALUE;
498     var max = Number.MIN_VALUE;
499     for (; i < data.length; i++) {
500         var point = data[i];
501         if (point.isOutlier && ignoreOutlier)
502             continue;
503         var currentMin = point.interval ? point.interval[0] : point.value;
504         var currentMax = point.interval ? point.interval[1] : point.value;
505
506         if (currentMin < min)
507             min = currentMin;
508         if (currentMax > max)
509             max = currentMax;
510
511         if (point.time >= endTime)
512             break;
513     }
514     return [min, max];
515 }
516
517 TimeSeries.prototype.series = function () { return this._series; }
518
519 TimeSeries.prototype.rawValues = function ()
520 {
521     return this._series.map(function (point) { return point.value });
522 }
523
524 TimeSeries.prototype.lastPoint = function ()
525 {
526     if (!this._series || !this._series.length)
527         return null;
528     return this._series[this._series.length - 1];
529 }
530
531 TimeSeries.prototype.previousPoint = function (point)
532 {
533     if (!point.seriesIndex)
534         return null;
535     return this._series[point.seriesIndex - 1];
536 }
537
538 TimeSeries.prototype.nextPoint = function (point)
539 {
540     if (!point.seriesIndex)
541         return null;
542     return this._series[point.seriesIndex + 1];
543 }
544
545 if (typeof module != 'undefined') {
546     Statistics = require('./js/statistics.js');
547     module.exports.Measurement = Measurement;
548     module.exports.RunsData = RunsData;
549     module.exports.TimeSeries = TimeSeries;
550 }