ac5d5b2c11dc197bbb0ec47c4481541f94fc2750
[WebKit.git] / Websites / perf.webkit.org / public / v3 / pages / analysis-task-page.js
1
2 class AnalysisTaskChartPane extends ChartPaneBase {
3     constructor() { super('analysis-task-chart-pane'); }
4 }
5
6 ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
7
8 class AnalysisTaskPage extends PageWithHeading {
9     constructor()
10     {
11         super('Analysis Task');
12         this._taskId = null;
13         this._task = null;
14         this._testGroups = null;
15         this._renderedTestGroups = null;
16         this._renderedCurrentTestGroup = undefined;
17         this._analysisResults = null;
18         this._measurementSet = null;
19         this._startPoint = null;
20         this._endPoint = null;
21         this._errorMessage = null;
22         this._currentTestGroup = null;
23         this._chartPane = this.content().querySelector('analysis-task-chart-pane').component();
24         this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
25         this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
26         this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
27
28         this.content().querySelector('.test-group-retry-form').onsubmit = this._retryCurrentTestGroup.bind(this);
29     }
30
31     title() { return this._task ? this._task.label() : 'Analysis Task'; }
32     routeName() { return 'analysis/task'; }
33
34     updateFromSerializedState(state)
35     {
36         var self = this;
37         if (state.remainingRoute) {
38             this._taskId = parseInt(state.remainingRoute);
39             AnalysisTask.fetchById(this._taskId).then(this._didFetchTask.bind(this), function (error) {
40                 self._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
41                 self.render();
42             });
43             TestGroup.fetchByTask(this._taskId).then(this._didFetchTestGroups.bind(this));
44             AnalysisResults.fetch(this._taskId).then(this._didFetchAnalysisResults.bind(this));
45         } else if (state.buildRequest) {
46             var buildRequestId = parseInt(state.buildRequest);
47             AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then(function () {
48                 if (self._task) {
49                     TestGroup.fetchByTask(self._task.id()).then(self._didFetchTestGroups.bind(self));
50                     AnalysisResults.fetch(self._task.id()).then(this._didFetchAnalysisResults.bind(this));
51                 }
52             }, function (error) {
53                 self._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
54                 self.render();
55             });
56         }
57     }
58
59     _didFetchTask(task)
60     {
61         this._task = task;
62         var platform = task.platform();
63         var metric = task.metric();
64         var lastModified = platform.lastModified(metric);
65
66         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
67         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
68
69         var formatter = metric.makeFormatter(4);
70         this._analysisResultsViewer.setValueFormatter(formatter);
71         this._analysisResultsViewer.setSmallerIsBetter(metric.isSmallerBetter());
72         this._testGroupResultsTable.setValueFormatter(formatter);
73
74         this._chartPane.configure(platform.id(), metric.id());
75
76         var domain = ChartsPage.createDomainForAnalysisTask(task);
77         this._chartPane.setOverviewDomain(domain[0], domain[1]);
78         this._chartPane.setMainDomain(domain[0], domain[1]);
79
80         this.render();
81     }
82
83     _didFetchMeasurement()
84     {
85         console.assert(this._task);
86         console.assert(this._measurementSet);
87         var series = this._measurementSet.fetchedTimeSeries('current', false, false);
88         var startPoint = series.findById(this._task.startMeasurementId());
89         var endPoint = series.findById(this._task.endMeasurementId());
90         if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
91             return;
92
93         this._analysisResultsViewer.setPoints(startPoint, endPoint);
94
95         this._startPoint = startPoint;
96         this._endPoint = endPoint;
97         this.render();
98     }
99
100     _didFetchTestGroups(testGroups)
101     {
102         this._testGroups = testGroups.sort(function (a, b) { return +a.createdAt() - b.createdAt(); });
103         this._currentTestGroup = testGroups.length ? testGroups[0] : null;
104
105         this._analysisResultsViewer.setTestGroups(testGroups);
106         this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
107         this._assignTestResultsIfPossible();
108         this.render();
109     }
110
111     _didFetchAnalysisResults(results)
112     {
113         this._analysisResults = results;
114         if (this._assignTestResultsIfPossible())
115             this.render();
116     }
117
118     _assignTestResultsIfPossible()
119     {
120         if (!this._task || !this._testGroups || !this._analysisResults)
121             return false;
122
123         for (var group of this._testGroups) {
124             for (var request of group.buildRequests())
125                 request.setResult(this._analysisResults.find(request.buildId(), this._task.metric()));
126         }
127
128         this._analysisResultsViewer.didUpdateResults();
129         this._testGroupResultsTable.didUpdateResults();
130
131         return true;
132     }
133
134     render()
135     {
136         super.render();
137
138         Instrumentation.startMeasuringTime('AnalysisTaskPage', 'render');
139
140         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
141
142         var v2URL = `/v2/#/analysis/task/${this._taskId}`;
143         this.content().querySelector('.error-message').innerHTML +=
144             `<p>To schedule a custom A/B testing, use <a href="${v2URL}">v2 UI</a>.</p>`;
145
146          this._chartPane.render();
147
148         if (this._task) {
149             this.renderReplace(this.content().querySelector('.analysis-task-name'), this._task.name());
150             var platform = this._task.platform();
151             var metric = this._task.metric();
152             var anchor = this.content().querySelector('.platform-metric-names a');
153             this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
154             anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
155         }
156
157         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
158         this._analysisResultsViewer.render();
159
160         var element = ComponentBase.createElement;
161         var link = ComponentBase.createLink;
162         if (this._testGroups != this._renderedTestGroups) {
163             this._renderedTestGroups = this._testGroups;
164             var self = this;
165             this.renderReplace(this.content().querySelector('.test-group-list'),
166                 this._testGroups.map(function (group) {
167                     return element('li', {class: 'test-group-list-' + group.id()}, link(group.label(), function () {
168                         self._showTestGroup(group);
169                     }));
170                 }));
171             this._renderedCurrentTestGroup = null;
172         }
173
174         if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
175             if (this._renderedCurrentTestGroup) {
176                 var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
177                 if (element)
178                     element.classList.remove('selected');
179             }
180             if (this._currentTestGroup) {
181                 var element = this.content().querySelector('.test-group-list-' + this._currentTestGroup.id());
182                 if (element)
183                     element.classList.add('selected');
184             }
185
186             this.content().querySelector('.test-group-retry-button').textContent = this._currentTestGroup ? 'Retry' : 'Confirm the change';
187
188             var repetitionCount = this._currentTestGroup ? this._currentTestGroup.repetitionCount() : 4;
189             var repetitionCountController = this.content().querySelector('.test-group-retry-repetition-count');
190             repetitionCountController.value = repetitionCount;
191
192             this._renderedCurrentTestGroup = this._currentTestGroup;
193         }
194
195         this.content().querySelector('.test-group-retry-button').disabled = !(this._currentTestGroup || this._startPoint);
196
197         this._testGroupResultsTable.render();
198
199         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
200     }
201
202     _showTestGroup(testGroup)
203     {
204         this._currentTestGroup = testGroup;        
205         this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
206         this.render();
207     }
208
209     _retryCurrentTestGroup(event)
210     {
211         event.preventDefault();
212         console.assert(this._currentTestGroup || this._startPoint);
213
214         var testGroupName;
215         var rootSetList;
216         var rootSetLabels;
217
218         if (this._currentTestGroup) {
219             var testGroup = this._currentTestGroup;
220             testGroupName = this._createRetryNameForTestGroup(testGroup.name());
221             rootSetList = testGroup.requestedRootSets();
222             rootSetLabels = rootSetList.map(function (rootSet) { return testGroup.labelForRootSet(rootSet); });
223         } else {
224             testGroupName = 'Confirming the change';
225             rootSetList = [this._startPoint.rootSet(), this._endPoint.rootSet()];
226             rootSetLabels = ['Point 0', `Point ${this._endPoint.seriesIndex - this._startPoint.seriesIndex}`];
227         }
228
229         var rootSetsByName = {};
230         for (var repository of rootSetList[0].repositories())
231             rootSetsByName[repository.name()] = [];
232
233         var setIndex = 0;
234         for (var rootSet of rootSetList) {
235             for (var repository of rootSet.repositories()) {
236                 var list = rootSetsByName[repository.name()];
237                 if (!list) {
238                     alert(`Set ${rootSetLabels[setIndex]} specifies ${repository.label()} but set ${rootSetLabels[0]} does not.`);
239                     return null;
240                 }
241                 list.push(rootSet.commitForRepository(repository).revision());
242             }
243             setIndex++;
244             for (var name in rootSetsByName) {
245                 var list = rootSetsByName[name];
246                 if (list.length < setIndex) {
247                     alert(`Set ${rootSetLabels[0]} specifies ${repository.label()} but set ${rootSetLabels[setIndex]} does not.`);
248                     return null;
249                 }
250             }
251         }
252
253         var repetitionCount = this.content().querySelector('.test-group-retry-repetition-count').value;
254
255         TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, rootSetsByName)
256             .then(this._didFetchTestGroups.bind(this), function (error) {
257             alert('Failed to create a new test group: ' + error);
258         });
259     }
260
261     _createRetryNameForTestGroup(name)
262     {
263         var nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
264         var number = 1;
265         if (nameWithNumberMatch) {
266             name = nameWithNumberMatch[1];
267             number = parseInt(nameWithNumberMatch[2]);
268         }
269
270         var newName;
271         do {
272             number++;
273             newName = `${name} (${number})`;
274         } while (this._hasDuplicateTestGroupName(newName));
275
276         return newName;
277     }
278
279     _hasDuplicateTestGroupName(name)
280     {
281         console.assert(this._testGroups);
282         for (var group of this._testGroups) {
283             if (group.name() == name)
284                 return true;
285         }
286         return false;
287     }
288
289     static htmlTemplate()
290     {
291         return `
292         <div class="analysis-tasl-page-container">
293             <div class="analysis-tasl-page">
294                 <h2 class="analysis-task-name"></h2>
295                 <h3 class="platform-metric-names"><a href=""></a></h3>
296                 <p class="error-message"></p>
297                 <div class="overview-chart"><analysis-task-chart-pane></analysis-task-chart-pane></div>
298                 <section class="analysis-results-view">
299                     <analysis-results-viewer></analysis-results-viewer>
300                 </section>
301                 <section class="test-group-view">
302                     <ul class="test-group-list"></ul>
303                     <div class="test-group-details">
304                         <test-group-results-table></test-group-results-table>
305                         <form class="test-group-retry-form">
306                             <button class="test-group-retry-button" type="submit">Retry</button>
307                             with
308                             <select class="test-group-retry-repetition-count">
309                                 <option>1</option>
310                                 <option>2</option>
311                                 <option>3</option>
312                                 <option>4</option>
313                                 <option>5</option>
314                                 <option>6</option>
315                                 <option>7</option>
316                                 <option>8</option>
317                                 <option>9</option>
318                                 <option>10</option>
319                             </select>
320                             iterations per set
321                         </form>
322                     </div>
323                 </section>
324             </div>
325         </div>
326 `;
327     }
328
329     static cssTemplate()
330     {
331         return `
332             .analysis-tasl-page-container {
333             }
334             .analysis-tasl-page {
335             }
336
337             .analysis-task-name {
338                 font-size: 1.2rem;
339                 font-weight: inherit;
340                 color: #c93;
341                 margin: 0 1rem;
342                 padding: 0;
343             }
344
345             .platform-metric-names {
346                 font-size: 1rem;
347                 font-weight: inherit;
348                 color: #c93;
349                 margin: 0 1rem;
350                 padding: 0;
351             }
352
353             .platform-metric-names a {
354                 text-decoration: none;
355                 color: inherit;
356             }
357
358             .platform-metric-names:empty {
359                 margin: 0;
360             }
361
362             .error-message:not(:empty) {
363                 margin: 1rem;
364                 padding: 0;
365             }
366
367             .analysis-results-view {
368                 margin: 1rem;
369             }
370
371             .test-configuration h3 {
372                 font-size: 1rem;
373                 font-weight: inherit;
374                 color: inherit;
375                 margin: 0 1rem;
376                 padding: 0;
377             }
378
379             .test-group-view {
380                 display: table;
381                 margin: 0 1rem;
382                 margin-bottom: 2rem;
383             }
384
385             .test-group-details {
386                 display: table-cell;
387                 margin-bottom: 1rem;
388                 padding: 0;
389                 margin: 0;
390             }
391
392             .test-group-retry-form {
393                 padding: 0;
394                 margin: 0.5rem;
395             }
396
397             .test-group-list {
398                 display: table-cell;
399                 margin: 0;
400                 padding: 0.2rem 0;
401                 list-style: none;
402                 border-right: solid 1px #ccc;
403                 white-space: nowrap;
404             }
405
406             .test-group-list:empty {
407                 margin: 0;
408                 padding: 0;
409                 border-right: none;
410             }
411
412             .test-group-list li {
413                 display: block;
414             }
415
416             .test-group-list a {
417                 display: block;
418                 color: inherit;
419                 text-decoration: none;
420                 font-size: 0.9rem;
421                 margin: 0;
422                 padding: 0.2rem;
423             }
424
425             .test-group-list li.selected a {
426                 background: rgba(204, 153, 51, 0.1);
427             }
428
429             .test-group-list li:not(.selected) a:hover {
430                 background: #eee;
431             }
432
433             .x-overview-chart {
434                 width: auto;
435                 height: 10rem;
436                 margin: 1rem;
437                 border: solid 0px red;
438             }
439 `;
440     }
441 }