Add the UI for scheduling a A/B testing with a custom root
[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         var id = this.id();
130         return PrivilegedAPI.sendRequest('associate-commit', {
131             task: id,
132             repository: repository.id(),
133             revision: revision,
134             kind: kind,
135         }).then(function (data) {
136             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
137                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
138         });
139     }
140
141     dissociateCommit(commit)
142     {
143         console.assert(commit instanceof CommitLog);
144         var id = this.id();
145         return PrivilegedAPI.sendRequest('associate-commit', {
146             task: id,
147             commit: commit.id(),
148         }).then(function (data) {
149             return AnalysisTask.cachedFetch('/api/analysis-tasks', {id: id}, true)
150                 .then(AnalysisTask._constructAnalysisTasksFromRawData.bind(AnalysisTask));
151         });
152     }
153
154     category()
155     {
156         var category = 'unconfirmed';
157
158         if (this._changeType == 'unchanged' || this._changeType == 'inconclusive'
159             || (this._changeType == 'regression' && this._fixes.length)
160             || (this._changeType == 'progression' && (this._causes.length || this._fixes.length)))
161             category = 'closed';
162         else if (this._causes.length || this._fixes.length || this._changeType == 'regression' || this._changeType == 'progression')
163             category = 'investigated';
164
165         return category;
166     }
167
168     static categories()
169     {
170         return [
171             'unconfirmed',
172             'investigated',
173             'closed'
174         ];
175     }
176
177     static fetchById(id)
178     {
179         return this._fetchSubset({id: id}).then(function (data) { return AnalysisTask.findById(id); });
180     }
181
182     static fetchByBuildRequestId(id)
183     {
184         return this._fetchSubset({buildRequest: id}).then(function (tasks) { return tasks[0]; });
185     }
186
187     static fetchByPlatformAndMetric(platformId, metricId, noCache)
188     {
189         return this._fetchSubset({platform: platformId, metric: metricId}, noCache).then(function (data) {
190             return AnalysisTask.findByPlatformAndMetric(platformId, metricId);
191         });
192     }
193
194     static fetchRelatedTasks(taskId)
195     {
196         // FIXME: We should add new sever-side API to just fetch the related tasks.
197         return this.fetchAll().then(function () {
198             var task = AnalysisTask.findById(taskId);
199             if (!task)
200                 return undefined;
201             var relatedTasks = new Set;
202             for (var bug of task.bugs()) {
203                 for (var otherTask of AnalysisTask.all()) {
204                     if (otherTask.bugs().includes(bug))
205                         relatedTasks.add(otherTask);
206                 }
207             }
208             for (var otherTask of AnalysisTask.all()) {
209                 if (task.isCustom())
210                     continue;
211                 if (task.endTime() < otherTask.startTime()
212                     || otherTask.endTime() < task.startTime()
213                     || task.metric() != otherTask.metric())
214                     continue;
215                 relatedTasks.add(otherTask);
216             }
217             return Array.from(relatedTasks);
218         });
219     }
220
221     static _fetchSubset(params, noCache)
222     {
223         if (this._fetchAllPromise)
224             return this._fetchAllPromise;
225         return this.cachedFetch('/api/analysis-tasks', params, noCache).then(this._constructAnalysisTasksFromRawData.bind(this));
226     }
227
228     static fetchAll()
229     {
230         if (!this._fetchAllPromise)
231             this._fetchAllPromise = RemoteAPI.getJSONWithStatus('/api/analysis-tasks').then(this._constructAnalysisTasksFromRawData.bind(this));
232         return this._fetchAllPromise;
233     }
234
235     static _constructAnalysisTasksFromRawData(data)
236     {
237         Instrumentation.startMeasuringTime('AnalysisTask', 'construction');
238
239         // FIXME: The backend shouldn't create a separate bug row per task for the same bug number.
240         var taskToBug = {};
241         for (var rawData of data.bugs) {
242             rawData.bugTracker = BugTracker.findById(rawData.bugTracker);
243             if (!rawData.bugTracker)
244                 continue;
245
246             var bug = Bug.ensureSingleton(rawData);
247             if (!taskToBug[rawData.task])
248                 taskToBug[rawData.task] = [];
249             taskToBug[rawData.task].push(bug);
250         }
251
252         for (var rawData of data.commits) {
253             rawData.repository = Repository.findById(rawData.repository);
254             if (!rawData.repository)
255                 continue;
256             CommitLog.ensureSingleton(rawData.id, rawData);
257         }
258
259         function resolveCommits(commits) {
260             return commits.map(function (id) { return CommitLog.findById(id); }).filter(function (commit) { return !!commit; });
261         }
262
263         var results = [];
264         for (var rawData of data.analysisTasks) {
265             rawData.platform = Platform.findById(rawData.platform);
266             rawData.metric = Metric.findById(rawData.metric);
267             rawData.bugs = taskToBug[rawData.id];
268             rawData.causes = resolveCommits(rawData.causes);
269             rawData.fixes = resolveCommits(rawData.fixes);
270             results.push(AnalysisTask.ensureSingleton(rawData.id, rawData));
271         }
272
273         Instrumentation.endMeasuringTime('AnalysisTask', 'construction');
274
275         return results;
276     }
277
278     static create(name, startRunId, endRunId)
279     {
280         return PrivilegedAPI.sendRequest('create-analysis-task', {
281             name: name,
282             startRun: startRunId,
283             endRun: endRunId,
284         });
285     }
286 }
287
288 if (typeof module != 'undefined')
289     module.exports.AnalysisTask = AnalysisTask;