2 class AnalysisTaskChartPane extends ChartPaneBase {
5 super('analysis-task-chart-pane');
9 setPage(page) { this._page = page; }
10 router() { return this._page.router(); }
12 _mainSelectionDidChange(selection, didEndDrag)
14 super._mainSelectionDidChange(selection);
16 this._page._chartSelectionDidChange();
21 var selection = this._mainChart ? this._mainChart.currentSelection() : null;
25 return this._mainChart.sampledDataBetween('current', selection[0], selection[1]);
29 ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
31 class AnalysisTaskPage extends PageWithHeading {
34 super('Analysis Task');
36 this._relatedTasks = null;
37 this._testGroups = null;
38 this._renderedTestGroups = null;
39 this._testGroupLabelMap = new Map;
40 this._renderedCurrentTestGroup = undefined;
41 this._analysisResults = null;
42 this._measurementSet = null;
43 this._startPoint = null;
44 this._endPoint = null;
45 this._errorMessage = null;
46 this._currentTestGroup = null;
47 this._filteredTestGroups = null;
48 this._showHiddenTestGroups = false;
50 this._chartPane = this.content().querySelector('analysis-task-chart-pane').component();
51 this._chartPane.setPage(this);
52 this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
53 this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
54 this._analysisResultsViewer.setRangeSelectorLabels(['A', 'B']);
55 this._analysisResultsViewer.setRangeSelectorCallback(this._selectedRowInAnalysisResultsViewer.bind(this));
56 this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
57 this._taskNameLabel = this.content().querySelector('.analysis-task-name editable-text').component();
58 this._taskNameLabel.setStartedEditingCallback(this._didStartEditingTaskName.bind(this));
59 this._taskNameLabel.setUpdateCallback(this._updateTaskName.bind(this));
61 this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
62 this._taskStatusControl = this.content().querySelector('.change-type-form select');
64 this._bugList = this.content().querySelector('.associated-bugs mutable-list-view').component();
65 this._bugList.setKindList(BugTracker.all());
66 this._bugList.setAddCallback(this._associateBug.bind(this));
68 this._causeList = this.content().querySelector('.cause-list mutable-list-view').component();
69 this._causeList.setAddCallback(this._associateCommit.bind(this, 'cause'));
71 this._fixList = this.content().querySelector('.fix-list mutable-list-view').component();
72 this._fixList.setAddCallback(this._associateCommit.bind(this, 'fix'));
74 this._newTestGroupFormForChart = this.content().querySelector('.overview-chart customizable-test-group-form').component();
75 this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
77 this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view customizable-test-group-form').component();
78 this._newTestGroupFormForViewer.setStartCallback(this._createNewTestGroupFromViewer.bind(this));
80 this._retryForm = this.content().querySelector('.test-group-retry-form test-group-form').component();
81 this._retryForm.setStartCallback(this._retryCurrentTestGroup.bind(this));
82 this._hideButton = this.content().querySelector('.test-group-hide-button');
83 this._hideButton.onclick = this._hideCurrentTestGroup.bind(this);
86 title() { return this._task ? this._task.label() : 'Analysis Task'; }
87 routeName() { return 'analysis/task'; }
89 updateFromSerializedState(state)
92 if (state.remainingRoute) {
93 var taskId = parseInt(state.remainingRoute);
94 AnalysisTask.fetchById(taskId).then(this._didFetchTask.bind(this), function (error) {
95 self._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
98 this._fetchRelatedInfoForTaskId(taskId);
99 } else if (state.buildRequest) {
100 var buildRequestId = parseInt(state.buildRequest);
101 AnalysisTask.fetchByBuildRequestId(buildRequestId).then(this._didFetchTask.bind(this)).then(function (task) {
102 self._fetchRelatedInfoForTaskId(task.id());
103 }, function (error) {
104 self._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
110 _fetchRelatedInfoForTaskId(taskId)
112 TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
113 AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
114 AnalysisTask.fetchRelatedTasks(taskId).then(this._didFetchRelatedAnalysisTasks.bind(this));
119 console.assert(!this._task);
122 var platform = task.platform();
123 var metric = task.metric();
124 var lastModified = platform.lastModified(metric);
126 this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
127 this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
129 var formatter = metric.makeFormatter(4);
130 this._analysisResultsViewer.setValueFormatter(formatter);
131 this._testGroupResultsTable.setValueFormatter(formatter);
133 this._chartPane.configure(platform.id(), metric.id());
135 var domain = ChartsPage.createDomainForAnalysisTask(task);
136 this._chartPane.setOverviewDomain(domain[0], domain[1]);
137 this._chartPane.setMainDomain(domain[0], domain[1]);
144 _didFetchRelatedAnalysisTasks(relatedTasks)
146 this._relatedTasks = relatedTasks;
150 _didFetchMeasurement()
152 console.assert(this._task);
153 console.assert(this._measurementSet);
154 var series = this._measurementSet.fetchedTimeSeries('current', false, false);
155 var startPoint = series.findById(this._task.startMeasurementId());
156 var endPoint = series.findById(this._task.endMeasurementId());
157 if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
160 this._analysisResultsViewer.setPoints(startPoint, endPoint);
162 this._startPoint = startPoint;
163 this._endPoint = endPoint;
167 _didFetchTestGroups(testGroups)
169 this._testGroups = testGroups.sort(function (a, b) { return +a.createdAt() - b.createdAt(); });
170 this._didUpdateTestGroupHiddenState();
171 this._assignTestResultsIfPossible();
177 this._showHiddenTestGroups = true;
178 this._didUpdateTestGroupHiddenState();
182 _didUpdateTestGroupHiddenState()
184 this._renderedCurrentTestGroup = null;
185 this._renderedTestGroups = null;
186 if (!this._showHiddenTestGroups)
187 this._filteredTestGroups = this._testGroups.filter(function (group) { return !group.isHidden(); });
189 this._filteredTestGroups = this._testGroups;
190 this._currentTestGroup = this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null;
191 this._analysisResultsViewer.setTestGroups(this._filteredTestGroups);
192 this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
195 _didFetchAnalysisResults(results)
197 this._analysisResults = results;
198 if (this._assignTestResultsIfPossible())
202 _assignTestResultsIfPossible()
204 if (!this._task || !this._testGroups || !this._analysisResults)
207 for (var group of this._testGroups) {
208 for (var request of group.buildRequests())
209 request.setResult(this._analysisResults.find(request.buildId(), this._task.metric()));
212 this._analysisResultsViewer.didUpdateResults();
213 this._testGroupResultsTable.didUpdateResults();
222 Instrumentation.startMeasuringTime('AnalysisTaskPage', 'render');
224 this.content().querySelector('.error-message').textContent = this._errorMessage || '';
226 this._chartPane.render();
228 var element = ComponentBase.createElement;
229 var link = ComponentBase.createLink;
231 this._taskNameLabel.setText(this._task.name());
232 var platform = this._task.platform();
233 var metric = this._task.metric();
234 var anchor = this.content().querySelector('.platform-metric-names a');
235 this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
236 anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
239 this._bugList.setList(this._task.bugs().map(function (bug) {
240 return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
241 'Disassociate this bug', self._dissociateBug.bind(self, bug));
244 this._causeList.setList(this._task.causes().map(this._makeCommitListItem.bind(this)));
245 this._fixList.setList(this._task.fixes().map(this._makeCommitListItem.bind(this)));
247 this._taskStatusControl.value = this._task.changeType() || 'unconfirmed';
251 if (this._startPoint) {
252 var rootSet = this._startPoint.rootSet();
253 repositoryList = Repository.sortByNamePreferringOnesWithURL(rootSet.repositories());
255 repositoryList = Repository.sortByNamePreferringOnesWithURL(Repository.all());
257 this._bugList.render();
259 this._causeList.setKindList(repositoryList);
260 this._causeList.render();
262 this._fixList.setKindList(repositoryList);
263 this._fixList.render();
265 this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
266 this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
267 this.content().querySelector('.test-group-view').style.display = this._task ? null : 'none';
268 this._taskNameLabel.render();
270 if (this._relatedTasks && this._task) {
271 var router = this.router();
272 var link = ComponentBase.createLink;
273 var thisTask = this._task;
274 this.renderReplace(this.content().querySelector('.related-tasks-list'),
275 this._relatedTasks.map(function (otherTask) {
276 console.assert(otherTask.metric() == thisTask.metric());
278 var taskLabel = otherTask.label();
279 if (otherTask.platform() != thisTask.platform() && taskLabel.indexOf(otherTask.platform().label()) < 0)
280 suffix = ` on ${otherTask.platform().label()}`;
281 return element('li', [link(taskLabel, router.url(`analysis/task/${otherTask.id()}`)), suffix]);
285 var selectedRange = this._analysisResultsViewer.selectedRange();
286 var a = selectedRange['A'];
287 var b = selectedRange['B'];
288 this._newTestGroupFormForViewer.setRootSetMap(a && b ? {'A': a.rootSet(), 'B': b.rootSet()} : null);
289 this._newTestGroupFormForViewer.render();
291 this._renderTestGroupList();
292 this._renderTestGroupDetails();
294 var points = this._chartPane.selectedPoints();
295 this._newTestGroupFormForChart.setRootSetMap(points && points.length >= 2 ?
296 {'A': points[0].rootSet(), 'B': points[points.length - 1].rootSet()} : null);
297 this._newTestGroupFormForChart.render();
299 this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
300 this._analysisResultsViewer.render();
302 this._testGroupResultsTable.render();
304 Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
307 _makeCommitListItem(commit)
309 return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
310 'Disassociate this commit', this._dissociateCommit.bind(this, commit));
313 _renderTestGroupList()
315 var element = ComponentBase.createElement;
316 var link = ComponentBase.createLink;
317 if (this._testGroups != this._renderedTestGroups) {
318 this._renderedTestGroups = this._testGroups;
319 this._testGroupLabelMap.clear();
321 var unhiddenTestGroups = this._filteredTestGroups.filter(function (group) { return !group.isHidden(); });
322 var hiddenTestGroups = this._filteredTestGroups.filter(function (group) { return group.isHidden(); });
325 for (var group of hiddenTestGroups)
326 listItems.unshift(this._createTestGroupListItem(group));
327 for (var group of unhiddenTestGroups)
328 listItems.unshift(this._createTestGroupListItem(group));
330 if (this._testGroups.length != this._filteredTestGroups.length) {
331 listItems.push(element('li', {class: 'test-group-list-show-all'},
332 link('Show hidden tests', this._showAllTestGroups.bind(this))));
335 this.renderReplace(this.content().querySelector('.test-group-list'), listItems);
337 this._renderedCurrentTestGroup = null;
340 if (this._testGroups) {
341 for (var testGroup of this._filteredTestGroups) {
342 var label = this._testGroupLabelMap.get(testGroup);
343 label.setText(testGroup.label());
349 _createTestGroupListItem(group)
351 var text = new EditableText(group.label());
352 text.setStartedEditingCallback(function () { return text.render(); });
353 text.setUpdateCallback(this._updateTestGroupName.bind(this, group));
355 this._testGroupLabelMap.set(group, text);
356 return ComponentBase.createElement('li', {class: 'test-group-list-' + group.id()},
357 ComponentBase.createLink(text, group.label(), this._showTestGroup.bind(this, group)));
360 _renderTestGroupDetails()
362 if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
363 if (this._renderedCurrentTestGroup) {
364 var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
366 element.classList.remove('selected');
368 if (this._currentTestGroup) {
369 var element = this.content().querySelector('.test-group-list-' + this._currentTestGroup.id());
371 element.classList.add('selected');
374 this._chartPane.setMainSelection(null);
375 if (this._currentTestGroup) {
376 var rootSetsInTestGroup = this._currentTestGroup.requestedRootSets();
377 var startTime = rootSetsInTestGroup[0].latestCommitTime();
378 var endTime = rootSetsInTestGroup[rootSetsInTestGroup.length - 1].latestCommitTime();
379 if (startTime != endTime)
380 this._chartPane.setMainSelection([startTime, endTime]);
383 this._retryForm.setLabel('Retry');
384 if (this._currentTestGroup)
385 this._retryForm.setRepetitionCount(this._currentTestGroup.repetitionCount());
386 this._retryForm.element().style.display = this._currentTestGroup ? null : 'none';
388 this.content().querySelector('.test-group-hide-button').textContent
389 = this._currentTestGroup && this._currentTestGroup.isHidden() ? 'Unhide' : 'Hide';
391 this.content().querySelector('.pending-request-cancel-warning').style.display
392 = this._currentTestGroup && this._currentTestGroup.hasPending() ? null : 'none';
394 this._renderedCurrentTestGroup = this._currentTestGroup;
396 this._retryForm.render();
399 _showTestGroup(testGroup)
401 this._currentTestGroup = testGroup;
402 this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
406 _didStartEditingTaskName()
408 this._taskNameLabel.render();
413 console.assert(this._task);
414 this._taskNameLabel.render();
417 return self._task.updateName(self._taskNameLabel.editedText()).then(function () {
419 }, function (error) {
421 alert('Failed to update the name: ' + error);
425 _updateTestGroupName(testGroup)
427 var label = this._testGroupLabelMap.get(testGroup);
431 return testGroup.updateName(label.editedText()).then(function () {
433 }, function (error) {
435 alert('Failed to hide the test name: ' + error);
439 _hideCurrentTestGroup()
442 console.assert(this._currentTestGroup);
443 return this._currentTestGroup.updateHiddenFlag(!this._currentTestGroup.isHidden()).then(function () {
444 self._didUpdateTestGroupHiddenState();
446 }, function (error) {
447 self._mayHaveMutatedTestGroupHiddenState();
449 alert('Failed to update the group: ' + error);
453 _updateChangeType(event)
455 event.preventDefault();
456 console.assert(this._task);
458 var newChangeType = this._taskStatusControl.value;
459 if (newChangeType == 'unconfirmed')
460 newChangeType = null;
462 var render = this.render.bind(this);
463 return this._task.updateChangeType(newChangeType).then(render, function (error) {
465 alert('Failed to update the status: ' + error);
469 _associateBug(tracker, bugNumber)
471 console.assert(tracker instanceof BugTracker);
472 bugNumber = parseInt(bugNumber);
474 var render = this.render.bind(this);
475 return this._task.associateBug(tracker, bugNumber).then(render, function (error) {
477 alert('Failed to associate the bug: ' + error);
483 var render = this.render.bind(this);
484 return this._task.dissociateBug(bug).then(render, function (error) {
486 alert('Failed to dissociate the bug: ' + error);
490 _associateCommit(kind, repository, revision)
492 var render = this.render.bind(this);
493 return this._task.associateCommit(kind, repository, revision).then(render, function (error) {
495 alert('Failed to associate the commit: ' + error);
499 _dissociateCommit(commit)
501 var render = this.render.bind(this);
502 return this._task.dissociateCommit(commit).then(render, function (error) {
504 alert('Failed to dissociate the commit: ' + error);
508 _retryCurrentTestGroup(repetitionCount)
510 console.assert(this._currentTestGroup);
511 var testGroup = this._currentTestGroup;
512 var newName = this._createRetryNameForTestGroup(testGroup.name());
513 var rootSetList = testGroup.requestedRootSets();
516 for (var rootSet of rootSetList)
517 rootSetMap[testGroup.labelForRootSet(rootSet)] = rootSet;
519 return this._createTestGroupAfterVerifyingRootSetList(newName, repetitionCount, rootSetMap);
522 _chartSelectionDidChange()
527 _createNewTestGroupFromChart(name, repetitionCount, rootSetMap)
529 return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
532 _selectedRowInAnalysisResultsViewer()
537 _createNewTestGroupFromViewer(name, repetitionCount, rootSetMap)
539 return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
542 _createTestGroupAfterVerifyingRootSetList(testGroupName, repetitionCount, rootSetMap)
544 if (this._hasDuplicateTestGroupName(testGroupName))
545 alert(`There is already a test group named "${testGroupName}"`);
547 var rootSetsByName = {};
549 for (firstLabel in rootSetMap) {
550 var rootSet = rootSetMap[firstLabel];
551 for (var repository of rootSet.repositories())
552 rootSetsByName[repository.name()] = [];
557 for (var label in rootSetMap) {
558 var rootSet = rootSetMap[label];
559 for (var repository of rootSet.repositories()) {
560 var list = rootSetsByName[repository.name()];
562 alert(`Set ${label} specifies ${repository.label()} but set ${firstLabel} does not.`);
565 list.push(rootSet.revisionForRepository(repository));
568 for (var name in rootSetsByName) {
569 var list = rootSetsByName[name];
570 if (list.length < setIndex) {
571 alert(`Set ${firstLabel} specifies ${name} but set ${label} does not.`);
577 TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, rootSetsByName)
578 .then(this._didFetchTestGroups.bind(this), function (error) {
579 alert('Failed to create a new test group: ' + error);
583 _createRetryNameForTestGroup(name)
585 var nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
587 if (nameWithNumberMatch) {
588 name = nameWithNumberMatch[1];
589 number = parseInt(nameWithNumberMatch[2]);
595 newName = `${name} (${number})`;
596 } while (this._hasDuplicateTestGroupName(newName));
601 _hasDuplicateTestGroupName(name)
603 console.assert(this._testGroups);
604 for (var group of this._testGroups) {
605 if (group.name() == name)
611 static htmlTemplate()
614 <div class="analysis-task-page">
615 <h2 class="analysis-task-name"><editable-text></editable-text></h2>
616 <h3 class="platform-metric-names"><a href=""></a></h3>
617 <p class="error-message"></p>
618 <div class="analysis-task-status">
621 <form class="change-type-form">
623 <option value="unconfirmed">Unconfirmed</option>
624 <option value="regression">Definite regression</option>
625 <option value="progression">Definite progression</option>
626 <option value="inconclusive">Inconclusive (Closed)</option>
627 <option value="unchanged">No change (Closed)</option>
629 <button type="submit">Save</button>
632 <section class="associated-bugs">
633 <h3>Associated Bugs</h3>
634 <mutable-list-view></mutable-list-view>
636 <section class="cause-fix">
638 <span class="cause-list"><mutable-list-view></mutable-list-view></span>
640 <span class="fix-list"><mutable-list-view></mutable-list-view></span>
642 <section class="related-tasks">
643 <h3>Related Tasks</h3>
644 <ul class="related-tasks-list"></ul>
647 <section class="overview-chart">
648 <analysis-task-chart-pane></analysis-task-chart-pane>
649 <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
651 <section class="analysis-results-view">
652 <analysis-results-viewer></analysis-results-viewer>
653 <div class="new-test-group-form"><customizable-test-group-form></customizable-test-group-form></div>
655 <section class="test-group-view">
656 <ul class="test-group-list"></ul>
657 <div class="test-group-details">
658 <test-group-results-table></test-group-results-table>
659 <div class="test-group-retry-form"><test-group-form></test-group-form></div>
660 <button class="test-group-hide-button">Hide</button>
661 <span class="pending-request-cancel-warning">(cancels pending requests)</span>
671 .analysis-task-page {
674 .analysis-task-name {
676 font-weight: inherit;
682 .platform-metric-names {
684 font-weight: inherit;
690 .platform-metric-names a {
691 text-decoration: none;
695 .platform-metric-names:empty {
699 .error-message:not(:empty) {
708 .analysis-task-status {
711 padding-bottom: 1rem;
713 border-bottom: solid 1px #ccc;
716 .analysis-task-status > section {
719 border-left: solid 1px #eee;
724 .analysis-task-status > section.related-tasks {
728 .analysis-task-status > section:first-child {
732 .analysis-task-status h3 {
734 font-weight: inherit;
738 .analysis-task-status ul,
739 .analysis-task-status li {
745 .related-tasks-list {
751 .analysis-results-view {
752 border-top: solid 1px #ccc;
753 border-bottom: solid 1px #ccc;
758 .test-configuration h3 {
760 font-weight: inherit;
772 .test-group-details {
779 .new-test-group-form,
780 .test-group-retry-form {
785 .test-group-hide-button {
794 border-right: solid 1px #ccc;
799 .test-group-list:empty {
805 .test-group-list > li {
810 .test-group-list > li > a {
813 text-decoration: none;
818 .test-group-list > li.test-group-list-show-all {
826 .test-group-list > li.test-group-list-show-all:not(.selected) a:hover {
830 .test-group-list > li.selected > a {
831 background: rgba(204, 153, 51, 0.1);
834 .test-group-list > li:not(.selected) > a:hover {