Update Bot Watcher's Dashboard for Buildbot 0.9
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / dashboard / Scripts / BuildbotQueue.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 BuildbotQueue = function(buildbot, id, info)
27 {
28     BaseObject.call(this);
29
30     console.assert(buildbot);
31     console.assert(id);
32
33     this.buildbot = buildbot;
34     this.id = id;
35
36     // FIXME: Some of these are presentation only, and should be handled above BuildbotQueue level.
37     this.branches = info.branches;
38     this.platform = info.platform.name;
39     this.debug = info.debug;
40     this.builder = info.builder;
41     this.tester = info.tester;
42     this.performance = info.performance;
43     this.staticAnalyzer = info.staticAnalyzer;
44     this.leaks = info.leaks;
45     this.architecture = info.architecture;
46     this.testCategory = info.testCategory;
47     this.heading = info.heading;
48     this.crashesOnly = info.crashesOnly;
49
50     this.iterations = [];
51     this._knownIterations = {};
52
53     // Some queues process changes out of order, but we need to display results for the latest commit,
54     // not the latest build. BuildbotQueue ensures that at least one productive iteration
55     // that was run in order gets loaded (if the queue had any productive iterations, of course).
56     this._hasLoadedIterationForInOrderResult = false;
57 };
58
59 BaseObject.addConstructorFunctions(BuildbotQueue);
60
61 BuildbotQueue.RecentIterationsToLoad = 10;
62
63 BuildbotQueue.Event = {
64     IterationsAdded: "iterations-added",
65     UnauthorizedAccess: "unauthorized-access"
66 };
67
68 BuildbotQueue.prototype = {
69     constructor: BuildbotQueue,
70     __proto__: BaseObject.prototype,
71
72     get baseURL()
73     {
74         if (this.buildbot.VERSION_LESS_THAN_09)
75             return this.buildbot.baseURL + "json/builders/" + encodeURIComponent(this.id);
76
77         return this.buildbot.baseURL + "api/v2/builders/" + encodeURIComponent(this.id);
78     },
79
80     get allIterationsURL()
81     {
82         if (this.buildbot.VERSION_LESS_THAN_09)
83             return this.baseURL + "/builds/_all/?max=10000";
84
85         return this.baseURL + "/builds?order=-number";
86     },
87
88     get buildsURL()
89     {
90         // We need to limit the number of builds for which we fetch the info, as it would
91         // impact performance. For each build, we will be subsequently making REST API calls
92         // to fetch detailed build info.
93         return this.baseURL + "/builds?order=-number&limit=20";
94     },
95
96     get overviewURL()
97     {
98         if (this.buildbot.VERSION_LESS_THAN_09)
99             return this.buildbot.baseURL + "builders/" + encodeURIComponent(this.id) + "?numbuilds=50";
100
101         return this.buildbot.baseURL + "#/builders/" + encodeURIComponent(this.id) + "?numbuilds=50";
102     },
103
104     get recentFailedIterationCount()
105     {
106         var firstFinishedIteration = this.mostRecentFinishedIteration;
107         var mostRecentSuccessfulIteration = this.mostRecentSuccessfulIteration;
108         return this.iterations.indexOf(mostRecentSuccessfulIteration) - this.iterations.indexOf(firstFinishedIteration);
109     },
110
111     get firstRecentUnsuccessfulIteration()
112     {
113         if (!this.iterations.length)
114             return null;
115
116         for (var i = 0; i < this.iterations.length; ++i) {
117             if (!this.iterations[i].finished || !this.iterations[i].successful)
118                 continue;
119             if (this.iterations[i - 1] && this.iterations[i - 1].finished && !this.iterations[i - 1].successful)
120                 return this.iterations[i - 1];
121             return null;
122         }
123
124         if (!this.iterations[this.iterations.length - 1].successful)
125             return this.iterations[this.iterations.length - 1];
126
127         return null;
128     },
129
130     get mostRecentFinishedIteration()
131     {
132         for (var i = 0; i < this.iterations.length; ++i) {
133             if (!this.iterations[i].finished)
134                 continue;
135             return this.iterations[i];
136         }
137
138         return null;
139     },
140
141     get mostRecentSuccessfulIteration()
142     {
143         for (var i = 0; i < this.iterations.length; ++i) {
144             if (!this.iterations[i].finished || !this.iterations[i].successful)
145                 continue;
146             return this.iterations[i];
147         }
148
149         return null;
150     },
151
152     _load: function(url, callback)
153     {
154         if (this.buildbot.needsAuthentication && this.buildbot.authenticationStatus === Buildbot.AuthenticationStatus.InvalidCredentials)
155             return;
156
157         JSON.load(
158             url,
159             function(data) {
160                 this.buildbot.isAuthenticated = true;
161                 callback(data);
162             }.bind(this),
163             function(data) {
164                 if (data.errorType !== JSON.LoadError || data.errorHTTPCode !== 401)
165                     return;
166                 if (this.buildbot.isAuthenticated) {
167                     // FIXME (128006): Safari/WebKit should coalesce authentication requests with the same origin and authentication realm.
168                     // In absence of the fix, Safari presents additional authentication dialogs regardless of whether an earlier authentication
169                     // dialog was dismissed. As a way to ameliorate the user experience where a person authenticated successfully using an
170                     // earlier authentication dialog and cancelled the authentication dialog associated with the load for this queue, we call
171                     // ourself so that we can schedule another load, which should complete successfully now that we have credentials.
172                     this._load(url, callback);
173                     return;
174                 }
175
176                 this.buildbot.isAuthenticated = false;
177                 this.dispatchEventToListeners(BuildbotQueue.Event.UnauthorizedAccess, { });
178             }.bind(this),
179             {withCredentials: this.buildbot.needsAuthentication}
180         );
181     },
182
183     loadMoreHistoricalIterations: function()
184     {
185         var indexOfFirstNewlyLoadingIteration;
186         for (var i = 0; i < this.iterations.length; ++i) {
187             if (indexOfFirstNewlyLoadingIteration !== undefined && i >= indexOfFirstNewlyLoadingIteration + BuildbotQueue.RecentIterationsToLoad)
188                 return;
189             var iteration = this.iterations[i];
190             if (!iteration.finished)
191                 continue;
192             if (iteration.isLoading) {
193                 // Caller lacks visibility into loading, so it is likely to call this function too often.
194                 // Give it a chance to analyze everything that's been already requested first, and then it can decide whether it needs more.
195                 return;
196             }
197             if (iteration.loaded && indexOfFirstNewlyLoadingIteration !== undefined) {
198                 // There was a gap between loaded iterations, which we've closed now.
199                 return;
200             }
201             if (!iteration.loaded) {
202                 if (indexOfFirstNewlyLoadingIteration === undefined)
203                     indexOfFirstNewlyLoadingIteration = i;
204                 if (!this._hasLoadedIterationForInOrderResult)
205                     iteration.addEventListener(BuildbotIteration.Event.Updated, this._checkForInOrderResult.bind(this));
206                 iteration.update();
207             }
208         }
209     },
210
211     get buildsInfoURL()
212     {
213         if (this.buildbot.VERSION_LESS_THAN_09)
214             return this.baseURL;
215
216         return this.buildsURL;
217     },
218
219     getBuilds: function(data)
220     {
221         if (this.buildbot.VERSION_LESS_THAN_09)
222             return data.cachedBuilds.reverse();
223
224         return data.builds;
225     },
226
227     isBuildComplete: function(data, index)
228     {
229         if (this.buildbot.VERSION_LESS_THAN_09) {
230             var currentBuilds = {};
231             if (data.currentBuilds instanceof Array)
232                 data.currentBuilds.forEach(function(id) { currentBuilds[id] = true; });
233
234             return (!(data.cachedBuilds[index] in currentBuilds));
235         }
236
237         return data.builds[index].complete;
238     },
239
240     getIterationID: function(data, index)
241     {
242         if (this.buildbot.VERSION_LESS_THAN_09)
243             return data.cachedBuilds[index];
244
245         return data.builds[index].number;
246     },
247
248     update: function()
249     {
250         this._load(this.buildsInfoURL, function(data) {
251             var builds = this.getBuilds(data);
252             if (!(builds instanceof Array))
253                 return;
254
255             var newIterations = [];
256
257             for (var i = 0; i < builds.length; ++i) {
258                 var iterationID = this.getIterationID(data, i);
259                 var iteration = this._knownIterations[iterationID];
260                 if (!iteration) {
261                     iteration = new BuildbotIteration(this, iterationID, this.isBuildComplete(data, i));
262                     newIterations.push(iteration);
263                     this.iterations.push(iteration);
264                     this._knownIterations[iteration.id] = iteration;
265                 }
266
267                 if (i < BuildbotQueue.RecentIterationsToLoad && (!iteration.finished || !iteration.loaded)) {
268                     if (!this._hasLoadedIterationForInOrderResult)
269                         iteration.addEventListener(BuildbotIteration.Event.Updated, this._checkForInOrderResult.bind(this));
270                     iteration.update();
271                 }
272             }
273
274             if (!newIterations.length)
275                 return;
276
277             this.sortIterations();
278
279             this.dispatchEventToListeners(BuildbotQueue.Event.IterationsAdded, {addedIterations: newIterations});
280         }.bind(this));
281     },
282
283     _checkForInOrderResult: function(event)
284     {
285         if (this._hasLoadedIterationForInOrderResult)
286             return;
287         var iterationsInOriginalOrder = this.iterations.concat().sort(function(a, b) { return b.id - a.id; });
288         for (var i = 0; i < iterationsInOriginalOrder.length - 1; ++i) {
289             var i1 = iterationsInOriginalOrder[i];
290             var i2 = iterationsInOriginalOrder[i + 1];
291             if (i1.productive && i2.loaded && this.compareIterationsByRevisions(i1, i2) < 0) {
292                 this._hasLoadedIterationForInOrderResult = true;
293                 return;
294             }
295         }
296         this.loadMoreHistoricalIterations();
297     },
298
299     loadAll: function(callback)
300     {
301         // FIXME: Don't load everything at once, do it incrementally as requested.
302         this._load(this.allIterationsURL, function(data) {
303             for (var idString in data) {
304                 console.assert(typeof idString === "string");
305                 var iteration = new BuildbotIteration(this, data[idString]);
306                 this.iterations.push(iteration);
307                 this._knownIterations[iteration.id] = iteration;
308             }
309
310             this.sortIterations();
311
312             this._hasLoadedIterationForInOrderResult = true;
313
314             callback(this);
315         }.bind(this));
316     },
317
318     compareIterations: function(a, b)
319     {
320         result = this.compareIterationsByRevisions(a, b);
321         if (result)
322             return result;
323
324         // A loaded iteration may not have revision numbers if it failed early, before svn steps finished.
325         result = b.loaded - a.loaded;
326         if (result)
327             return result;
328
329         return b.id - a.id;
330     },
331
332     compareIterationsByRevisions: function(a, b)
333     {
334         var sortedRepositories = Dashboard.sortedRepositories;
335         for (var i = 0; i < sortedRepositories.length; ++i) {
336             var repositoryName = sortedRepositories[i].name;
337             var trac = sortedRepositories[i].trac;
338             console.assert(trac);
339             var indexA = trac.indexOfRevision(a.revision[repositoryName]);
340             var indexB = trac.indexOfRevision(b.revision[repositoryName]);
341             if (indexA !== -1 && indexB !== -1) {
342                 var result = indexB - indexA;
343                 if (result)
344                     return result;
345             }
346         }
347
348         return 0;
349     },
350
351     // Re-insert the iteration if its sort order changed (which happens once details about it get loaded).
352     updateIterationPosition: function(iteration)
353     {
354         var oldIndex;
355         var inserted = false;
356         for (var i = 0; i < this.iterations.length; ++i) {
357             if (!inserted && this.compareIterations(this.iterations[i], iteration) > 0) {
358                 this.iterations.splice(i, 0, iteration);
359                 if (oldIndex !== undefined)
360                     break;
361                 inserted = true;
362                 continue;
363             }
364             if (this.iterations[i] === iteration) {
365                 oldIndex = i;
366                 if (inserted)
367                     break;
368             }
369         }
370         this.iterations.splice(oldIndex, 1);
371     },
372
373     sortIterations: function()
374     {
375         this.iterations.sort(this.compareIterations.bind(this));
376     }
377 };