8280daa4b0ccd38fb282a2472ef698ebb003c5d7
[WebKit.git] / Websites / perf.webkit.org / public / v3 / pages / analysis-task-page.js
1
2 class AnalysisTaskChartPane extends ChartPaneBase {
3     constructor()
4     {
5         super('analysis-task-chart-pane');
6         this._page = null;
7         this._showForm = false;
8     }
9
10     setPage(page) { this._page = page; }
11     setShowForm(show)
12     {
13         this._showForm = show;
14         this.enqueueToRender();
15     }
16     router() { return this._page.router(); }
17
18     _mainSelectionDidChange(selection, didEndDrag)
19     {
20         super._mainSelectionDidChange(selection);
21         if (didEndDrag)
22             this.enqueueToRender();
23     }
24
25     didConstructShadowTree()
26     {
27         this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap, notifyOnCompletion) => {
28             this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap, notifyOnCompletion);
29         });
30     }
31
32     render()
33     {
34         super.render();
35         const points = this._mainChart ? this._mainChart.selectedPoints('current') : null;
36
37         this.content('form').style.display = this._showForm ? null : 'none';
38         if (this._showForm) {
39             const form = this.part('form');
40             form.setCommitSetMap(points && points.length() >= 2 ? {'A': points.firstPoint().commitSet(), 'B': points.lastPoint().commitSet()} : null);
41             form.enqueueToRender();
42         }
43     }
44
45     static paneFooterTemplate() { return '<customizable-test-group-form id="form"></customizable-test-group-form>'; }
46
47     static cssTemplate()
48     {
49         return super.cssTemplate() + `
50             #form {
51                 margin: 0.5rem;
52             }
53         `;
54     }
55 }
56
57 ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
58
59 class AnalysisTaskResultsPane extends ComponentBase {
60     constructor()
61     {
62         super('analysis-task-results-pane');
63         this._showForm = false;
64         this._repositoryList = [];
65         this._renderRepositoryListLazily = new LazilyEvaluatedFunction(this._renderRepositoryList.bind(this));
66         this._updateCommitViewerLazily = new LazilyEvaluatedFunction(this._updateCommitViewer.bind(this));
67     }
68
69     setPoints(startPoint, endPoint, metric)
70     {
71         const resultsViewer = this.part('results-viewer');
72         this._repositoryList = startPoint ? Repository.sortByNamePreferringOnesWithURL(startPoint.commitSet().repositories()) : [];
73         resultsViewer.setPoints(startPoint, endPoint, metric);
74         resultsViewer.enqueueToRender();
75     }
76
77     setTestGroups(testGroups, currentGroup)
78     {
79         this.part('results-viewer').setTestGroups(testGroups, currentGroup);
80         this.enqueueToRender();
81     }
82
83     setAnalysisResultsView(analysisResultsView)
84     {
85         this.part('results-viewer').setAnalysisResultsView(analysisResultsView);
86         this.enqueueToRender();
87     }
88
89     setShowForm(show)
90     {
91         this._showForm = show;
92         this.enqueueToRender();
93     }
94
95     didConstructShadowTree()
96     {
97         const resultsViewer = this.part('results-viewer');
98         resultsViewer.listenToAction('testGroupClick', (testGroup) => {
99             this.enqueueToRender();
100             this.dispatchAction('showTestGroup', testGroup)
101         });
102         resultsViewer.setRangeSelectorLabels(['A', 'B']);
103         resultsViewer.listenToAction('rangeSelectorClick', () => this.enqueueToRender());
104
105         const repositoryPicker = this.content('commit-viewer-repository');
106         repositoryPicker.addEventListener('change', () => this.enqueueToRender());
107         this.part('commit-viewer').setShowRepositoryName(false);
108
109         this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap, notifyOnCompletion) => {
110             this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap, notifyOnCompletion);
111         });
112     }
113
114     render()
115     {
116         const resultsViewer = this.part('results-viewer');
117
118         const repositoryPicker = this._renderRepositoryListLazily.evaluate(this._repositoryList);
119         const repository = Repository.findById(repositoryPicker.value);
120         const range = resultsViewer.selectedRange();
121         this._updateCommitViewerLazily.evaluate(repository, range['A'], range['B']);
122
123         this.content('form').style.display = this._showForm ? null : 'none';
124         if (!this._showForm)
125             return;
126
127         const selectedRange = this.part('results-viewer').selectedRange();
128         const firstCommitSet = selectedRange['A'];
129         const secondCommitSet = selectedRange['B'];
130         const form = this.part('form');
131         form.setCommitSetMap(firstCommitSet && secondCommitSet ? {'A': firstCommitSet, 'B': secondCommitSet} : null);
132         form.enqueueToRender();
133     }
134
135     _renderRepositoryList(repositoryList)
136     {
137         const element = ComponentBase.createElement;
138         const selectElement = this.content('commit-viewer-repository');
139         this.renderReplace(selectElement,
140             repositoryList.map((repository) => {
141                 return element('option', {value: repository.id()}, repository.label());
142             }));
143         return selectElement;
144     }
145
146     _updateCommitViewer(repository, preceedingCommitSet, lastCommitSet)
147     {
148         if (repository && preceedingCommitSet && lastCommitSet && !preceedingCommitSet.equals(lastCommitSet)) {
149             const precedingRevision = preceedingCommitSet.revisionForRepository(repository);
150             const lastRevision = lastCommitSet.revisionForRepository(repository);
151             if (precedingRevision && lastRevision && precedingRevision != lastRevision) {
152                 this.part('commit-viewer').view(repository, precedingRevision, lastRevision);
153                 return;
154             }
155         }
156         this.part('commit-viewer').view(null, null, null);
157     }
158
159     static htmlTemplate()
160     {
161         return `
162             <div id="results-container">
163                 <div id="results-viewer-container">
164                     <analysis-results-viewer id="results-viewer"></analysis-results-viewer>
165                 </div>
166                 <div id="commit-pane">
167                     <select id="commit-viewer-repository"></select>
168                     <commit-log-viewer id="commit-viewer"></commit-log-viewer>
169                 </div>
170             </div>
171             <customizable-test-group-form id="form"></customizable-test-group-form>
172         `;
173     }
174
175     static cssTemplate()
176     {
177         return `
178             #results-container {
179                 position: relative;
180                 text-align: center;
181                 padding-right: 20rem;
182             }
183             #results-viewer-container {
184                 overflow-x: scroll;
185                 overflow-y: hidden;
186             }
187             #commit-pane {
188                 position: absolute;
189                 width: 20rem;
190                 height: 100%;
191                 top: 0;
192                 right: 0;
193             }
194             #form {
195                 margin: 0.5rem;
196             }
197         `;
198     }
199 }
200
201 ComponentBase.defineElement('analysis-task-results-pane', AnalysisTaskResultsPane);
202
203 class AnalysisTaskConfiguratorPane extends ComponentBase {
204     constructor()
205     {
206         super('analysis-task-configurator-pane');
207         this._currentGroup = null;
208     }
209
210     didConstructShadowTree()
211     {
212         const form = this.part('form');
213         form.setHasTask(true);
214         form.listenToAction('startTesting', (...args) => {
215             this.dispatchAction('createCustomTestGroup', ...args);
216         });
217     }
218
219     setTestGroups(testGroups, currentGroup)
220     {
221         this._currentGroup = currentGroup;
222         const form = this.part('form');
223         if (!form.hasCommitSets() && currentGroup)
224             form.setConfigurations(currentGroup.test(), currentGroup.platform(), currentGroup.repetitionCount(), currentGroup.requestedCommitSets());
225         this.enqueueToRender();
226     }
227
228     render()
229     {
230         super.render();
231     }
232
233     static htmlTemplate()
234     {
235         return `<custom-configuration-test-group-form id="form"></custom-configuration-test-group-form>`;
236     }
237
238     static cssTemplate()
239     {
240         return `
241             #form {
242                 margin: 1rem;
243             }
244         `;
245     }
246 };
247
248 ComponentBase.defineElement('analysis-task-configurator-pane', AnalysisTaskConfiguratorPane);
249
250 class AnalysisTaskTestGroupPane extends ComponentBase {
251
252     constructor()
253     {
254         super('analysis-task-test-group-pane');
255         this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this._renderTestGroups.bind(this));
256         this._renderTestGroupVisibilityLazily = new LazilyEvaluatedFunction(this._renderTestGroupVisibility.bind(this));
257         this._renderTestGroupNamesLazily = new LazilyEvaluatedFunction(this._renderTestGroupNames.bind(this));
258         this._renderCurrentTestGroupLazily = new LazilyEvaluatedFunction(this._renderCurrentTestGroup.bind(this));
259         this._testGroupMap = new Map;
260         this._testGroups = [];
261         this._bisectingCommitSetByTestGroup = null;
262         this._currentTestGroup = null;
263         this._showHiddenGroups = false;
264         this._allTestGroupIdSetForCurrentTask = null;
265     }
266
267     didConstructShadowTree()
268     {
269         this.content('hide-button').onclick = () => this.dispatchAction('toggleTestGroupVisibility', this._currentTestGroup);
270         this.part('retry-form').listenToAction('startTesting', (repetitionCount, notifyOnCompletion) => {
271             this.dispatchAction('retryTestGroup', this._currentTestGroup, repetitionCount, notifyOnCompletion);
272         });
273         this.part('bisect-form').listenToAction('startTesting', (repetitionCount, notifyOnCompletion) => {
274             const bisectingCommitSet = this._bisectingCommitSetByTestGroup.get(this._currentTestGroup);
275             const [oneCommitSet, anotherCommitSet] = this._currentTestGroup.requestedCommitSets();
276             const commitSets = [oneCommitSet, bisectingCommitSet, anotherCommitSet];
277             this.dispatchAction('bisectTestGroup', this._currentTestGroup, commitSets, repetitionCount, notifyOnCompletion);
278         });
279     }
280
281     setTestGroups(testGroups, currentTestGroup, showHiddenGroups)
282     {
283         this._testGroups = testGroups;
284         this._currentTestGroup = currentTestGroup;
285         this._showHiddenGroups = showHiddenGroups;
286         this.part('revision-table').setTestGroup(currentTestGroup);
287         this.part('results-viewer').setTestGroup(currentTestGroup);
288
289         const analysisTask = currentTestGroup.task();
290         const allTestGroupIdsForCurrentTask = TestGroup.findAllByTask(analysisTask.id()).map((testGroup) => testGroup.id());
291         const testGroupChanged = !this._allTestGroupIdSetForCurrentTask
292             || this._allTestGroupIdSetForCurrentTask.size !== allTestGroupIdsForCurrentTask.length
293             || !allTestGroupIdsForCurrentTask.every((testGroupId) => this._allTestGroupIdSetForCurrentTask.has(testGroupId));
294
295         const computedForCurrentTestGroup = this._bisectingCommitSetByTestGroup && this._bisectingCommitSetByTestGroup.has(currentTestGroup);
296
297         if (!testGroupChanged && computedForCurrentTestGroup) {
298             this.enqueueToRender();
299             return;
300         }
301
302         if (testGroupChanged) {
303             this._bisectingCommitSetByTestGroup = new Map;
304             this._allTestGroupIdSetForCurrentTask = new Set(allTestGroupIdsForCurrentTask);
305         }
306
307         analysisTask.commitSetsFromTestGroupsAndMeasurementSet().then(async (availableCommitSets) => {
308             const commitSetClosestToMiddle = await CommitSetRangeBisector.commitSetClosestToMiddleOfAllCommits(currentTestGroup.requestedCommitSets(), availableCommitSets);
309             this._bisectingCommitSetByTestGroup.set(currentTestGroup, commitSetClosestToMiddle);
310             this.enqueueToRender();
311         });
312     }
313
314     setAnalysisResults(analysisResults, metric)
315     {
316         this.part('revision-table').setAnalysisResults(analysisResults);
317         this.part('results-viewer').setAnalysisResults(analysisResults, metric);
318         this.enqueueToRender();
319     }
320
321     render()
322     {
323         this._renderTestGroupsLazily.evaluate(this._showHiddenGroups, ...this._testGroups);
324         this._renderTestGroupVisibilityLazily.evaluate(...this._testGroups.map((group) => group.isHidden() ? 'hidden' : 'visible'));
325         this._renderTestGroupNamesLazily.evaluate(...this._testGroups.map((group) => group.label()));
326         this._renderCurrentTestGroup(this._currentTestGroup);
327     }
328
329     _renderTestGroups(showHiddenGroups, ...testGroups)
330     {
331         const element = ComponentBase.createElement;
332         const link = ComponentBase.createLink;
333
334         this._testGroupMap = new Map;
335         const testGroupItems = testGroups.map((group) => {
336             const text = new EditableText(group.label());
337             text.listenToAction('update', () => this.dispatchAction('renameTestGroup', group, text.editedText()));
338
339             const listItem = element('li', link(text, group.label(), () => this.dispatchAction('showTestGroup', group)));
340
341             this._testGroupMap.set(group, {text, listItem});
342             return listItem;
343         });
344
345         this.renderReplace(this.content('test-group-list'), [testGroupItems,
346             showHiddenGroups ? [] : element('li', {class: 'test-group-list-show-all'}, link('Show hidden tests', () => {
347                 this.dispatchAction('showHiddenTestGroups');
348             }))]);
349     }
350
351     _renderTestGroupVisibility(...groupVisibilities)
352     {
353         for (let i = 0; i < groupVisibilities.length; i++)
354             this._testGroupMap.get(this._testGroups[i]).listItem.className = groupVisibilities[i];
355     }
356
357     _renderTestGroupNames(...groupNames)
358     {
359         for (let i = 0; i < groupNames.length; i++)
360             this._testGroupMap.get(this._testGroups[i]).text.setText(groupNames[i]);
361     }
362
363     _renderCurrentTestGroup(currentGroup)
364     {
365         const selected = this.content('test-group-list').querySelector('.selected');
366         if (selected)
367             selected.classList.remove('selected');
368         if (currentGroup)
369             this._testGroupMap.get(currentGroup).listItem.classList.add('selected');
370
371         if (currentGroup) {
372             this.part('retry-form').setRepetitionCount(currentGroup.initialRepetitionCount());
373             this.part('bisect-form').setRepetitionCount(currentGroup.initialRepetitionCount());
374             const summary = `${currentGroup.initialRepetitionCount()} requested, ${currentGroup.repetitionCount() - currentGroup.initialRepetitionCount()} added due to failures.`;
375             this.content('status-summary').innerHTML = summary;
376         }
377         this.content('retry-form').style.display = currentGroup ? null : 'none';
378         this.content('bisect-form').style.display = currentGroup && this._bisectingCommitSetByTestGroup.get(currentGroup) ? null : 'none';
379         this.content('status-summary').style.display = currentGroup && currentGroup.repetitionCount() > currentGroup.initialRepetitionCount() ? null : 'none';
380
381         const hideButton = this.content('hide-button');
382         hideButton.textContent = currentGroup && currentGroup.isHidden() ? 'Unhide' : 'Hide';
383         hideButton.style.display = currentGroup ? null : 'none';
384
385         this.content('pending-request-cancel-warning').style.display = currentGroup && currentGroup.hasPending() ? null : 'none';
386     }
387
388     static htmlTemplate()
389     {
390         return `
391             <ul id="test-group-list"></ul>
392             <div id="test-group-details">
393                 <test-group-results-viewer id="results-viewer"></test-group-results-viewer>
394                 <test-group-revision-table id="revision-table"></test-group-revision-table>
395                 <div id="status-summary" class="summary"></div>
396                 <test-group-form id="retry-form">Retry</test-group-form>
397                 <test-group-form id="bisect-form">Bisect</test-group-form>
398                 <button id="hide-button">Hide</button>
399                 <span id="pending-request-cancel-warning">(cancels pending requests)</span>
400             </div>`;
401     }
402
403     static cssTemplate()
404     {
405         return `
406             :host {
407                 display: flex !important;
408                 font-size: 0.9rem;
409             }
410
411             #test-group-list {
412                 flex: none;
413                 margin: 0;
414                 padding: 0.2rem 0;
415                 list-style: none;
416                 border-right: solid 1px #ccc;
417                 white-space: nowrap;
418                 min-width: 8rem;
419             }
420
421             li {
422                 display: block;
423                 font-size: 0.9rem;
424             }
425
426             li > a {
427                 display: block;
428                 color: inherit;
429                 text-decoration: none;
430                 margin: 0;
431                 padding: 0.2rem;
432             }
433
434             li.test-group-list-show-all {
435                 font-size: 0.8rem;
436                 margin-top: 0.5rem;
437                 padding-right: 1rem;
438                 text-align: center;
439                 color: #999;
440             }
441
442             li.test-group-list-show-all:not(.selected) a:hover {
443                 background: inherit;
444             }
445
446             li.selected > a {
447                 background: rgba(204, 153, 51, 0.1);
448             }
449
450             li.hidden {
451                 color: #999;
452             }
453
454             li:not(.selected) > a:hover {
455                 background: #eee;
456             }
457
458             div.summary {
459                 padding-left: 1rem;
460             }
461
462             #test-group-details {
463                 display: table-cell;
464                 margin-bottom: 1rem;
465                 padding: 0 0.5rem;
466                 margin: 0;
467             }
468
469             #retry-form, #bisect-form {
470                 display: block;
471                 margin: 0.5rem;
472             }
473
474             #hide-button {
475                 margin: 0.5rem;
476             }`;
477     }
478 }
479
480 ComponentBase.defineElement('analysis-task-test-group-pane', AnalysisTaskTestGroupPane);
481
482 class AnalysisTaskPage extends PageWithHeading {
483     constructor()
484     {
485         super('Analysis Task');
486         this._renderTaskNameAndStatusLazily = new LazilyEvaluatedFunction(this._renderTaskNameAndStatus.bind(this));
487         this._renderCauseAndFixesLazily = new LazilyEvaluatedFunction(this._renderCauseAndFixes.bind(this));
488         this._renderRelatedTasksLazily = new LazilyEvaluatedFunction(this._renderRelatedTasks.bind(this));
489         this._resetVariables();
490     }
491
492     title() { return this._task ? this._task.label() : 'Analysis Task'; }
493     routeName() { return 'analysis/task'; }
494
495     _resetVariables()
496     {
497         this._task = null;
498         this._metric = null;
499         this._triggerable = null;
500         this._relatedTasks = null;
501         this._testGroups = null;
502         this._testGroupLabelMap = new Map;
503         this._analysisResults = null;
504         this._measurementSet = null;
505         this._startPoint = null;
506         this._endPoint = null;
507         this._errorMessage = null;
508         this._currentTestGroup = null;
509         this._filteredTestGroups = null;
510         this._showHiddenTestGroups = false;
511     }
512
513     updateFromSerializedState(state)
514     {
515         this._resetVariables();
516         if (state.remainingRoute) {
517             const taskId = parseInt(state.remainingRoute);
518             AnalysisTask.fetchById(taskId).then(this._didFetchTask.bind(this)).then(() => {
519                 this._fetchRelatedInfoForTaskId(taskId);
520             }, (error) => {
521                 this._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
522                 this.enqueueToRender();
523             });
524         } else if (state.buildRequest) {
525             const buildRequestId = parseInt(state.buildRequest);
526             AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then((task) => {
527                 this._fetchRelatedInfoForTaskId(task.id());
528             }, (error) => {
529                 this._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
530                 this.enqueueToRender();
531             });
532         }
533     }
534
535     didConstructShadowTree()
536     {
537         this.part('analysis-task-name').listenToAction('update', () => this._updateTaskName(this.part('analysis-task-name').editedText()));
538
539         this.content('change-type-form').onsubmit = ComponentBase.createEventHandler((event) => this._updateChangeType(event));
540
541         this.part('chart-pane').listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
542
543         const resultsPane = this.part('results-pane');
544         resultsPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
545         resultsPane.listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
546
547         this.part('configurator-pane').listenToAction('createCustomTestGroup', this._createCustomTestGroup.bind(this));
548
549         const groupPane = this.part('group-pane');
550         groupPane.listenToAction('showTestGroup', (testGroup) => this._showTestGroup(testGroup));
551         groupPane.listenToAction('showHiddenTestGroups', () => this._showAllTestGroups());
552         groupPane.listenToAction('renameTestGroup', (testGroup, newName) => this._updateTestGroupName(testGroup, newName));
553         groupPane.listenToAction('toggleTestGroupVisibility', (testGroup) => this._hideCurrentTestGroup(testGroup));
554         groupPane.listenToAction('retryTestGroup', (testGroup, repetitionCount, notifyOnCompletion) => this._retryCurrentTestGroup(testGroup, repetitionCount, notifyOnCompletion));
555         groupPane.listenToAction('bisectTestGroup', (testGroup, commitSets, repetitionCount, notifyOnCompletion) => this._bisectCurrentTestGroup(testGroup, commitSets, repetitionCount, notifyOnCompletion));
556
557         this.part('cause-list').listenToAction('addItem', (repository, revision) => {
558             this._associateCommit('cause', repository, revision);
559         });
560         this.part('fix-list').listenToAction('addItem', (repository, revision) => {
561             this._associateCommit('fix', repository, revision);
562         });
563     }
564
565     _fetchRelatedInfoForTaskId(taskId)
566     {
567         TestGroup.fetchForTask(taskId).then(this._didFetchTestGroups.bind(this));
568         AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
569         AnalysisTask.fetchRelatedTasks(taskId).then((relatedTasks) => {
570             this._relatedTasks = relatedTasks;
571             this.enqueueToRender();
572         });
573     }
574
575     _didFetchTask(task)
576     {
577         console.assert(!this._task);
578
579         this._task = task;
580         const bugList = this.part('bug-list');
581         this.part('bug-list').setTask(this._task);
582         this.enqueueToRender();
583
584         if (task.isCustom())
585             return task;
586
587         const platform = task.platform();
588         const metric = task.metric();
589         const lastModified = platform.lastModified(metric);
590         const triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
591         this._triggerable = triggerable && !triggerable.isDisabled() ? triggerable : null;
592         this._metric = metric;
593
594         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
595         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
596
597         const chart = this.part('chart-pane');
598         const domain = ChartsPage.createDomainForAnalysisTask(task);
599         chart.configure(platform.id(), metric.id());
600         chart.setOverviewDomain(domain[0], domain[1]);
601         chart.setMainDomain(domain[0], domain[1]);
602
603         return task;
604     }
605
606     _didFetchMeasurement()
607     {
608         console.assert(this._task);
609         console.assert(this._measurementSet);
610         var series = this._measurementSet.fetchedTimeSeries('current', false, false);
611         var startPoint = series.findById(this._task.startMeasurementId());
612         var endPoint = series.findById(this._task.endMeasurementId());
613
614         if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
615             return;
616
617         this.part('results-pane').setPoints(startPoint, endPoint, this._task.metric());
618
619         this._startPoint = startPoint;
620         this._endPoint = endPoint;
621         this.enqueueToRender();
622     }
623
624     _didFetchTestGroups(testGroups)
625     {
626         this._testGroups = testGroups.sort(function (a, b) { return +a.createdAt() - b.createdAt(); });
627         this._didUpdateTestGroupHiddenState();
628         this._assignTestResultsIfPossible();
629         this.enqueueToRender();
630     }
631
632     _showAllTestGroups()
633     {
634         this._showHiddenTestGroups = true;
635         this._didUpdateTestGroupHiddenState();
636         this.enqueueToRender();
637     }
638
639     _didUpdateTestGroupHiddenState()
640     {
641         if (!this._showHiddenTestGroups)
642             this._filteredTestGroups = this._testGroups.filter(function (group) { return !group.isHidden(); });
643         else
644             this._filteredTestGroups = this._testGroups;
645         this._showTestGroup(this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null);
646     }
647
648     _didFetchAnalysisResults(results)
649     {
650         this._analysisResults = results;
651         if (this._assignTestResultsIfPossible())
652             this.enqueueToRender();
653     }
654
655     _assignTestResultsIfPossible()
656     {
657         if (!this._task || !this._testGroups || !this._analysisResults)
658             return false;
659
660         let metric = this._metric;
661         this.part('group-pane').setAnalysisResults(this._analysisResults, metric);
662         if (metric) {
663             const view = this._analysisResults.viewForMetric(metric);
664             this.part('results-pane').setAnalysisResultsView(view);
665         }
666
667         return true;
668     }
669
670     render()
671     {
672         super.render();
673
674         Instrumentation.startMeasuringTime('AnalysisTaskPage', 'render');
675
676         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
677
678         this._renderTaskNameAndStatusLazily.evaluate(this._task, this._task ? this._task.name() : null, this._task ? this._task.changeType() : null);
679         this._renderCauseAndFixesLazily.evaluate(this._startPoint, this._task, this.part('cause-list'), this._task ? this._task.causes() : []);
680         this._renderCauseAndFixesLazily.evaluate(this._startPoint, this._task, this.part('fix-list'), this._task ? this._task.fixes() : []);
681         this._renderRelatedTasksLazily.evaluate(this._task, this._relatedTasks);
682
683         this.content('chart-pane').style.display = this._task && !this._task.isCustom() ? null : 'none';
684         this.part('chart-pane').setShowForm(!!this._triggerable);
685
686         this.content('results-pane').style.display = this._task && !this._task.isCustom() ? null : 'none';
687         this.part('results-pane').setShowForm(!!this._triggerable);
688
689         this.content('configurator-pane').style.display = this._task && this._task.isCustom() ? null : 'none';
690
691         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
692     }
693
694     _renderTaskNameAndStatus(task, taskName, changeType)
695     {
696         this.part('analysis-task-name').setText(taskName);
697         if (task && !task.isCustom()) {
698             const link = ComponentBase.createLink;
699             const platform = task.platform();
700             const metric = task.metric();
701             const subtitle = `${metric.fullName()} on ${platform.label()}`;
702             this.renderReplace(this.content('platform-metric-names'),
703                 link(subtitle, this.router().url('charts', ChartsPage.createStateForAnalysisTask(task))));
704         }
705         this.content('change-type').value = changeType || 'unconfirmed';
706     }
707
708     _renderRelatedTasks(task, relatedTasks)
709     {
710         const element = ComponentBase.createElement;
711         const link = ComponentBase.createLink;
712         this.renderReplace(this.content('related-tasks-list'), (task && relatedTasks ? relatedTasks : []).map((otherTask) => {
713                 let suffix = '';
714                 const taskLabel = otherTask.label();
715                 if (otherTask.metric() && otherTask.metric() != task.metric() && taskLabel.indexOf(otherTask.metric().label()) < 0)
716                     suffix += ` with "${otherTask.metric().label()}"`;
717                 if (otherTask.platform() && otherTask.platform() != task.platform() && taskLabel.indexOf(otherTask.platform().label()) < 0)
718                     suffix += ` on ${otherTask.platform().label()}`;
719                 return element('li', [link(taskLabel, this.router().url(`analysis/task/${otherTask.id()}`)), suffix]);
720             }));
721     }
722
723     _renderCauseAndFixes(startPoint, task, list, commits)
724     {
725         const hasData = startPoint && task;
726         this.content('cause-fix').style.display = hasData ? null : 'none';
727         if (!hasData)
728             return;
729
730         const commitSet = startPoint.commitSet();
731         const repositoryList = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());
732
733         const makeItem = (commit) => {
734             return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
735                 'Disassociate this commit', this._dissociateCommit.bind(this, commit));
736         }
737
738         list.setKindList(repositoryList);
739         list.setList(commits.map((commit) => makeItem(commit)));
740     }
741
742     _showTestGroup(testGroup)
743     {
744         this._currentTestGroup = testGroup;
745         this.part('configurator-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
746         this.part('results-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
747         const groupsInReverseChronology = this._filteredTestGroups.slice(0).reverse();
748         const showHiddenGroups = !this._testGroups.some((group) => group.isHidden()) || this._showHiddenTestGroups;
749         this.part('group-pane').setTestGroups(groupsInReverseChronology, this._currentTestGroup, showHiddenGroups);
750         this.enqueueToRender();
751     }
752
753     _updateTaskName(newName)
754     {
755         console.assert(this._task);
756
757         return this._task.updateName(newName).then(() => {
758             this.enqueueToRender();
759         }, (error) => {
760             this.enqueueToRender();
761             alert('Failed to update the name: ' + error);
762         });
763     }
764
765     _updateTestGroupName(testGroup, newName)
766     {
767         return testGroup.updateName(newName).then(() => {
768             this._showTestGroup(this._currentTestGroup);
769             this.enqueueToRender();
770         }, (error) => {
771             this.enqueueToRender();
772             alert('Failed to hide the test name: ' + error);
773         });
774     }
775
776     _hideCurrentTestGroup(testGroup)
777     {
778         return testGroup.updateHiddenFlag(!testGroup.isHidden()).then(() => {
779             this._didUpdateTestGroupHiddenState();
780             this.enqueueToRender();
781         }, function (error) {
782             this._mayHaveMutatedTestGroupHiddenState();
783             this.enqueueToRender();
784             alert('Failed to update the group: ' + error);
785         });
786     }
787
788     _updateChangeType(event)
789     {
790         event.preventDefault();
791         console.assert(this._task);
792
793         let newChangeType = this.content('change-type').value;
794         if (newChangeType == 'unconfirmed')
795             newChangeType = null;
796
797         const updateRendering = () => {
798             this.part('chart-pane').didUpdateAnnotations();
799             this.enqueueToRender();
800         };
801         return this._task.updateChangeType(newChangeType).then(updateRendering, (error) => {
802             updateRendering();
803             alert('Failed to update the status: ' + error);
804         });
805     }
806
807     _associateCommit(kind, repository, revision)
808     {
809         const updateRendering = () => { this.enqueueToRender(); };
810         return this._task.associateCommit(kind, repository, revision).then(updateRendering, (error) => {
811             updateRendering();
812             if (error == 'AmbiguousRevision')
813                 alert('There are multiple revisions that match the specified string: ' + revision);
814             else if (error == 'CommitNotFound')
815                 alert('There are no revisions that match the specified string:' + revision);
816             else
817                 alert('Failed to associate the commit: ' + error);
818         });
819     }
820
821     _dissociateCommit(commit)
822     {
823         const updateRendering = () => { this.enqueueToRender(); };
824         return this._task.dissociateCommit(commit).then(updateRendering, (error) => {
825             updateRendering();
826             alert('Failed to dissociate the commit: ' + error);
827         });
828     }
829
830     _retryCurrentTestGroup(testGroup, repetitionCount, notifyOnCompletion)
831     {
832         const existingNames = (this._testGroups || []).map((group) => group.name());
833         const newName = CommitSet.createNameWithoutCollision(testGroup.name(), new Set(existingNames));
834         const commitSetList = testGroup.requestedCommitSets();
835         const platform = this._task.platform() || testGroup.platform();
836         return TestGroup.createWithCustomConfiguration(this._task, platform, testGroup.test(), newName, repetitionCount, commitSetList, notifyOnCompletion)
837             .then(this._didFetchTestGroups.bind(this), function (error) {
838             alert('Failed to create a new test group: ' + error);
839         });
840     }
841
842     async _bisectCurrentTestGroup(testGroup, commitSets, repetitionCount, notifyOnCompletion)
843     {
844         console.assert(testGroup.task());
845         const existingTestGroupNames = new Set((this._testGroups || []).map((testGroup) => testGroup.name()));
846
847         for (let i = 1; i < commitSets.length; i++) {
848             const previousCommitSet = commitSets[i - 1];
849             const currentCommitSet = commitSets[i];
850             const testGroupName = CommitSet.createNameWithoutCollision(CommitSet.diff(previousCommitSet, currentCommitSet), existingTestGroupNames);
851             try {
852                 const testGroups = await TestGroup.createAndRefetchTestGroups(testGroup.task(), testGroupName, repetitionCount, [previousCommitSet, currentCommitSet], notifyOnCompletion);
853                 await this._didFetchTestGroups(testGroups);
854             } catch(error) {
855                 alert('Failed to create a new test group: ' + error);
856                 break;
857             }
858         }
859     }
860
861     _createTestGroupAfterVerifyingCommitSetList(testGroupName, repetitionCount, commitSetMap, notifyOnCompletion)
862     {
863         if (this._hasDuplicateTestGroupName(testGroupName)) {
864             alert(`There is already a test group named "${testGroupName}"`);
865             return;
866         }
867
868         const firstLabel = Object.keys(commitSetMap)[0];
869         const firstCommitSet = commitSetMap[firstLabel];
870
871         for (let currentLabel in commitSetMap) {
872             const commitSet = commitSetMap[currentLabel];
873             for (let repository of commitSet.repositories()) {
874                 if (!firstCommitSet.revisionForRepository(repository))
875                     return alert(`Set ${currentLabel} specifies ${repository.label()} but set ${firstLabel} does not.`);
876             }
877             for (let repository of firstCommitSet.repositories()) {
878                 if (!commitSet.revisionForRepository(repository))
879                     return alert(`Set ${firstLabel} specifies ${repository.label()} but set ${currentLabel} does not.`);
880             }
881         }
882
883         const commitSets = [];
884         for (let label in commitSetMap)
885             commitSets.push(commitSetMap[label]);
886
887         return TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, commitSets, notifyOnCompletion)
888             .then(this._didFetchTestGroups.bind(this), function (error) {
889             alert('Failed to create a new test group: ' + error);
890         });
891     }
892
893     _createCustomTestGroup(repetitionCount, testGroupName, commitSets, platform, test, notifyOnCompletion)
894     {
895         console.assert(this._task.isCustom());
896         if (this._hasDuplicateTestGroupName(testGroupName)) {
897             alert(`There is already a test group named "${testGroupName}"`);
898             return;
899         }
900
901         TestGroup.createWithCustomConfiguration(this._task, platform, test, testGroupName, repetitionCount, commitSets, notifyOnCompletion)
902             .then(this._didFetchTestGroups.bind(this), function (error) {
903             alert('Failed to create a new test group: ' + error);
904         });
905     }
906
907     _hasDuplicateTestGroupName(name)
908     {
909         console.assert(this._testGroups);
910         for (var group of this._testGroups) {
911             if (group.name() == name)
912                 return true;
913         }
914         return false;
915     }
916
917     static htmlTemplate()
918     {
919         return `
920             <div class="analysis-task-page">
921                 <h2 class="analysis-task-name"><editable-text id="analysis-task-name"></editable-text></h2>
922                 <h3 id="platform-metric-names"></h3>
923                 <p class="error-message"></p>
924                 <div class="analysis-task-status">
925                     <section>
926                         <h3>Status</h3>
927                         <form id="change-type-form">
928                             <select id="change-type">
929                                 <option value="unconfirmed">Unconfirmed</option>
930                                 <option value="regression">Definite regression</option>
931                                 <option value="progression">Definite progression</option>
932                                 <option value="inconclusive">Inconclusive (Closed)</option>
933                                 <option value="unchanged">No change (Closed)</option>
934                             </select>
935                             <button type="submit">Save</button>
936                         </form>
937                     </section>
938                     <section class="associated-bugs">
939                         <h3>Associated Bugs</h3>
940                         <analysis-task-bug-list id="bug-list"></analysis-task-bug-list>
941                     </section>
942                     <section id="cause-fix">
943                         <h3>Caused by</h3>
944                         <mutable-list-view id="cause-list"></mutable-list-view>
945                         <h3>Fixed by</h3>
946                         <mutable-list-view id="fix-list"></mutable-list-view>
947                     </section>
948                     <section class="related-tasks">
949                         <h3>Related Tasks</h3>
950                         <ul id="related-tasks-list"></ul>
951                     </section>
952                 </div>
953                 <analysis-task-chart-pane id="chart-pane"></analysis-task-chart-pane>
954                 <analysis-task-results-pane id="results-pane"></analysis-task-results-pane>
955                 <analysis-task-configurator-pane id="configurator-pane"></analysis-task-configurator-pane>
956                 <analysis-task-test-group-pane id="group-pane"></analysis-task-test-group-pane>
957             </div>
958 `;
959     }
960
961     static cssTemplate()
962     {
963         return `
964             .analysis-task-page {
965             }
966
967             .analysis-task-name {
968                 font-size: 1.2rem;
969                 font-weight: inherit;
970                 color: #c93;
971                 margin: 0 1rem;
972                 padding: 0;
973             }
974
975             #platform-metric-names {
976                 font-size: 1rem;
977                 font-weight: inherit;
978                 color: #c93;
979                 margin: 0 1rem;
980                 padding: 0;
981             }
982
983             #platform-metric-names a {
984                 text-decoration: none;
985                 color: inherit;
986             }
987
988             #platform-metric-names:empty {
989                 visibility: hidden;
990                 height: 0;
991                 width: 0;
992                 /* FIXME: Use display: none instead once r214290 is shipped everywhere */
993             }
994
995             .error-message:empty {
996                 visibility: hidden;
997                 height: 0;
998                 width: 0;
999                 /* FIXME: Use display: none instead once r214290 is shipped everywhere */
1000             }
1001
1002             .error-message:not(:empty) {
1003                 margin: 1rem;
1004                 padding: 0;
1005             }
1006
1007             #chart-pane,
1008             #results-pane {
1009                 display: block;
1010                 padding: 0 1rem;
1011                 border-bottom: solid 1px #ccc;
1012             }
1013
1014             #results-pane {
1015                 margin-top: 1rem;
1016             }
1017
1018             #group-pane {
1019                 margin: 1rem;
1020                 margin-bottom: 2rem;
1021             }
1022
1023             .analysis-task-status {
1024                 margin: 0;
1025                 display: flex;
1026                 padding-bottom: 1rem;
1027                 margin-bottom: 1rem;
1028                 border-bottom: solid 1px #ccc;
1029             }
1030
1031             .analysis-task-status > section {
1032                 flex-grow: 1;
1033                 flex-shrink: 0;
1034                 border-left: solid 1px #eee;
1035                 padding-left: 1rem;
1036                 padding-right: 1rem;
1037             }
1038
1039             .analysis-task-status > section.related-tasks {
1040                 flex-shrink: 1;
1041             }
1042
1043             .analysis-task-status > section:first-child {
1044                 border-left: none;
1045             }
1046
1047             .analysis-task-status h3 {
1048                 font-size: 1rem;
1049                 font-weight: inherit;
1050                 color: #c93;
1051             }
1052
1053             .analysis-task-status ul,
1054             .analysis-task-status li {
1055                 list-style: none;
1056                 padding: 0;
1057                 margin: 0;
1058             }
1059
1060             .related-tasks-list {
1061                 max-height: 10rem;
1062                 overflow-y: scroll;
1063             }
1064
1065             .test-configuration h3 {
1066                 font-size: 1rem;
1067                 font-weight: inherit;
1068                 color: inherit;
1069                 margin: 0 1rem;
1070                 padding: 0;
1071             }`;
1072     }
1073 }