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