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