Add the support for scheduling a A/B testing with a patch.
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / models / test-group.js
1 'use strict';
2
3 class TestGroup extends LabeledObject {
4
5     constructor(id, object)
6     {
7         super(id, object);
8         this._taskId = object.task;
9         this._authorName = object.author;
10         this._createdAt = new Date(object.createdAt);
11         this._isHidden = object.hidden;
12         this._buildRequests = [];
13         this._orderBuildRequestsLazily = new LazilyEvaluatedFunction((...buildRequests) => {
14             return buildRequests.sort((a, b) => a.order() - b.order());
15         });
16         this._repositories = null;
17         this._computeRequestedCommitSetsLazily = new LazilyEvaluatedFunction(this._computeRequestedCommitSets.bind(this));
18         this._requestedCommitSets = null;
19         this._commitSetToLabel = new Map;
20         console.assert(!object.platform || object.platform instanceof Platform);
21         this._platform = object.platform;
22     }
23
24     updateSingleton(object)
25     {
26         super.updateSingleton(object);
27
28         console.assert(this._taskId == object.task);
29         console.assert(+this._createdAt == +object.createdAt);
30         console.assert(this._platform == object.platform);
31
32         this._isHidden = object.hidden;
33     }
34
35     createdAt() { return this._createdAt; }
36     isHidden() { return this._isHidden; }
37     buildRequests() { return this._buildRequests; }
38     addBuildRequest(request)
39     {
40         this._buildRequests.push(request);
41         this._requestedCommitSets = null;
42         this._commitSetToLabel.clear();
43     }
44
45     test()
46     {
47         const request = this._lastRequest();
48         return request ? request.test() : null;
49     }
50
51     platform()
52     {
53         const request = this._lastRequest();
54         return request ? request.platform() : null;
55     }
56
57     _lastRequest()
58     {
59         const requests = this._orderedBuildRequests();
60         return requests.length ? requests[requests.length - 1] : null;
61     }
62
63     _orderedBuildRequests()
64     {
65         return this._orderBuildRequestsLazily.evaluate(...this._buildRequests);
66     }
67
68     repetitionCount()
69     {
70         if (!this._buildRequests.length)
71             return 0;
72         const commitSet = this._buildRequests[0].commitSet();
73         let count = 0;
74         for (const request of this._buildRequests) {
75             if (request.isTest() && request.commitSet() == commitSet)
76                 count++;
77         }
78         return count;
79     }
80
81     requestedCommitSets()
82     {
83         return this._computeRequestedCommitSetsLazily.evaluate(...this._orderedBuildRequests());
84     }
85
86     _computeRequestedCommitSets(...orderedBuildRequests)
87     {
88         const requestedCommitSets = [];
89         const commitSetLabelMap = new Map;
90         for (const request of orderedBuildRequests) {
91             const set = request.commitSet();
92             if (!this._requestedCommitSets.includes(set))
93                 this._requestedCommitSets.push(set);
94         }
95         return requestedCommitSets;
96     }
97
98     requestsForCommitSet(commitSet)
99     {
100         this._orderedBuildRequests().filter((request) => request.commitSet() == commitSet);
101     }
102
103     labelForCommitSet(commitSet)
104     {
105         const requestedSets = this.requestedCommitSets();
106         const setIndex = requestedSets.indexOf(commitSet);
107         if (setIndex < 0)
108             return null;
109         return String.fromCharCode('A'.charCodeAt(0) + setIndex);
110     }
111
112     hasFinished()
113     {
114         return this._buildRequests.every(function (request) { return request.hasFinished(); });
115     }
116
117     hasStarted()
118     {
119         return this._buildRequests.some(function (request) { return request.hasStarted(); });
120     }
121
122     hasPending()
123     {
124         return this._buildRequests.some(function (request) { return request.isPending(); });
125     }
126
127     compareTestResults(metric, beforeValues, afterValues)
128     {
129         console.assert(metric);
130         const beforeMean = Statistics.sum(beforeValues) / beforeValues.length;
131         const afterMean = Statistics.sum(afterValues) / afterValues.length;
132
133         var result = {changeType: null, status: 'failed', label: 'Failed', fullLabel: 'Failed', isStatisticallySignificant: false};
134
135         var hasCompleted = this.hasFinished();
136         if (!hasCompleted) {
137             if (this.hasStarted()) {
138                 result.status = 'running';
139                 result.label = 'Running';
140                 result.fullLabel = 'Running';
141             } else {
142                 console.assert(result.changeType === null);
143                 result.status = 'pending';
144                 result.label = 'Pending';
145                 result.fullLabel = 'Pending';
146             }
147         }
148
149         if (beforeValues.length && afterValues.length) {
150             var diff = afterMean - beforeMean;
151             var smallerIsBetter = metric.isSmallerBetter();
152             var changeType = diff < 0 == smallerIsBetter ? 'better' : 'worse';
153             var changeLabel = Math.abs(diff / beforeMean * 100).toFixed(2) + '% ' + changeType;
154             var isSignificant = Statistics.testWelchsT(beforeValues, afterValues);
155             var significanceLabel = isSignificant ? 'significant' : 'insignificant';
156
157             result.changeType = changeType;
158             result.label = changeLabel;
159             if (hasCompleted)
160                 result.status = isSignificant ? result.changeType : 'unchanged';
161             result.fullLabel = `${result.label} (statistically ${significanceLabel})`;
162             result.isStatisticallySignificant = isSignificant;
163         }
164
165         return result;
166     }
167
168     updateName(newName)
169     {
170         var self = this;
171         var id = this.id();
172         return PrivilegedAPI.sendRequest('update-test-group', {
173             group: id,
174             name: newName,
175         }).then(function (data) {
176             return TestGroup.cachedFetch(`/api/test-groups/${id}`, {}, true)
177                 .then(TestGroup._createModelsFromFetchedTestGroups.bind(TestGroup));
178         });
179     }
180
181     updateHiddenFlag(hidden)
182     {
183         var self = this;
184         var id = this.id();
185         return PrivilegedAPI.sendRequest('update-test-group', {
186             group: id,
187             hidden: !!hidden,
188         }).then(function (data) {
189             return TestGroup.cachedFetch(`/api/test-groups/${id}`, {}, true)
190                 .then(TestGroup._createModelsFromFetchedTestGroups.bind(TestGroup));
191         });
192     }
193
194     static createWithTask(taskName, platform, test, groupName, repetitionCount, commitSets)
195     {
196         console.assert(commitSets.length == 2);
197         const revisionSets = this._revisionSetsFromCommitSets(commitSets);
198         const params = {taskName, name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets};
199         return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => {
200             return AnalysisTask.fetchById(data['taskId']);
201         }).then((task) => {
202             return this._fetchTestGroupsForTask(task.id()).then(() => task);
203         });
204     }
205
206     static createWithCustomConfiguration(task, platform, test, groupName, repetitionCount, commitSets)
207     {
208         console.assert(commitSets.length == 2);
209         const revisionSets = this._revisionSetsFromCommitSets(commitSets);
210         const params = {task: task.id(), name: groupName, platform: platform.id(), test: test.id(), repetitionCount, revisionSets};
211         return PrivilegedAPI.sendRequest('create-test-group', params).then((data) => {
212             return this._fetchTestGroupsForTask(task.id());
213         });
214     }
215
216     static createAndRefetchTestGroups(task, name, repetitionCount, commitSets)
217     {
218         console.assert(commitSets.length == 2);
219         const revisionSets = this._revisionSetsFromCommitSets(commitSets);
220         return PrivilegedAPI.sendRequest('create-test-group', {
221             task: task.id(),
222             name: name,
223             repetitionCount: repetitionCount,
224             revisionSets: revisionSets,
225         }).then((data) => this._fetchTestGroupsForTask(task.id()));
226     }
227
228     static _revisionSetsFromCommitSets(commitSets)
229     {
230         return commitSets.map((commitSet) => {
231             console.assert(commitSet instanceof CustomCommitSet || commitSet instanceof CommitSet);
232             const revisionSet = {};
233             for (let repository of commitSet.repositories()) {
234                 const patchFile = commitSet.patchForRepository(repository);
235                 revisionSet[repository.id()] = {
236                     revision: commitSet.revisionForRepository(repository),
237                     patch: patchFile ? patchFile.id() : null,
238                 };
239             }
240             const customRoots = commitSet.customRoots();
241             if (customRoots && customRoots.length)
242                 revisionSet['customRoots'] = customRoots.map((uploadedFile) => uploadedFile.id());
243             return revisionSet;
244         });
245     }
246
247     static _fetchTestGroupsForTask(taskId)
248     {
249         return this.cachedFetch('/api/test-groups', {task: taskId}, true).then((data) => this._createModelsFromFetchedTestGroups(data));
250     }
251
252     static fetchByTask(taskId)
253     {
254         return this.cachedFetch('/api/test-groups', {task: taskId}).then(this._createModelsFromFetchedTestGroups.bind(this));
255     }
256
257     static _createModelsFromFetchedTestGroups(data)
258     {
259         var testGroups = data['testGroups'].map(function (row) {
260             row.platform = Platform.findById(row.platform);
261             return TestGroup.ensureSingleton(row.id, row);
262         });
263
264         BuildRequest.constructBuildRequestsFromData(data);
265
266         return testGroups;
267     }
268 }
269
270 if (typeof module != 'undefined')
271     module.exports.TestGroup = TestGroup;