68317ce97cb500c7a1ed6758c33f40e9266e8903
[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                 reject(data.status);
44             else
45                 resolve(data);
46         }).fail(function (xhr, status, error) {
47             console.log(xhr);
48             reject(xhr.status + (error ? ', ' + error : ''));
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     console.log('Fecthing ' + url);
81
82     return new Ember.RSVP.Promise(function (resolve, reject) {
83         $.getJSON(url, function (data) {
84             if (data.status != 'OK') {
85                 reject(data.status);
86                 return;
87             }
88
89             var fetchedCommits = data.commits;
90             fetchedCommits.forEach(function (commit) { commit.time = new Date(commit.time.replace(' ', 'T')); });
91
92             if (useCache)
93                 CommitLogs._cacheConsecutiveCommits(repository, from, to, fetchedCommits);
94
95             resolve(fetchedCommits);
96         }).fail(function (xhr, status, error) {
97             reject(xhr.status + (error ? ', ' + error : ''));
98         })
99     });
100 }
101
102 CommitLogs._cachedCommitsBetween = function (repository, from, to)
103 {
104     var cachedCommits = this._cachedCommitsByRepository[repository];
105     if (!cachedCommits)
106         return null;
107
108     var startCommit = cachedCommits.commitsByRevision[from];
109     var endCommit = cachedCommits.commitsByRevision[to];
110     if (!startCommit || !endCommit)
111         return null;
112
113     return cachedCommits.commitsByTime.slice(startCommit.cacheIndex, endCommit.cacheIndex + 1);
114 }
115
116 CommitLogs._cacheConsecutiveCommits = function (repository, from, to, consecutiveCommits)
117 {
118     var cachedCommits = this._cachedCommitsByRepository[repository];
119     if (!cachedCommits) {
120         cachedCommits = {commitsByRevision: {}, commitsByTime: []};
121         this._cachedCommitsByRepository[repository] = cachedCommits;
122     }
123
124     consecutiveCommits.forEach(function (commit) {
125         if (cachedCommits.commitsByRevision[commit.revision])
126             return;
127         cachedCommits.commitsByRevision[commit.revision] = commit;
128         cachedCommits.commitsByTime.push(commit);
129     });
130
131     cachedCommits.commitsByTime.sort(function (a, b) { return a.time - b.time; });
132     cachedCommits.commitsByTime.forEach(function (commit, index) { commit.cacheIndex = index; });
133 }
134
135
136 function Measurement(rawData)
137 {
138     this._raw = rawData;
139
140     var latestTime = -1;
141     var revisions = this._raw['revisions'];
142     // FIXME: Fix this in the server side.
143     if (Array.isArray(revisions))
144         revisions = {};
145     this._raw['revisions'] = revisions;
146
147     for (var repositoryName in revisions) {
148         var commitTimeOrUndefined = revisions[repositoryName][1]; // e.g. ["162190", 1389945046000]
149         if (latestTime < commitTimeOrUndefined)
150             latestTime = commitTimeOrUndefined;
151     }
152     this._latestCommitTime = latestTime !== -1 ? new Date(latestTime) : null;
153     this._buildTime = new Date(this._raw['buildTime']);
154     this._confidenceInterval = undefined;
155     this._formattedRevisions = undefined;
156 }
157
158 Measurement.prototype.commitTimeForRepository = function (repositoryName)
159 {
160     var revisions = this._raw['revisions'];
161     var rawData = revisions[repositoryName];
162     if (!rawData)
163         return null;
164     return new Date(rawData[1]);
165 }
166
167 Measurement.prototype.formattedRevisions = function (previousMeasurement)
168 {
169     var revisions = this._raw['revisions'];
170     var previousRevisions = previousMeasurement ? previousMeasurement._raw['revisions'] : null;
171     var formattedRevisions = {};
172     for (var repositoryName in revisions) {
173         var currentRevision = revisions[repositoryName][0];
174         var previousRevision = previousRevisions ? previousRevisions[repositoryName][0] : null;
175         var formatttedRevision = this._formatRevisionRange(previousRevision, currentRevision);
176         formattedRevisions[repositoryName] = formatttedRevision;
177     }
178
179     return formattedRevisions;
180 }
181
182 Measurement.prototype._formatRevisionRange = function (previousRevision, currentRevision)
183 {
184     var revisionChanged = false;
185     if (previousRevision == currentRevision)
186         previousRevision = null;
187     else
188         revisionChanged = true;
189
190     var revisionPrefix = '';
191     var revisionDelimiter = '-';
192     var label = '';
193     if (parseInt(currentRevision) == currentRevision) { // e.g. r12345.
194         currentRevision = parseInt(currentRevision);
195         revisionPrefix = 'r';
196         if (previousRevision)
197             previousRevision = (parseInt(previousRevision) + 1);
198     } else if (currentRevision.indexOf(' ') >= 0) // e.g. 10.9 13C64.
199         revisionDelimiter = ' - ';
200     else if (currentRevision.length == 40) { // e.g. git hash
201         formattedCurrentHash = currentRevision.substring(0, 8);
202         if (previousRevision)
203             label = previousRevision.substring(0, 8) + '..' + formattedCurrentHash;
204         else
205             label = 'At ' + formattedCurrentHash;
206     }
207
208     if (!label) {
209         if (previousRevision)
210             label = revisionPrefix + previousRevision + revisionDelimiter + revisionPrefix + currentRevision;
211         else
212             label = 'At ' + revisionPrefix + currentRevision;
213     }
214
215     return {
216         label: label,
217         previousRevision: previousRevision,
218         currentRevision: currentRevision,
219         revisionChanged: revisionChanged
220     };
221 }
222
223 Measurement.prototype.id = function ()
224 {
225     return this._raw['id'];
226 }
227
228 Measurement.prototype.mean = function()
229 {
230     return this._raw['mean'];
231 }
232
233 Measurement.prototype.confidenceInterval = function()
234 {
235     if (this._confidenceInterval === undefined) {
236         var delta = Statistics.confidenceIntervalDelta(0.95, this._raw["iterationCount"], this._raw["sum"], this._raw["squareSum"]);
237         var mean = this.mean();
238         this._confidenceInterval = isNaN(delta) ? null : [mean - delta, mean + delta];
239     }
240     return this._confidenceInterval;
241 }
242
243 Measurement.prototype.latestCommitTime = function()
244 {
245     return this._latestCommitTime || this._buildTime;
246 }
247
248 Measurement.prototype.buildNumber = function ()
249 {
250     return this._raw['buildNumber'];
251 }
252
253 Measurement.prototype.builderId = function ()
254 {
255     return this._raw['builder'];
256 }
257
258 Measurement.prototype.buildTime = function()
259 {
260     return this._buildTime;
261 }
262
263 Measurement.prototype.formattedBuildTime = function ()
264 {
265     return Measurement._formatDate(this.buildTime());
266 }
267
268 Measurement._formatDate = function (date)
269 {
270     return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
271 }
272
273 Measurement.prototype.bugs = function ()
274 {
275     return this._raw['bugs'];
276 }
277
278 Measurement.prototype.hasBugs = function ()
279 {
280     var bugs = this.bugs();
281     return bugs && Object.keys(bugs).length;
282 }
283
284 Measurement.prototype.associateBug = function (trackerId, bugNumber)
285 {
286     var bugs = this._raw['bugs'];
287     trackerId = parseInt(trackerId);
288     bugNumber = bugNumber ? parseInt(bugNumber) : null;
289     return PrivilegedAPI.sendRequest('associate-bug', {
290         run: this.id(),
291         tracker: trackerId,
292         bugNumber: bugNumber,
293     }).then(function () {
294         if (bugNumber)
295             bugs[trackerId] = bugNumber;
296         else
297             delete bugs[trackerId];
298     });
299 }
300
301 function RunsData(rawData)
302 {
303     this._measurements = rawData.map(function (run) { return new Measurement(run); });
304 }
305
306 RunsData.prototype.count = function ()
307 {
308     return this._measurements.length;
309 }
310
311 RunsData.prototype.timeSeriesByCommitTime = function ()
312 {
313     return new TimeSeries(this._measurements.map(function (measurement) {
314         var confidenceInterval = measurement.confidenceInterval();
315         return {
316             measurement: measurement,
317             time: measurement.latestCommitTime(),
318             value: measurement.mean(),
319             interval: measurement.confidenceInterval(),
320         };
321     }));
322 }
323
324 RunsData.prototype.timeSeriesByBuildTime = function ()
325 {
326     return new TimeSeries(this._measurements.map(function (measurement) {
327         return {
328             measurement: measurement,
329             time: measurement.buildTime(),
330             value: measurement.mean(),
331             interval: measurement.confidenceInterval(),
332         };
333     }));
334 }
335
336 // FIXME: We need to devise a way to fetch runs in multiple chunks so that
337 // we don't have to fetch the entire time series to just show the last 3 days.
338 RunsData.fetchRuns = function (platformId, metricId)
339 {
340     var filename = platformId + '-' + metricId + '.json';
341
342     return new Ember.RSVP.Promise(function (resolve, reject) {
343         $.getJSON('../api/runs/' + filename, function (data) {
344             if (data.status != 'OK') {
345                 reject(data.status);
346                 return;
347             }
348             delete data.status;
349
350             for (var config in data)
351                 data[config] = new RunsData(data[config]);
352
353             resolve(data);
354         }).fail(function (xhr, status, error) {
355             reject(xhr.status + (error ? ', ' + error : ''));
356         })
357     });
358 }
359
360 function TimeSeries(series)
361 {
362     this._series = series.sort(function (a, b) { return a.time - b.time; });
363     var self = this;
364     var min = undefined;
365     var max = undefined;
366     this._series.forEach(function (point, index) {
367         point.series = self;
368         point.seriesIndex = index;
369         if (min === undefined || min > point.value)
370             min = point.value;
371         if (max === undefined || max < point.value)
372             max = point.value;
373     });
374     this._min = min;
375     this._max = max;
376 }
377
378 TimeSeries.prototype.minMaxForTimeRange = function (startTime, endTime)
379 {
380     var data = this._series;
381     var i = 0;
382     if (startTime !== undefined) {
383         for (i = 0; i < data.length; i++) {
384             var point = data[i];
385             if (point.time >= startTime)
386                 break;
387         }
388         if (i)
389             i--;
390     }
391     var min = Number.MAX_VALUE;
392     var max = Number.MIN_VALUE;
393     for (; i < data.length; i++) {
394         var point = data[i];
395         var currentMin = point.interval ? point.interval[0] : point.value;
396         var currentMax = point.interval ? point.interval[1] : point.value;
397
398         if (currentMin < min)
399             min = currentMin;
400         if (currentMax > max)
401             max = currentMax;
402
403         if (point.time >= endTime)
404             break;
405     }
406     return [min, max];
407 }
408
409 TimeSeries.prototype.series = function () { return this._series; }
410
411 TimeSeries.prototype.previousPoint = function (point)
412 {
413     if (!point.seriesIndex)
414         return null;
415     return this._series[point.seriesIndex - 1];
416 }