Add a bisect button to automatically schedule bisecting A/B tasks.
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / models / analysis-task.js
1 'use strict';
2
3 class AnalysisTask extends LabeledObject {
4     constructor(id, object)
5     {
6         super(id, object);
7         this._author = object.author;
8         this._createdAt = object.createdAt;
9
10         console.assert(!object.platform || object.platform instanceof Platform);
11         this._platform = object.platform;
12
13         console.assert(!object.metric || object.metric instanceof Metric);
14         this._metric = object.metric;
15
16         this._startMeasurementId = object.startRun;
17         this._startTime = object.startRunTime;
18         this._endMeasurementId = object.endRun;
19         this._endTime = object.endRunTime;
20         this._category = object.category;
21         this._changeType = object.result; // Can't change due to v2 compatibility.
22         this._needed = object.needed;
23         this._bugs = object.bugs || [];
24         this._causes = object.causes || [];
25         this._fixes = object.fixes || [];
26         this._buildRequestCount = +object.buildRequestCount;
27         this._finishedBuildRequestCount = +object.finishedBuildRequestCount;
28     }
29
30     static findByPlatformAndMetric(platformId, metricId)
31     {
32         return this.all().filter((task) => {
33             const platform = task._platform;
34             const metric = task._metric;
35             return platform && metric && platform.id() == platformId && metric.id() == metricId;
36         });
37     }
38
39     updateSingleton(object)
40     {
41         super.updateSingleton(object);
42
43         console.assert(this._author == object.author);
44         console.assert(+this._createdAt == +object.createdAt);
45         console.assert(this._platform == object.platform);
46         console.assert(this._metric == object.metric);
47         console.assert(this._startMeasurementId == object.startRun);
48         console.assert(this._startTime == object.startRunTime);
49         console.assert(this._endMeasurementId == object.endRun);
50         console.assert(this._endTime == object.endRunTime);
51
52         this._category = object.category;
53         this._changeType = object.result; // Can't change due to v2 compatibility.
54         this._needed = object.needed;
55         this._bugs = object.bugs || [];
56         this._causes = object.causes || [];
57         this._fixes = object.fixes || [];
58         this._buildRequestCount = +object.buildRequestCount;
59         this._finishedBuildRequestCount = +object.finishedBuildRequestCount;
60     }
61
62     isCustom() { return !this._platform; }
63     hasResults() { return this._finishedBuildRequestCount; }
64     hasPendingRequests() { return this._finishedBuildRequestCount < this._buildRequestCount; }
65     requestLabel() { return `${this._finishedBuildRequestCount} of ${this._buildRequestCount}`; }
66
67     startMeasurementId() { return this._startMeasurementId; }
68     startTime() { return this._startTime; }
69     endMeasurementId() { return this._endMeasurementId; }
70     endTime() { return this._endTime; }
71
72     author() { return this._author || ''; }
73     createdAt() { return this._createdAt; }
74     bugs() { return this._bugs; }
75     causes() { return this._causes; }
76     fixes() { return this._fixes; }
77     platform() { return this._platform; }
78     metric() { return this._metric; }
79
80     changeType() { return this._changeType; }
81
82     updateName(newName) { return this._updateRemoteState({name: newName}); }
83     updateChangeType(changeType) { return this._updateRemoteState({result: changeType}); }
84
85     _updateRemoteState(param)
86     {
87         param.task = this.id();
88         return PrivilegedAPI.sendRequest('update-analysis-task', param).then(function (data) {
89             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: param.task}, true)
90                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
91         });
92     }
93
94     associateBug(tracker, bugNumber)
95     {
96         console.assert(tracker instanceof BugTracker);
97         console.assert(typeof(bugNumber) == 'number');
98         var id = this.id();
99         return PrivilegedAPI.sendRequest('associate-bug', {
100             task: id,
101             bugTracker: tracker.id(),
102             number: bugNumber,
103         }).then(function (data) {
104             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
105                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
106         });
107     }
108
109     dissociateBug(bug)
110     {
111         console.assert(bug instanceof Bug);
112         console.assert(this.bugs().includes(bug));
113         var id = this.id();
114         return PrivilegedAPI.sendRequest('associate-bug', {
115             task: id,
116             bugTracker: bug.bugTracker().id(),
117             number: bug.bugNumber(),
118             shouldDelete: true,
119         }).then(function (data) {
120             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
121                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
122         });
123     }
124
125     associateCommit(kind, repository, revision)
126     {
127         console.assert(kind == 'cause' || kind == 'fix');
128         console.assert(repository instanceof Repository);
129         if (revision.startsWith('r'))
130             revision = revision.substring(1);
131         const id = this.id();
132         return PrivilegedAPI.sendRequest('associate-commit', {
133             task: id,
134             repository: repository.id(),
135             revision: revision,
136             kind: kind,
137         }).then((data) => {
138             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id}, true)
139                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
140         });
141     }
142
143     dissociateCommit(commit)
144     {
145         console.assert(commit instanceof CommitLog);
146         var id = this.id();
147         return PrivilegedAPI.sendRequest('associate-commit', {
148             task: id,
149             commit: commit.id(),
150         }).then(function (data) {
151             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
152                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
153         });
154     }
155
156     category()
157     {
158         var category = 'unconfirmed';
159
160         if (this._changeType == 'unchanged' || this._changeType == 'inconclusive'
161             || (this._changeType == 'regression' && this._fixes.length)
162             || (this._changeType == 'progression' && (this._causes.length || this._fixes.length)))
163             category = 'closed';
164         else if (this._causes.length || this._fixes.length || this._changeType == 'regression' || this._changeType == 'progression')
165             category = 'investigated';
166
167         return category;
168     }
169
170     async commitSetsFromTestGroupsAndMeasurementSet()
171     {
172
173         const platform = this.platform();
174         const metric = this.metric();
175         if (!platform || !metric)
176             return [];
177
178         const lastModified = platform.lastModified(metric);
179         const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
180         const fetchingMeasurementSetPromise = measurementSet.fetchBetween(this.startTime(), this.endTime());
181
182         const allTestGroupsInTask = await TestGroup.fetchForTask(this.id());
183         const allCommitSetsInTask = new Set;
184         for (const group of allTestGroupsInTask)
185             group.requestedCommitSets().forEach((commitSet) => allCommitSetsInTask.add(commitSet));
186
187         await fetchingMeasurementSetPromise;
188
189         const series = measurementSet.fetchedTimeSeries('current', false, false);
190         const startPoint = series.findById(this.startMeasurementId());
191         const endPoint = series.findById(this.endMeasurementId());
192
193         return Array.from(series.viewBetweenPoints(startPoint, endPoint)).map((point) => point.commitSet());
194     }
195
196     static categories()
197     {
198         return [
199             'unconfirmed',
200             'investigated',
201             'closed'
202         ];
203     }
204
205     static fetchById(id)
206     {
207         return this._fetchSubset({id: id}, true).then(function (data) { return AnalysisTask.findById(id); });
208     }
209
210     static fetchByBuildRequestId(id)
211     {
212         return this._fetchSubset({buildRequest: id}).then(function (tasks) { return tasks[0]; });
213     }
214
215     static fetchByPlatformAndMetric(platformId, metricId, noCache)
216     {
217         return this._fetchSubset({platform: platformId, metric: metricId}, noCache).then(function (data) {
218             return AnalysisTask.findByPlatformAndMetric(platformId, metricId);
219         });
220     }
221
222     static fetchRelatedTasks(taskId)
223     {
224         // FIXME: We should add new sever-side API to just fetch the related tasks.
225         return this.fetchAll().then(function () {
226             var task = AnalysisTask.findById(taskId);
227             if (!task)
228                 return undefined;
229             var relatedTasks = new Set;
230             for (var bug of task.bugs()) {
231                 for (var otherTask of AnalysisTask.all()) {
232                     if (otherTask.bugs().includes(bug))
233                         relatedTasks.add(otherTask);
234                 }
235             }
236             for (var otherTask of AnalysisTask.all()) {
237                 if (task.isCustom())
238                     continue;
239                 if (task.endTime() < otherTask.startTime()
240                     || otherTask.endTime() < task.startTime()
241                     || task.metric() != otherTask.metric())
242                     continue;
243                 relatedTasks.add(otherTask);
244             }
245             return Array.from(relatedTasks);
246         });
247     }
248
249     static _fetchSubset(params, noCache)
250     {
251         if (this._fetchAllPromise)
252             return this._fetchAllPromise;
253         return this.cachedFetch('/api/analysis-tasks', params, noCache).then(this._constructAnalysisTasksFromRawData.bind(this));
254     }
255
256     static fetchAll()
257     {
258         if (!this._fetchAllPromise)
259             this._fetchAllPromise = RemoteAPI.getJSONWithStatus('/api/analysis-tasks').then(this._constructAnalysisTasksFromRawData.bind(this));
260         return this._fetchAllPromise;
261     }
262
263     static _constructAnalysisTasksFromRawData(data)
264     {
265         Instrumentation.startMeasuringTime('AnalysisTask', 'construction');
266
267         // FIXME: The backend shouldn't create a separate bug row per task for the same bug number.
268         var taskToBug = {};
269         for (var rawData of data.bugs) {
270             rawData.bugTracker = BugTracker.findById(rawData.bugTracker);
271             if (!rawData.bugTracker)
272                 continue;
273
274             var bug = Bug.ensureSingleton(rawData);
275             if (!taskToBug[rawData.task])
276                 taskToBug[rawData.task] = [];
277             taskToBug[rawData.task].push(bug);
278         }
279
280         for (var rawData of data.commits) {
281             rawData.repository = Repository.findById(rawData.repository);
282             if (!rawData.repository)
283                 continue;
284             CommitLog.ensureSingleton(rawData.id, rawData);
285         }
286
287         function resolveCommits(commits) {
288             return commits.map(function (id) { return CommitLog.findById(id); }).filter(function (commit) { return !!commit; });
289         }
290
291         var results = [];
292         for (var rawData of data.analysisTasks) {
293             rawData.platform = Platform.findById(rawData.platform);
294             rawData.metric = Metric.findById(rawData.metric);
295             rawData.bugs = taskToBug[rawData.id];
296             rawData.causes = resolveCommits(rawData.causes);
297             rawData.fixes = resolveCommits(rawData.fixes);
298             results.push(AnalysisTask.ensureSingleton(rawData.id, rawData));
299         }
300
301         Instrumentation.endMeasuringTime('AnalysisTask', 'construction');
302
303         return results;
304     }
305
306     static create(name, startRunId, endRunId)
307     {
308         return PrivilegedAPI.sendRequest('create-analysis-task', {
309             name: name,
310             startRun: startRunId,
311             endRun: endRunId,
312         });
313     }
314 }
315
316 if (typeof module != 'undefined')
317     module.exports.AnalysisTask = AnalysisTask;