Refactor compareIterations to remove duplicate code.
[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         return this.buildbot.baseURL + "json/builders/" + encodeURIComponent(this.id);
75     },
76
77     get allIterationsURL()
78     {
79         // Getting too many builds results in a timeout error, 10000 is OK.
80         return this.buildbot.baseURL + "json/builders/" + encodeURIComponent(this.id) + "/builds/_all/?max=10000";
81     },
82
83     get overviewURL()
84     {
85         return this.buildbot.baseURL + "builders/" + encodeURIComponent(this.id) + "?numbuilds=50";
86     },
87
88     get recentFailedIterationCount()
89     {
90         var firstFinishedIteration = this.mostRecentFinishedIteration;
91         var mostRecentSuccessfulIteration = this.mostRecentSuccessfulIteration;
92         return this.iterations.indexOf(mostRecentSuccessfulIteration) - this.iterations.indexOf(firstFinishedIteration);
93     },
94
95     get firstRecentUnsuccessfulIteration()
96     {
97         if (!this.iterations.length)
98             return null;
99
100         for (var i = 0; i < this.iterations.length; ++i) {
101             if (!this.iterations[i].finished || !this.iterations[i].successful)
102                 continue;
103             if (this.iterations[i - 1] && this.iterations[i - 1].finished && !this.iterations[i - 1].successful)
104                 return this.iterations[i - 1];
105             return null;
106         }
107
108         if (!this.iterations[this.iterations.length - 1].successful)
109             return this.iterations[this.iterations.length - 1];
110
111         return null;
112     },
113
114     get mostRecentFinishedIteration()
115     {
116         for (var i = 0; i < this.iterations.length; ++i) {
117             if (!this.iterations[i].finished)
118                 continue;
119             return this.iterations[i];
120         }
121
122         return null;
123     },
124
125     get mostRecentSuccessfulIteration()
126     {
127         for (var i = 0; i < this.iterations.length; ++i) {
128             if (!this.iterations[i].finished || !this.iterations[i].successful)
129                 continue;
130             return this.iterations[i];
131         }
132
133         return null;
134     },
135
136     _load: function(url, callback)
137     {
138         if (this.buildbot.needsAuthentication && this.buildbot.authenticationStatus === Buildbot.AuthenticationStatus.InvalidCredentials)
139             return;
140
141         JSON.load(
142             url,
143             function(data) {
144                 this.buildbot.isAuthenticated = true;
145                 callback(data);
146             }.bind(this),
147             function(data) {
148                 if (data.errorType !== JSON.LoadError || data.errorHTTPCode !== 401)
149                     return;
150                 if (this.buildbot.isAuthenticated) {
151                     // FIXME (128006): Safari/WebKit should coalesce authentication requests with the same origin and authentication realm.
152                     // In absence of the fix, Safari presents additional authentication dialogs regardless of whether an earlier authentication
153                     // dialog was dismissed. As a way to ameliorate the user experience where a person authenticated successfully using an
154                     // earlier authentication dialog and cancelled the authentication dialog associated with the load for this queue, we call
155                     // ourself so that we can schedule another load, which should complete successfully now that we have credentials.
156                     this._load(url, callback);
157                     return;
158                 }
159
160                 this.buildbot.isAuthenticated = false;
161                 this.dispatchEventToListeners(BuildbotQueue.Event.UnauthorizedAccess, { });
162             }.bind(this),
163             {withCredentials: this.buildbot.needsAuthentication}
164         );
165     },
166
167     loadMoreHistoricalIterations: function()
168     {
169         var indexOfFirstNewlyLoadingIteration;
170         for (var i = 0; i < this.iterations.length; ++i) {
171             if (indexOfFirstNewlyLoadingIteration !== undefined && i >= indexOfFirstNewlyLoadingIteration + BuildbotQueue.RecentIterationsToLoad)
172                 return;
173             var iteration = this.iterations[i];
174             if (!iteration.finished)
175                 continue;
176             if (iteration.isLoading) {
177                 // Caller lacks visibility into loading, so it is likely to call this function too often.
178                 // Give it a chance to analyze everything that's been already requested first, and then it can decide whether it needs more.
179                 return;
180             }
181             if (iteration.loaded && indexOfFirstNewlyLoadingIteration !== undefined) {
182                 // There was a gap between loaded iterations, which we've closed now.
183                 return;
184             }
185             if (!iteration.loaded) {
186                 if (indexOfFirstNewlyLoadingIteration === undefined)
187                     indexOfFirstNewlyLoadingIteration = i;
188                 if (!this._hasLoadedIterationForInOrderResult)
189                     iteration.addEventListener(BuildbotIteration.Event.Updated, this._checkForInOrderResult.bind(this));
190                 iteration.update();
191             }
192         }
193     },
194
195     update: function()
196     {
197         this._load(this.baseURL, function(data) {
198             if (!(data.cachedBuilds instanceof Array))
199                 return;
200
201             var currentBuilds = {};
202             if (data.currentBuilds instanceof Array)
203                 data.currentBuilds.forEach(function(id) { currentBuilds[id] = true; });
204
205             var loadingStop = Math.max(0, data.cachedBuilds.length - BuildbotQueue.RecentIterationsToLoad);
206
207             var newIterations = [];
208
209             for (var i = data.cachedBuilds.length - 1; i >= 0; --i) {
210                 var iteration = this._knownIterations[data.cachedBuilds[i]];
211                 if (!iteration) {
212                     iteration = new BuildbotIteration(this, parseInt(data.cachedBuilds[i], 10), !(data.cachedBuilds[i] in currentBuilds));
213                     newIterations.push(iteration);
214                     this.iterations.push(iteration);
215                     this._knownIterations[iteration.id] = iteration;
216                 }
217
218                 if (i >= loadingStop && (!iteration.finished || !iteration.loaded)) {
219                     if (!this._hasLoadedIterationForInOrderResult)
220                         iteration.addEventListener(BuildbotIteration.Event.Updated, this._checkForInOrderResult.bind(this));
221                     iteration.update();
222                 }
223             }
224
225             if (!newIterations.length)
226                 return;
227
228             this.sortIterations();
229
230             this.dispatchEventToListeners(BuildbotQueue.Event.IterationsAdded, {addedIterations: newIterations});
231         }.bind(this));
232     },
233
234     _checkForInOrderResult: function(event)
235     {
236         if (this._hasLoadedIterationForInOrderResult)
237             return;
238         var iterationsInOriginalOrder = this.iterations.concat().sort(function(a, b) { return b.id - a.id; });
239         for (var i = 0; i < iterationsInOriginalOrder.length - 1; ++i) {
240             var i1 = iterationsInOriginalOrder[i];
241             var i2 = iterationsInOriginalOrder[i + 1];
242             if (i1.productive && i2.loaded && this.compareIterationsByRevisions(i1, i2) < 0) {
243                 this._hasLoadedIterationForInOrderResult = true;
244                 return;
245             }
246         }
247         this.loadMoreHistoricalIterations();
248     },
249
250     loadAll: function(callback)
251     {
252         // FIXME: Don't load everything at once, do it incrementally as requested.
253         this._load(this.allIterationsURL, function(data) {
254             for (var idString in data) {
255                 console.assert(typeof idString === "string");
256                 var iteration = new BuildbotIteration(this, data[idString]);
257                 this.iterations.push(iteration);
258                 this._knownIterations[iteration.id] = iteration;
259             }
260
261             this.sortIterations();
262
263             this._hasLoadedIterationForInOrderResult = true;
264
265             callback(this);
266         }.bind(this));
267     },
268
269     compareIterations: function(a, b)
270     {
271         result = this.compareIterationsByRevisions(a, b);
272         if (result)
273             return result;
274
275         // A loaded iteration may not have revision numbers if it failed early, before svn steps finished.
276         result = b.loaded - a.loaded;
277         if (result)
278             return result;
279
280         return b.id - a.id;
281     },
282
283     compareIterationsByRevisions: function(a, b)
284     {
285         var sortedRepositories = Dashboard.sortedRepositories;
286         for (var i = 0; i < sortedRepositories.length; ++i) {
287             var repositoryName = sortedRepositories[i].name;
288             var result = b.revision[repositoryName] - a.revision[repositoryName];
289             if (result)
290                 return result;
291         }
292
293         return 0;
294     },
295
296     sortIterations: function()
297     {
298         this.iterations.sort(this.compareIterations.bind(this));
299     }
300 };