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