<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[214502] trunk/Websites/perf.webkit.org</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/214502">214502</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-03-28 16:00:36 -0700 (Tue, 28 Mar 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Modernize AnalysisTaskPage
https://bugs.webkit.org/show_bug.cgi?id=170165

Reviewed by Antti Koivisto.

Modernized AnalysisTaskPage and related components. The main refactoring happened in AnalysisTaskPage
from which AnalysisTaskResultsPane and AnalysisTaskTestGroupPane have been extracted.

Decoupled BuildRequest from its results. AnalysisResultsViewer and TestGroupResultsTable now stores
a reference to AnalysisResultsView and Metric to find the results for each build request.
This refactoring is necessary in order to view results of an arbitrary metric in the future.

Also refactored ResultsTable and its subclasses extensively. Instead of making its render() to invoke
subclass' methods such as buildRowGroups, heading, and additionalHeading, rely on each subclass call
to invoke renderTable(), renamed from render(), with callbacks to add extra headers and columns.

This patch also fixes a number of usability issues found by the user such as changing the test name
resets the customized revisions by the virtue of the modern code being naturally more correct.

* public/v3/components/analysis-results-viewer.js:
(AnalysisResultsViewer):
(AnalysisResultsViewer.prototype.setTestGroupCallback): Deleted. Replaced by &quot;testGroupClick&quot; action.
(AnalysisResultsViewer.prototype.setRangeSelectorLabels): Moved here from ResultsTable since it's
never used in ResultsTable or TestGroupResultsTable.
(AnalysisResultsViewer.prototype.selectedRange): Ditto.
(AnalysisResultsViewer.prototype.setPoints): Now takes metric as the third argument.
(AnalysisResultsViewer.prototype.setTestGroups): Now takes the current test group.
(AnalysisResultsViewer.prototype.didUpdateResults): Deleted.
(AnalysisResultsViewer.prototype.setAnalysisResultsView): Added.
(AnalysisResultsViewer.prototype.render): Invoke _renderTestGroups lazily. Also simplified the logic
to find the selected list item. Since we always use a shadow DOM now, we can simply look for an element
with &quot;.seleted&quot; instead of crafting a unique class name.
(AnalysisResultsViewer.prototype.renderTestGroups): Renamed from buildRowGroups. Specify callbacks to
insert headers for A/B radio buttons, which has been moved from ResultsTable.prototype.render, and the
stacked blocks of testing results.
(AnalysisResultsViewer.prototype._classForTestGroup): Deleted.
(AnalysisResultsViewer.prototype._openStackingBlock): Deleted.
(AnalysisResultsViewer.prototype._expandBetween): Create a new set for expandedPoints to make
_renderTestGroupsLazily.evaluate do the work.
(AnalysisResultsViewer._layoutBlocks): Moved from TestGroupStackingGrid.layout.
(AnalysisResultsViewer._sortBlocksByRow): Moved from AnalysisResultsViewer.TestGroupStackingGrid.
(AnalysisResultsViewer._insertAfterBlockWithSameRange): Ditto.
(AnalysisResultsViewer._insertBlockInFirstAvailableColumn): Ditto.
(AnalysisResultsViewer._createCellsForRow): Ditto.

(AnalysisResultsViewer.TestGroupStackingBlock):
(AnalysisResultsViewer.TestGroupStackingBlock.prototype.addRowIndex):
(AnalysisResultsViewer.TestGroupStackingBlock.prototype.createStackingCell): No longer creates a unique
class name here. See the inline comment for AnalysisResultsViewer.prototype.render.
(AnalysisResultsViewer.TestGroupStackingBlock.prototype.isThin): Deleted. We used to collapse &quot;failed&quot;
test groups as a thin vertical line, and we wanted to show them next to each other in _layoutBlock but
we don't do that anymore.
(AnalysisResultsViewer.TestGroupStackingBlock.prototype._valuesForCommitSet): Added. Uses
this._analysisResultsView to extract the results for the current metrics.
(AnalysisResultsViewer.TestGroupStackingBlock.prototype._computeTestGroupStatus):

* public/v3/components/analysis-task-bug-list.js: Added.
(AnalysisTaskBugList): Added. Extracted from AnalysisTaskChartPane.
(AnalysisTaskBugList.prototype.setTask): Added.
(AnalysisTaskBugList.prototype.didConstructShadowTree): Added.
(AnalysisTaskBugList.prototype.render): Added.
(AnalysisTaskBugList.prototype._associateBug): Added.
(AnalysisTaskBugList.prototype._dissociateBug): Added.
(AnalysisTaskBugList.htmlTemplate): Added.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase.htmlTemplate): Added a hook to insert more content at the end in AnalysisTaskChartPane.
(ChartPaneBase.paneFooterTemplate): Added.

* public/v3/components/customizable-test-group-form.js:
(CustomizableTestGroupForm):
(CustomizableTestGroupForm.prototype.setCommitSetMap):
(CustomizableTestGroupForm.prototype.startTesting): Renamed from _submitted. Now dispatches an action
by the name of &quot;startTesting&quot; instead of calling this._startCallback.
(CustomizableTestGroupForm.prototype.didConstructShadowTree): Added. Moved the logic to attach event
handlers here to avoid eagerly creating the shadow tree in the constructor.
(CustomizableTestGroupForm.prototype._computeCommitSetMap): Use the newly added this._revisionEditorMap
to find the relevant input element instead of running a querySelector.
(CustomizableTestGroupForm.prototype.render): Lazily invoke _renderCustomRevisionTable. This avoids
overriding the customized revisions when the user finally types in the test group name.
(CustomizableTestGroupForm.prototype._renderCustomRevisionTable): Extracted from render.
(CustomizableTestGroupForm.prototype._constructRevisionRadioButtons): Made this a non-static method
since it needs to update this._revisionEditorMap now. Merged _constructRevisionRadioButtons.
(CustomizableTestGroupForm.prototype._createRadioButton): Deleted. See above.
(CustomizableTestGroupForm.cssTemplate):
(CustomizableTestGroupForm.formContent): Use IDs instead of classes to make this.content(ID) work.

* public/v3/components/mutable-list-view.js:
(MutableListView.prototype.setList):
(MutableListView.prototype.setKindList):
(MutableListView.prototype.setAddCallback): Deleted. Replaced by &quot;addItem&quot; action.
(MutableListView.prototype.render):
(MutableListItem.prototype.content):

* public/v3/components/results-table.js:
(ResultsTable): Removed this._rangeSelectorLabels, this._rangeSelectorCallback, and this._selectedRange
as they are only used by AnalysisResultsViewer. Also replaced this._valueFormatter by
this._analysisResultsView which knows a metric.
(ResultsTable.prototype.setValueFormatter): Deleted.
(ResultsTable.prototype.setRangeSelectorLabels): Deleted.
(ResultsTable.prototype.setRangeSelectorCallback): Deleted.
(ResultsTable.prototype.selectedRange): Deleted.
(ResultsTable.prototype._rangeSelectorClicked): Deleted.
(ResultsTable.prototype.setAnalysisResultsView): Added.
(ResultsTable.prototype.renderTable): Added. Removed the logic to add _rangeSelectorLabels since it has
been moved to AnalysisResultsViewer.prototype.render inside buildColumns, which also inserts additional
columns which used to be stored on each ResultsTableRow. Use the same technique to insert additional
headers. Also take the name (thead tr th) of row header (tbody tr td) as an argument and automatically
create a table cell of an appropriate colspan.
(ResultsTable.prototype._createRevisionListCells):
(ResultsTable.prototype.heading): Deleted. Superseded by buildHeaders callback.
(ResultsTable.prototype.additionalHeading): Ditto.
(ResultsTable.prototype.buildRowGroups): Deleted. It is now the responsibility of each subclass to call
ResultsTable's renderTable() in the subclass' render() function.
(ResultsTable.prototype._computeRepositoryList): No longer takes extraRepositories as an argument.
Instead, this function now returns a pair of the repository list and the list of constant commits.
(ResultsTable.htmlTemplate):
(ResultsTable.cssTemplate):

* public/v3/components/test-group-form.js:
(TestGroupForm): Avoid eagerly creating the shadow tree. Also removed the removed the dead code.
(TestGroupForm.prototype.setRepetitionCount): Simply override the value of the select element.
(TestGroupForm.prototype.didConstructShadowTree): Added. Attach event handlers here to avoid eagerly
creating the shadow tree in the constructor.
(TestGroupForm.prototype.startTesting): Renamed from _submitted. Dispatch &quot;startTesting&quot; action instead
of invoking _startCallback which has been removed.
(TestGroupForm.htmlTemplate):
(TestGroupForm.formContent):

* public/v3/components/test-group-results-table.js:
(TestGroupResultsTable):
(TestGroupResultsTable.prototype.didUpdateResults): Deleted. No longer neeed per setAnalysisResultsView
in ResultsTable.
(TestGroupResultsTable.prototype.setTestGroup):
(TestGroupResultsTable.prototype.heading): Deleted.
(TestGroupResultsTable.prototype.render):
(TestGroupResultsTable.prototype._renderTestGroup): Extracted from render.
(TestGroupResultsTable.prototype._buildRowGroups): Renamed from buildRowGroups.
(TestGroupResultsTable.prototype._buildRowGroupForCommitSet): Extracted from buildRowGroups.
(TestGroupResultsTable.prototype._buildComparisonRow): Extracted from buildRowGroups.buildRowGroups

* public/v3/index.html: Include analysis-task-bug-list.js.

* public/v3/models/analysis-results.js:
(AnalysisResults): Inverted the map so that we can easily create a view based on metric.
(AnalysisResults.prototype.find): Ditto.
(AnalysisResults.prototype.add): Ditto.
(AnalysisResults.prototype.viewForMetric): Added.
(AnalysisResults.fetch):
(AnalysisResultsView): Added.
(AnalysisResultsView.prototype.metric): Added.
(AnalysisResultsView.prototype.resultForBuildId): Added.

* public/v3/models/build-request.js:
(BuildRequest.result): Deleted.
(BuildRequest.setResult): Deleted.

* public/v3/models/test-group.js:
(TestGroup): Removed this._allCommitSets since it was never used.
(TestGroup.prototype.didSetResult): Deleted since it was never used.
(TestGroup.prototype.compareTestResults): Now takes an array of measurement set values.
(TestGroup.prototype._valuesForCommitSet): Deleted.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane): This class now includes the form to cutomize the revisions.
(AnalysisTaskChartPane.prototype.setShowForm): Added.
(AnalysisTaskChartPane.prototype._mainSelectionDidChange):
(AnalysisTaskChartPane.prototype.didConstructShadowTree): Added. Dispatches &quot;newTestGroup&quot; action when
the user presses the button to start a new A/B testing from the chart.
(AnalysisTaskChartPane.prototype.render): Added.
(AnalysisTaskChartPane.prototype.paneFooterTemplate): Added.
(AnalysisTaskChartPane.cssTemplate):

(AnalysisTaskResultsPane): Added. Encapsulates AnalysisResultsViewer and CustomizableTestGroupForm.
(AnalysisTaskResultsPane.prototype.setPoints): Added.
(AnalysisTaskResultsPane.prototype.setTestGroups): Added.
(AnalysisTaskResultsPane.prototype.setAnalysisResultsView): Added.
(AnalysisTaskResultsPane.prototype.setShowForm): Added.
(AnalysisTaskResultsPane.prototype.didConstructShadowTree): Added. Dispatches &quot;newTestGroup&quot; action
when the user presses the button to start a new A/B testing from the chart.
(AnalysisTaskResultsPane.prototype.render): Added.
(AnalysisTaskResultsPane.htmlTemplate): Added.
(AnalysisTaskResultsPane.cssTemplate): Added.

(AnalysisTaskTestGroupPane): Added. Encapsulates TestGroupResultsTable and CustomizableTestGroupForm.
(AnalysisTaskTestGroupPane.prototype.didConstructShadowTree): Added.
(AnalysisTaskTestGroupPane.prototype.setTestGroups): Added.
(AnalysisTaskTestGroupPane.prototype.setAnalysisResultsView): Added.
(AnalysisTaskTestGroupPane.prototype.render): Added.
(AnalysisTaskTestGroupPane.prototype._renderTestGroups): Added. Updates the list of test groups. Hide
the hidden groups unless showHiddenGroups is set. Updates this._testGroupMap so that the visibility of
groups and their names can be updated without having to re-render the entire list.
(AnalysisTaskTestGroupPane.prototype._renderTestGroupVisibility): Added.
(AnalysisTaskTestGroupPane.prototype._renderTestGroupNames): Added.
(AnalysisTaskTestGroupPane.prototype._renderCurrentTestGroup): Added. Update TestGroupResultsTable with
the selected test group. Also highlight the list view, and update the hide-unhide toggle button's label
as needed.
(AnalysisTaskTestGroupPane.htmlTemplate): Added.
(AnalysisTaskTestGroupPane.cssTemplate): Added.

(AnalysisTaskPage): Deleted a massive number of instance variables. They are now manged by newly added
AnalysisTaskChartPane, AnalysisTaskResultsPane, and AnalysisTaskTestGroupPane
(AnalysisTaskPage.prototype.didConstructShadowTree): Added. Attach various event handlers here to avoid
eagerly creating the shadow tree in the constructor.
(AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId):
(AnalysisTaskPage.prototype._didFetchTask): No longer sets the value formatter to the results viewer
and the results table as they now recieve AnalysisResultsView later in _assignTestResultsIfPossible.
(AnalysisTaskPage.prototype._didFetchMeasurement): Set the metric to the results viewer.
(AnalysisTaskPage.prototype._didUpdateTestGroupHiddenState):
(AnalysisTaskPage.prototype._assignTestResultsIfPossible): Create AnalysisResultsView from the newly
retrieved AnalysisResults and pass it to AnalysisTaskResultsPane and AnalysisTaskTestGroupPane.
(AnalysisTaskPage.prototype.render): Dramatically simplified.
(AnalysisTaskPage.prototype._renderTaskNameAndStatus): Extracted from render.
(AnalysisTaskPage.prototype._renderRelatedTasks): Ditto.
(AnalysisTaskPage.prototype._renderCauseAndFixes): Ditto.
(AnalysisTaskPage.prototype._showTestGroup):
(AnalysisTaskPage.prototype._updateTaskName): Now takes the new name as an argument.
(AnalysisTaskPage.prototype._updateTestGroupName): Now takes the new name as the second argument.
(AnalysisTaskPage.prototype._hideCurrentTestGroup): Now takes the test group to hide.
(AnalysisTaskPage.prototype._associateCommit): Moved to AnalysisTaskBugList.
(AnalysisTaskPage.prototype._dissociateCommit): Ditto.
(AnalysisTaskPage.prototype._retryCurrentTestGroup): Now takes the test group as the first argument.
(AnalysisTaskPage.prototype._chartSelectionDidChange): Deleted.
(AnalysisTaskPage.prototype._createNewTestGroupFromChart): Deleted.
(AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): Deleted.
(AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Deleted.
(AnalysisTaskPage.htmlTemplate):
(AnalysisTaskPage.cssTemplate):

* unit-tests/test-groups-tests.js: Updated a test case which was expecting BuildReqeust's result, which
has been removed, to exist.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsanalysisresultsviewerjs">trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs">trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentscustomizabletestgroupformjs">trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsmutablelistviewjs">trunk/Websites/perf.webkit.org/public/v3/components/mutable-list-view.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsresultstablejs">trunk/Websites/perf.webkit.org/public/v3/components/results-table.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstestgroupformjs">trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstestgroupresultstablejs">trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsanalysisresultsjs">trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs">trunk/Websites/perf.webkit.org/public/v3/models/build-request.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstestgroupjs">trunk/Websites/perf.webkit.org/public/v3/models/test-group.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunitteststestgroupstestsjs">trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsanalysistaskbuglistjs">trunk/Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -1,3 +1,237 @@
</span><ins>+2017-03-28  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Modernize AnalysisTaskPage
+        https://bugs.webkit.org/show_bug.cgi?id=170165
+
+        Reviewed by Antti Koivisto.
+
+        Modernized AnalysisTaskPage and related components. The main refactoring happened in AnalysisTaskPage
+        from which AnalysisTaskResultsPane and AnalysisTaskTestGroupPane have been extracted.
+
+        Decoupled BuildRequest from its results. AnalysisResultsViewer and TestGroupResultsTable now stores
+        a reference to AnalysisResultsView and Metric to find the results for each build request.
+        This refactoring is necessary in order to view results of an arbitrary metric in the future.
+
+        Also refactored ResultsTable and its subclasses extensively. Instead of making its render() to invoke
+        subclass' methods such as buildRowGroups, heading, and additionalHeading, rely on each subclass call
+        to invoke renderTable(), renamed from render(), with callbacks to add extra headers and columns.
+
+        This patch also fixes a number of usability issues found by the user such as changing the test name
+        resets the customized revisions by the virtue of the modern code being naturally more correct.
+
+        * public/v3/components/analysis-results-viewer.js:
+        (AnalysisResultsViewer):
+        (AnalysisResultsViewer.prototype.setTestGroupCallback): Deleted. Replaced by &quot;testGroupClick&quot; action.
+        (AnalysisResultsViewer.prototype.setRangeSelectorLabels): Moved here from ResultsTable since it's
+        never used in ResultsTable or TestGroupResultsTable.
+        (AnalysisResultsViewer.prototype.selectedRange): Ditto.
+        (AnalysisResultsViewer.prototype.setPoints): Now takes metric as the third argument.
+        (AnalysisResultsViewer.prototype.setTestGroups): Now takes the current test group.
+        (AnalysisResultsViewer.prototype.didUpdateResults): Deleted.
+        (AnalysisResultsViewer.prototype.setAnalysisResultsView): Added.
+        (AnalysisResultsViewer.prototype.render): Invoke _renderTestGroups lazily. Also simplified the logic
+        to find the selected list item. Since we always use a shadow DOM now, we can simply look for an element
+        with &quot;.seleted&quot; instead of crafting a unique class name.
+        (AnalysisResultsViewer.prototype.renderTestGroups): Renamed from buildRowGroups. Specify callbacks to
+        insert headers for A/B radio buttons, which has been moved from ResultsTable.prototype.render, and the
+        stacked blocks of testing results.
+        (AnalysisResultsViewer.prototype._classForTestGroup): Deleted.
+        (AnalysisResultsViewer.prototype._openStackingBlock): Deleted.
+        (AnalysisResultsViewer.prototype._expandBetween): Create a new set for expandedPoints to make
+        _renderTestGroupsLazily.evaluate do the work.
+        (AnalysisResultsViewer._layoutBlocks): Moved from TestGroupStackingGrid.layout.
+        (AnalysisResultsViewer._sortBlocksByRow): Moved from AnalysisResultsViewer.TestGroupStackingGrid.
+        (AnalysisResultsViewer._insertAfterBlockWithSameRange): Ditto.
+        (AnalysisResultsViewer._insertBlockInFirstAvailableColumn): Ditto.
+        (AnalysisResultsViewer._createCellsForRow): Ditto.
+
+        (AnalysisResultsViewer.TestGroupStackingBlock):
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype.addRowIndex):
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype.createStackingCell): No longer creates a unique
+        class name here. See the inline comment for AnalysisResultsViewer.prototype.render.
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype.isThin): Deleted. We used to collapse &quot;failed&quot;
+        test groups as a thin vertical line, and we wanted to show them next to each other in _layoutBlock but
+        we don't do that anymore.
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype._valuesForCommitSet): Added. Uses
+        this._analysisResultsView to extract the results for the current metrics.
+        (AnalysisResultsViewer.TestGroupStackingBlock.prototype._computeTestGroupStatus):
+
+        * public/v3/components/analysis-task-bug-list.js: Added.
+        (AnalysisTaskBugList): Added. Extracted from AnalysisTaskChartPane.
+        (AnalysisTaskBugList.prototype.setTask): Added.
+        (AnalysisTaskBugList.prototype.didConstructShadowTree): Added.
+        (AnalysisTaskBugList.prototype.render): Added.
+        (AnalysisTaskBugList.prototype._associateBug): Added.
+        (AnalysisTaskBugList.prototype._dissociateBug): Added.
+        (AnalysisTaskBugList.htmlTemplate): Added.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase.htmlTemplate): Added a hook to insert more content at the end in AnalysisTaskChartPane.
+        (ChartPaneBase.paneFooterTemplate): Added.
+
+        * public/v3/components/customizable-test-group-form.js:
+        (CustomizableTestGroupForm):
+        (CustomizableTestGroupForm.prototype.setCommitSetMap):
+        (CustomizableTestGroupForm.prototype.startTesting): Renamed from _submitted. Now dispatches an action
+        by the name of &quot;startTesting&quot; instead of calling this._startCallback.
+        (CustomizableTestGroupForm.prototype.didConstructShadowTree): Added. Moved the logic to attach event
+        handlers here to avoid eagerly creating the shadow tree in the constructor.
+        (CustomizableTestGroupForm.prototype._computeCommitSetMap): Use the newly added this._revisionEditorMap
+        to find the relevant input element instead of running a querySelector.
+        (CustomizableTestGroupForm.prototype.render): Lazily invoke _renderCustomRevisionTable. This avoids
+        overriding the customized revisions when the user finally types in the test group name.
+        (CustomizableTestGroupForm.prototype._renderCustomRevisionTable): Extracted from render.
+        (CustomizableTestGroupForm.prototype._constructRevisionRadioButtons): Made this a non-static method
+        since it needs to update this._revisionEditorMap now. Merged _constructRevisionRadioButtons.
+        (CustomizableTestGroupForm.prototype._createRadioButton): Deleted. See above.
+        (CustomizableTestGroupForm.cssTemplate):
+        (CustomizableTestGroupForm.formContent): Use IDs instead of classes to make this.content(ID) work.
+
+        * public/v3/components/mutable-list-view.js:
+        (MutableListView.prototype.setList):
+        (MutableListView.prototype.setKindList):
+        (MutableListView.prototype.setAddCallback): Deleted. Replaced by &quot;addItem&quot; action.
+        (MutableListView.prototype.render):
+        (MutableListItem.prototype.content):
+
+        * public/v3/components/results-table.js:
+        (ResultsTable): Removed this._rangeSelectorLabels, this._rangeSelectorCallback, and this._selectedRange
+        as they are only used by AnalysisResultsViewer. Also replaced this._valueFormatter by
+        this._analysisResultsView which knows a metric.
+        (ResultsTable.prototype.setValueFormatter): Deleted.
+        (ResultsTable.prototype.setRangeSelectorLabels): Deleted.
+        (ResultsTable.prototype.setRangeSelectorCallback): Deleted.
+        (ResultsTable.prototype.selectedRange): Deleted.
+        (ResultsTable.prototype._rangeSelectorClicked): Deleted.
+        (ResultsTable.prototype.setAnalysisResultsView): Added.
+        (ResultsTable.prototype.renderTable): Added. Removed the logic to add _rangeSelectorLabels since it has
+        been moved to AnalysisResultsViewer.prototype.render inside buildColumns, which also inserts additional
+        columns which used to be stored on each ResultsTableRow. Use the same technique to insert additional
+        headers. Also take the name (thead tr th) of row header (tbody tr td) as an argument and automatically
+        create a table cell of an appropriate colspan.
+        (ResultsTable.prototype._createRevisionListCells):
+        (ResultsTable.prototype.heading): Deleted. Superseded by buildHeaders callback.
+        (ResultsTable.prototype.additionalHeading): Ditto.
+        (ResultsTable.prototype.buildRowGroups): Deleted. It is now the responsibility of each subclass to call
+        ResultsTable's renderTable() in the subclass' render() function.
+        (ResultsTable.prototype._computeRepositoryList): No longer takes extraRepositories as an argument.
+        Instead, this function now returns a pair of the repository list and the list of constant commits.
+        (ResultsTable.htmlTemplate):
+        (ResultsTable.cssTemplate):
+
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm): Avoid eagerly creating the shadow tree. Also removed the removed the dead code.
+        (TestGroupForm.prototype.setRepetitionCount): Simply override the value of the select element.
+        (TestGroupForm.prototype.didConstructShadowTree): Added. Attach event handlers here to avoid eagerly
+        creating the shadow tree in the constructor.
+        (TestGroupForm.prototype.startTesting): Renamed from _submitted. Dispatch &quot;startTesting&quot; action instead
+        of invoking _startCallback which has been removed.
+        (TestGroupForm.htmlTemplate):
+        (TestGroupForm.formContent):
+
+        * public/v3/components/test-group-results-table.js:
+        (TestGroupResultsTable):
+        (TestGroupResultsTable.prototype.didUpdateResults): Deleted. No longer neeed per setAnalysisResultsView
+        in ResultsTable.
+        (TestGroupResultsTable.prototype.setTestGroup):
+        (TestGroupResultsTable.prototype.heading): Deleted.
+        (TestGroupResultsTable.prototype.render):
+        (TestGroupResultsTable.prototype._renderTestGroup): Extracted from render.
+        (TestGroupResultsTable.prototype._buildRowGroups): Renamed from buildRowGroups.
+        (TestGroupResultsTable.prototype._buildRowGroupForCommitSet): Extracted from buildRowGroups.
+        (TestGroupResultsTable.prototype._buildComparisonRow): Extracted from buildRowGroups.buildRowGroups
+
+        * public/v3/index.html: Include analysis-task-bug-list.js.
+
+        * public/v3/models/analysis-results.js:
+        (AnalysisResults): Inverted the map so that we can easily create a view based on metric.
+        (AnalysisResults.prototype.find): Ditto.
+        (AnalysisResults.prototype.add): Ditto.
+        (AnalysisResults.prototype.viewForMetric): Added.
+        (AnalysisResults.fetch):
+        (AnalysisResultsView): Added.
+        (AnalysisResultsView.prototype.metric): Added.
+        (AnalysisResultsView.prototype.resultForBuildId): Added.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest.result): Deleted.
+        (BuildRequest.setResult): Deleted.
+
+        * public/v3/models/test-group.js:
+        (TestGroup): Removed this._allCommitSets since it was never used.
+        (TestGroup.prototype.didSetResult): Deleted since it was never used.
+        (TestGroup.prototype.compareTestResults): Now takes an array of measurement set values.
+        (TestGroup.prototype._valuesForCommitSet): Deleted.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane): This class now includes the form to cutomize the revisions.
+        (AnalysisTaskChartPane.prototype.setShowForm): Added.
+        (AnalysisTaskChartPane.prototype._mainSelectionDidChange):
+        (AnalysisTaskChartPane.prototype.didConstructShadowTree): Added. Dispatches &quot;newTestGroup&quot; action when
+        the user presses the button to start a new A/B testing from the chart.
+        (AnalysisTaskChartPane.prototype.render): Added.
+        (AnalysisTaskChartPane.prototype.paneFooterTemplate): Added.
+        (AnalysisTaskChartPane.cssTemplate):
+
+        (AnalysisTaskResultsPane): Added. Encapsulates AnalysisResultsViewer and CustomizableTestGroupForm.
+        (AnalysisTaskResultsPane.prototype.setPoints): Added.
+        (AnalysisTaskResultsPane.prototype.setTestGroups): Added.
+        (AnalysisTaskResultsPane.prototype.setAnalysisResultsView): Added.
+        (AnalysisTaskResultsPane.prototype.setShowForm): Added.
+        (AnalysisTaskResultsPane.prototype.didConstructShadowTree): Added. Dispatches &quot;newTestGroup&quot; action
+        when the user presses the button to start a new A/B testing from the chart.
+        (AnalysisTaskResultsPane.prototype.render): Added.
+        (AnalysisTaskResultsPane.htmlTemplate): Added.
+        (AnalysisTaskResultsPane.cssTemplate): Added.
+
+        (AnalysisTaskTestGroupPane): Added. Encapsulates TestGroupResultsTable and CustomizableTestGroupForm.
+        (AnalysisTaskTestGroupPane.prototype.didConstructShadowTree): Added.
+        (AnalysisTaskTestGroupPane.prototype.setTestGroups): Added.
+        (AnalysisTaskTestGroupPane.prototype.setAnalysisResultsView): Added.
+        (AnalysisTaskTestGroupPane.prototype.render): Added.
+        (AnalysisTaskTestGroupPane.prototype._renderTestGroups): Added. Updates the list of test groups. Hide
+        the hidden groups unless showHiddenGroups is set. Updates this._testGroupMap so that the visibility of
+        groups and their names can be updated without having to re-render the entire list.
+        (AnalysisTaskTestGroupPane.prototype._renderTestGroupVisibility): Added.
+        (AnalysisTaskTestGroupPane.prototype._renderTestGroupNames): Added.
+        (AnalysisTaskTestGroupPane.prototype._renderCurrentTestGroup): Added. Update TestGroupResultsTable with
+        the selected test group. Also highlight the list view, and update the hide-unhide toggle button's label
+        as needed.
+        (AnalysisTaskTestGroupPane.htmlTemplate): Added.
+        (AnalysisTaskTestGroupPane.cssTemplate): Added.
+
+        (AnalysisTaskPage): Deleted a massive number of instance variables. They are now manged by newly added
+        AnalysisTaskChartPane, AnalysisTaskResultsPane, and AnalysisTaskTestGroupPane
+        (AnalysisTaskPage.prototype.didConstructShadowTree): Added. Attach various event handlers here to avoid
+        eagerly creating the shadow tree in the constructor.
+        (AnalysisTaskPage.prototype._fetchRelatedInfoForTaskId):
+        (AnalysisTaskPage.prototype._didFetchTask): No longer sets the value formatter to the results viewer
+        and the results table as they now recieve AnalysisResultsView later in _assignTestResultsIfPossible.
+        (AnalysisTaskPage.prototype._didFetchMeasurement): Set the metric to the results viewer.
+        (AnalysisTaskPage.prototype._didUpdateTestGroupHiddenState):
+        (AnalysisTaskPage.prototype._assignTestResultsIfPossible): Create AnalysisResultsView from the newly
+        retrieved AnalysisResults and pass it to AnalysisTaskResultsPane and AnalysisTaskTestGroupPane.
+        (AnalysisTaskPage.prototype.render): Dramatically simplified.
+        (AnalysisTaskPage.prototype._renderTaskNameAndStatus): Extracted from render.
+        (AnalysisTaskPage.prototype._renderRelatedTasks): Ditto.
+        (AnalysisTaskPage.prototype._renderCauseAndFixes): Ditto.
+        (AnalysisTaskPage.prototype._showTestGroup):
+        (AnalysisTaskPage.prototype._updateTaskName): Now takes the new name as an argument.
+        (AnalysisTaskPage.prototype._updateTestGroupName): Now takes the new name as the second argument.
+        (AnalysisTaskPage.prototype._hideCurrentTestGroup): Now takes the test group to hide.
+        (AnalysisTaskPage.prototype._associateCommit): Moved to AnalysisTaskBugList.
+        (AnalysisTaskPage.prototype._dissociateCommit): Ditto.
+        (AnalysisTaskPage.prototype._retryCurrentTestGroup): Now takes the test group as the first argument.
+        (AnalysisTaskPage.prototype._chartSelectionDidChange): Deleted.
+        (AnalysisTaskPage.prototype._createNewTestGroupFromChart): Deleted.
+        (AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): Deleted.
+        (AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Deleted.
+        (AnalysisTaskPage.htmlTemplate):
+        (AnalysisTaskPage.cssTemplate):
+
+        * unit-tests/test-groups-tests.js: Updated a test case which was expecting BuildReqeust's result, which
+        has been removed, to exist.
+
</ins><span class="cx"> 2017-03-23  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Share more code between ManifestGenerator and /api/triggerables
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsanalysisresultsviewerjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -5,107 +5,88 @@
</span><span class="cx">         super('analysis-results-viewer');
</span><span class="cx">         this._startPoint = null;
</span><span class="cx">         this._endPoint = null;
</span><ins>+        this._metric = null;
</ins><span class="cx">         this._testGroups = null;
</span><span class="cx">         this._currentTestGroup = null;
</span><del>-        this._renderedCurrentTestGroup = null;
-        this._shouldRenderTable = true;
-        this._additionalHeading = null;
-        this._testGroupCallback = null;
</del><ins>+        this._rangeSelectorLabels = [];
+        this._selectedRange = {};
</ins><span class="cx">         this._expandedPoints = new Set;
</span><ins>+        this._groupToCellMap = new Map;
+
+        this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this.renderTestGroups.bind(this));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    setTestGroupCallback(callback) { this._testGroupCallback = callback; }
</del><ins>+    setRangeSelectorLabels(labels) { this._rangeSelectorLabels = labels; }
+    selectedRange() { return this._selectedRange; }
</ins><span class="cx"> 
</span><del>-    setCurrentTestGroup(testGroup)
</del><ins>+    setPoints(startPoint, endPoint, metric)
</ins><span class="cx">     {
</span><del>-        this._currentTestGroup = testGroup;
-    }
-
-    setPoints(startPoint, endPoint)
-    {
</del><ins>+        this._metric = metric;
</ins><span class="cx">         this._startPoint = startPoint;
</span><span class="cx">         this._endPoint = endPoint;
</span><del>-        this._shouldRenderTable = true;
-        this._expandedPoints.clear();
</del><ins>+        this._expandedPoints = new Set;
</ins><span class="cx">         this._expandedPoints.add(startPoint);
</span><span class="cx">         this._expandedPoints.add(endPoint);
</span><ins>+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    setTestGroups(testGroups)
</del><ins>+    setTestGroups(testGroups, currentTestGroup)
</ins><span class="cx">     {
</span><span class="cx">         this._testGroups = testGroups;
</span><del>-        this._shouldRenderTable = true;
</del><ins>+        this._currentTestGroup = currentTestGroup;
+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    didUpdateResults()
</del><ins>+    setAnalysisResultsView(analysisResultsView)
</ins><span class="cx">     {
</span><del>-        this._shouldRenderTable = true;
</del><ins>+        console.assert(analysisResultsView instanceof AnalysisResultsView);
+        this._analysisResultsView = analysisResultsView;
+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     render()
</span><span class="cx">     {
</span><del>-        if (!this._valueFormatter || !this._startPoint)
-            return;
-
</del><ins>+        super.render();
</ins><span class="cx">         Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'render');
</span><span class="cx"> 
</span><del>-        if (this._shouldRenderTable) {
-            this._shouldRenderTable = false;
-            this._renderedCurrentTestGroup = null;
</del><ins>+        this._renderTestGroupsLazily.evaluate(this._testGroups,
+            this._startPoint, this._endPoint, this._metric, this._analysisResultsView, this._expandedPoints);
</ins><span class="cx"> 
</span><del>-            Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'renderTable');
-            super.render();
-            Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'renderTable');
-        }
</del><ins>+        const selectedCell = this.content().querySelector('td.selected');
+        if (selectedCell)
+            selectedCell.classList.remove('selected');
+        if (this._groupToCellMap &amp;&amp; this._currentTestGroup)
+            this._groupToCellMap.get(this._currentTestGroup).classList.add('selected');
</ins><span class="cx"> 
</span><del>-        if (this._currentTestGroup != this._renderedCurrentTestGroup) {
-            if (this._renderedCurrentTestGroup) {
-                var className = this._classForTestGroup(this._renderedCurrentTestGroup);
-                var element = this.content().querySelector('.' + className);
-                if (element)
-                    element.classList.remove('selected');
-            }
-            if (this._currentTestGroup) {
-                var className = this._classForTestGroup(this._currentTestGroup);
-                var element = this.content().querySelector('.' + className);
-                if (element)
-                    element.classList.add('selected');
-            }
-            this._renderedCurrentTestGroup = this._currentTestGroup;
-        }
-
</del><span class="cx">         Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'render');
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    heading() { return [ComponentBase.createElement('th', 'Point')]; }
-    additionalHeading() { return this._additionalHeading; }
-
-    buildRowGroups()
</del><ins>+    renderTestGroups(testGroups, startPoint, endPoint, metric, analysisResults, expandedPoints)
</ins><span class="cx">     {
</span><del>-        Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'buildRowGroups');
</del><ins>+        if (!testGroups || !startPoint || !endPoint || !metric || !analysisResults)
+            return false;
</ins><span class="cx"> 
</span><del>-        var testGroups = this._testGroups || [];
-        var commitSetsInTestGroups = this._collectCommitSetsInTestGroups(testGroups);
</del><ins>+        Instrumentation.startMeasuringTime('AnalysisResultsViewer', 'renderTestGroups');
</ins><span class="cx"> 
</span><del>-        var rowToMatchingCommitSets = new Map;
-        var rowList = this._buildRowsForPointsAndTestGroups(commitSetsInTestGroups, rowToMatchingCommitSets);
</del><ins>+        const commitSetsInTestGroups = this._collectCommitSetsInTestGroups(testGroups);
+        const rowToMatchingCommitSets = new Map;
+        const rows = this._buildRowsForPointsAndTestGroups(commitSetsInTestGroups, rowToMatchingCommitSets);
</ins><span class="cx"> 
</span><del>-        var testGroupLayoutMap = new Map;
-        var self = this;
-        rowList.forEach(function (row, rowIndex) {
-            var matchingCommitSets = rowToMatchingCommitSets.get(row);
</del><ins>+        const testGroupLayoutMap = new Map;
+        rows.forEach((row, rowIndex) =&gt; {
+            const matchingCommitSets = rowToMatchingCommitSets.get(row);
</ins><span class="cx">             if (!matchingCommitSets) {
</span><span class="cx">                 console.assert(row instanceof AnalysisResultsViewer.ExpandableRow);
</span><span class="cx">                 return;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            for (var entry of matchingCommitSets) {
-                var testGroup = entry.testGroup();
</del><ins>+            for (let entry of matchingCommitSets) {
+                const testGroup = entry.testGroup();
</ins><span class="cx"> 
</span><del>-                var block = testGroupLayoutMap.get(testGroup);
</del><ins>+                let block = testGroupLayoutMap.get(testGroup);
</ins><span class="cx">                 if (!block) {
</span><del>-                    block = new AnalysisResultsViewer.TestGroupStackingBlock(
-                        testGroup, self._classForTestGroup(testGroup), self._openStackingBlock.bind(self, testGroup));
</del><ins>+                    block = new AnalysisResultsViewer.TestGroupStackingBlock(testGroup, this._analysisResultsView,
+                        this._groupToCellMap, () =&gt; this.dispatchAction('testGroupClick', testGroup));
</ins><span class="cx">                     testGroupLayoutMap.set(testGroup, block);
</span><span class="cx">                 }
</span><span class="cx">                 block.addRowIndex(entry, rowIndex);
</span><span class="lines">@@ -112,22 +93,37 @@
</span><span class="cx">             }
</span><span class="cx">         });
</span><span class="cx"> 
</span><del>-        var grid = new AnalysisResultsViewer.TestGroupStackingGrid(rowList.length);
-        for (var testGroup of testGroups) {
-            var block = testGroupLayoutMap.get(testGroup);
-            if (block)
-                grid.insertBlockToColumn(block);
</del><ins>+        const [additionalColumnsByRow, columnCount] = AnalysisResultsViewer._layoutBlocks(rows.length, testGroups.map((group) =&gt; testGroupLayoutMap.get(group)));
+
+        const element = ComponentBase.createElement;
+        const buildHeaders = (headers) =&gt; {
+            return [
+                this._rangeSelectorLabels.map((label) =&gt; element('th', label)),
+                headers,
+                columnCount ? element('td', {colspan: columnCount + 1, class: 'stacking-block'}) : [],
+            ]
+        };
+        const buildColumns = (columns, row, rowIndex) =&gt; {
+            return [
+                this._rangeSelectorLabels.map((label) =&gt; {
+                    if (!row.commitSet())
+                        return element('td', '');
+                    const checked = this._selectedRange[label] == row.commitSet();
+                    const onchange = () =&gt; {
+                        this._selectedRange[label] = row.commitSet();
+                        this.dispatchAction('rangeSelectorClick', label, row);
+                    };
+                    return element('td', element('input', {type: 'radio', name: label, checked, onchange}));
+                }),
+                columns,
+                additionalColumnsByRow[rowIndex],
+            ];
</ins><span class="cx">         }
</span><ins>+        this.renderTable(metric.makeFormatter(4), [{rows}], 'Point', buildHeaders, buildColumns);
</ins><span class="cx"> 
</span><del>-        grid.layout();
-        for (var rowIndex = 0; rowIndex &lt; rowList.length; rowIndex++)
-            rowList[rowIndex].setAdditionalColumns(grid.createCellsForRow(rowIndex));
</del><ins>+        Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'renderTestGroups');
</ins><span class="cx"> 
</span><del>-        this._additionalHeading = grid._columns ? ComponentBase.createElement('td', {colspan: grid._columns.length + 1, class: 'stacking-block'}) : [];
-
-        Instrumentation.endMeasuringTime('AnalysisResultsViewer', 'buildRowGroups');
-
-        return [{rows: rowList}];
</del><ins>+        return true;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _collectCommitSetsInTestGroups(testGroups)
</span><span class="lines">@@ -235,17 +231,6 @@
</span><span class="cx">         return rowList;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _classForTestGroup(testGroup)
-    {
-        return 'stacked-test-group-' + testGroup.id();
-    }
-
-    _openStackingBlock(testGroup)
-    {
-        if (this._testGroupCallback)
-            this._testGroupCallback(testGroup);
-    }
-    
</del><span class="cx">     _expandBetween(pointBeforeExpansion, pointAfterExpansion)
</span><span class="cx">     {
</span><span class="cx">         console.assert(pointBeforeExpansion.series == pointAfterExpansion.series);
</span><span class="lines">@@ -257,12 +242,99 @@
</span><span class="cx">         var increment = Math.ceil((indexAfterEnd - indexBeforeStart) / 5);
</span><span class="cx">         if (increment &lt; 3)
</span><span class="cx">             increment = 1;
</span><ins>+
+        const expandedPoints = new Set([...this._expandedPoints]);
</ins><span class="cx">         for (var i = indexBeforeStart + 1; i &lt; indexAfterEnd; i += increment)
</span><del>-            this._expandedPoints.add(series.findPointByIndex(i));
-        this._shouldRenderTable = true;
</del><ins>+            expandedPoints.add(series.findPointByIndex(i));
+        this._expandedPoints = expandedPoints;
+
</ins><span class="cx">         this.enqueueToRender();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static _layoutBlocks(rowCount, blocks)
+    {
+        const sortedBlocks = this._sortBlocksByRow(blocks);
+
+        const columns = [];
+        for (const block of sortedBlocks)
+            this._insertBlockInFirstAvailableColumn(columns, block);
+
+        const rows = new Array(rowCount);
+        for (let i = 0; i &lt; rowCount; i++)
+            rows[i] = this._createCellsForRow(columns, i);
+
+        return [rows, columns.length];
+    }
+
+    static _sortBlocksByRow(blocks)
+    {
+        for (let i = 0; i &lt; blocks.length; i++)
+            blocks[i].index = i;
+
+        return blocks.slice(0).sort((block1, block2) =&gt; {
+            const startRowDiff = block1.startRowIndex() - block2.startRowIndex();
+            if (startRowDiff)
+                return startRowDiff;
+
+            // Order backwards for end rows in order to place test groups with a larger range at the beginning.
+            const endRowDiff = block2.endRowIndex() - block1.endRowIndex();
+            if (endRowDiff)
+                return endRowDiff;
+
+            return block1.index - block2.index;
+        });
+    }
+
+    static _insertBlockInFirstAvailableColumn(columns, newBlock)
+    {
+        for (const existingColumn of columns) {
+            for (let i = 0; i &lt; existingColumn.length; i++) {
+                const currentBlock = existingColumn[i];
+                if ((!i || existingColumn[i - 1].endRowIndex() &lt; newBlock.startRowIndex())
+                    &amp;&amp; newBlock.endRowIndex() &lt; currentBlock.startRowIndex()) {
+                    existingColumn.splice(i, 0, newBlock);
+                    return;
+                }
+            }
+            const lastBlock = existingColumn[existingColumn.length - 1];
+            console.assert(lastBlock);
+            if (lastBlock.endRowIndex() &lt; newBlock.startRowIndex()) {
+                existingColumn.push(newBlock);
+                return;
+            }
+        }
+        columns.push([newBlock]);
+    }
+
+    static _createCellsForRow(columns, rowIndex)
+    {
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+
+        const crateEmptyCell = (rowspan) =&gt; element('td', {rowspan: rowspan, class: 'stacking-block'}, '');
+
+        const cells = [element('td', {class: 'stacking-block'}, '')];
+        for (const blocksInColumn of columns) {
+            if (!rowIndex &amp;&amp; blocksInColumn[0].startRowIndex()) {
+                cells.push(crateEmptyCell(blocksInColumn[0].startRowIndex()));
+                continue;
+            }
+            for (let i = 0; i &lt; blocksInColumn.length; i++) {
+                const block = blocksInColumn[i];
+                if (block.startRowIndex() == rowIndex) {
+                    cells.push(block.createStackingCell());
+                    break;
+                }
+                const rowCount = i + 1 &lt; blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
+                const remainingRows = rowCount - block.endRowIndex() - 1;
+                if (rowIndex == block.endRowIndex() + 1 &amp;&amp; rowIndex &lt; rowCount)
+                    cells.push(crateEmptyCell(remainingRows));
+            }
+        }
+
+        return cells;
+    }
+
</ins><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `&lt;section class=&quot;analysis-view&quot;&gt;${ResultsTable.htmlTemplate()}&lt;/section&gt;`;
</span><span class="lines">@@ -371,14 +443,12 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> AnalysisResultsViewer.TestGroupStackingBlock = class {
</span><del>-    constructor(testGroup, className, callback)
</del><ins>+    constructor(testGroup, analysisResultsView, groupToCellMap, callback)
</ins><span class="cx">     {
</span><span class="cx">         this._testGroup = testGroup;
</span><ins>+        this._analysisResultsView = analysisResultsView;
</ins><span class="cx">         this._commitSetIndexRowIndexMap = [];
</span><del>-        this._className = className;
-        this._label = null;
-        this._title = null;
-        this._status = null;
</del><ins>+        this._groupToCellMap = groupToCellMap;
</ins><span class="cx">         this._callback = callback;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -385,7 +455,7 @@
</span><span class="cx">     addRowIndex(commitSetInTestGroup, rowIndex)
</span><span class="cx">     {
</span><span class="cx">         console.assert(commitSetInTestGroup instanceof AnalysisResultsViewer.CommitSetInTestGroup);
</span><del>-        this._commitSetIndexRowIndexMap.push({commitSet: commitSetInTestGroup.commitSet(), rowIndex: rowIndex});
</del><ins>+        this._commitSetIndexRowIndexMap.push({commitSet: commitSetInTestGroup.commitSet(), rowIndex});
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     testGroup() { return this._testGroup; }
</span><span class="lines">@@ -392,14 +462,18 @@
</span><span class="cx"> 
</span><span class="cx">     createStackingCell()
</span><span class="cx">     {
</span><del>-        this._computeTestGroupStatus();
</del><ins>+        const {label, title, status} = this._computeTestGroupStatus();
</ins><span class="cx"> 
</span><del>-        return ComponentBase.createElement('td', {
</del><ins>+        const cell = ComponentBase.createElement('td', {
</ins><span class="cx">             rowspan: this.endRowIndex() - this.startRowIndex() + 1,
</span><del>-            title: this._title,
-            class: 'stacking-block ' + this._className + ' ' + this._status,
</del><ins>+            title,
+            class: 'stacking-block ' + status,
</ins><span class="cx">             onclick: this._callback,
</span><del>-        }, ComponentBase.createLink(this._label, this._title, this._callback));
</del><ins>+        }, ComponentBase.createLink(label, title, this._callback));
+
+        this._groupToCellMap.set(this._testGroup, cell);
+
+        return cell;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     isComplete() { return this._commitSetIndexRowIndexMap.length &gt;= 2; }
</span><span class="lines">@@ -406,113 +480,22 @@
</span><span class="cx"> 
</span><span class="cx">     startRowIndex() { return this._commitSetIndexRowIndexMap[0].rowIndex; }
</span><span class="cx">     endRowIndex() { return this._commitSetIndexRowIndexMap[this._commitSetIndexRowIndexMap.length - 1].rowIndex; }
</span><del>-    isThin()
</del><ins>+
+    _valuesForCommitSet(testGroup, commitSet)
</ins><span class="cx">     {
</span><del>-        this._computeTestGroupStatus();
-        return this._status == 'failed';
</del><ins>+        return testGroup.requestsForCommitSet(commitSet).map((request) =&gt; {
+            return this._analysisResultsView.resultForBuildId(request.buildId());
+        }).filter((result) =&gt; !!result).map((result) =&gt; result.value);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _computeTestGroupStatus()
</span><span class="cx">     {
</span><del>-        if (this._status || !this.isComplete())
-            return;
-
</del><ins>+        if (!this.isComplete())
+            return {label: null, title: null, status: null};
</ins><span class="cx">         console.assert(this._commitSetIndexRowIndexMap.length &lt;= 2); // FIXME: Support having more root sets.
</span><del>-
-        var result = this._testGroup.compareTestResults(
-            this._commitSetIndexRowIndexMap[0].commitSet, this._commitSetIndexRowIndexMap[1].commitSet);
-
-        this._label = result.label;
-        this._title = result.fullLabel;
-        this._status = result.status;
</del><ins>+        const startValues = this._valuesForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[0].commitSet);
+        const endValues = this._valuesForCommitSet(this._testGroup, this._commitSetIndexRowIndexMap[1].commitSet);
+        const result = this._testGroup.compareTestResults(startValues, endValues);
+        return {label: result.label, title: result.fullLabel, status: result.status};
</ins><span class="cx">     }
</span><span class="cx"> }
</span><del>-
-AnalysisResultsViewer.TestGroupStackingGrid = class {
-    constructor(rowCount)
-    {
-        this._blocks = [];
-        this._columns = null;
-        this._rowCount = rowCount;
-    }
-
-    insertBlockToColumn(newBlock)
-    {
-        console.assert(newBlock instanceof AnalysisResultsViewer.TestGroupStackingBlock);
-        for (var i = this._blocks.length - 1; i &gt;= 0; i--) {
-            var currentBlock = this._blocks[i];
-            if (currentBlock.startRowIndex() == newBlock.startRowIndex()
-                &amp;&amp; currentBlock.endRowIndex() == newBlock.endRowIndex()) {
-                this._blocks.splice(i + 1, 0, newBlock);
-                return;
-            }
-        }
-        this._blocks.push(newBlock);
-    }
-
-    layout()
-    {
-        this._columns = [];
-        for (var block of this._blocks)
-            this._layoutBlock(block);
-    }
-
-    _layoutBlock(newBlock)
-    {
-        for (var columnIndex = 0; columnIndex &lt; this._columns.length; columnIndex++) {
-            var existingColumn = this._columns[columnIndex];
-            if (newBlock.isThin() != existingColumn[0].isThin())
-                continue;
-
-            for (var i = 0; i &lt; existingColumn.length; i++) {
-                var currentBlock = existingColumn[i];
-                if ((!i || existingColumn[i - 1].endRowIndex() &lt; newBlock.startRowIndex())
-                    &amp;&amp; newBlock.endRowIndex() &lt; currentBlock.startRowIndex()) {
-                    existingColumn.splice(i, 0, newBlock);
-                    return;
-                }
-            }
-
-            var lastBlock = existingColumn[existingColumn.length - 1];
-            if (lastBlock.endRowIndex() &lt; newBlock.startRowIndex()) {
-                existingColumn.push(newBlock);
-                return;
-            }
-        }
-        this._columns.push([newBlock]);
-    }
-
-    createCellsForRow(rowIndex)
-    {
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-
-        var cells = [element('td', {class: 'stacking-block'}, '')];
-        for (var columnIndex = 0; columnIndex &lt; this._columns.length; columnIndex++) {
-            var blocksInColumn = this._columns[columnIndex];
-            if (!rowIndex &amp;&amp; blocksInColumn[0].startRowIndex()) {
-                cells.push(this._createEmptyStackingCell(blocksInColumn[0].startRowIndex()));
-                continue;
-            }
-            for (var i = 0; i &lt; blocksInColumn.length; i++) {
-                var block = blocksInColumn[i];
-                if (block.startRowIndex() == rowIndex) {
-                    cells.push(block.createStackingCell());
-                    break;
-                }
-                var rowCount = i + 1 &lt; blocksInColumn.length ? blocksInColumn[i + 1].startRowIndex() : this._rowCount;
-                var remainingRows = rowCount - block.endRowIndex() - 1;
-                if (rowIndex == block.endRowIndex() + 1 &amp;&amp; rowIndex &lt; rowCount)
-                    cells.push(this._createEmptyStackingCell(remainingRows));
-            }
-        }
-
-        return cells;
-    }
-
-    _createEmptyStackingCell(rowspan, content)
-    {
-        return ComponentBase.createElement('td', {rowspan: rowspan, class: 'stacking-block'}, '');
-    }
-
-}
</del></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsanalysistaskbuglistjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js (0 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/analysis-task-bug-list.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -0,0 +1,55 @@
</span><ins>+
+class AnalysisTaskBugList extends ComponentBase {
+
+    constructor()
+    {
+        super('analysis-task-bug-list');
+        this._task = null;
+    }
+
+    setTask(task)
+    {
+        console.assert(task == null || task instanceof AnalysisTask);
+        this._task = task;
+        this.enqueueToRender();
+    }
+
+    didConstructShadowTree()
+    {
+        this.part('bug-list').setKindList(BugTracker.all());
+        this.part('bug-list').listenToAction('addItem', (tracker, bugNumber) =&gt; this._associateBug(tracker, bugNumber));
+    }
+
+    render()
+    {
+        const bugList = this._task ? this._task.bugs().map((bug) =&gt; {
+            return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
+                'Dissociate this bug', () =&gt; this._dissociateBug(bug));
+        }) : [];
+        this.part('bug-list').setList(bugList);
+    }
+
+    _associateBug(tracker, bugNumber)
+    {
+        console.assert(tracker instanceof BugTracker);
+        bugNumber = parseInt(bugNumber);
+
+        return this._task.associateBug(tracker, bugNumber).then(() =&gt; this.enqueueToRender(), (error) =&gt; {
+            this.enqueueToRender();
+            alert('Failed to associate the bug: ' + error);
+        });
+    }
+
+    _dissociateBug(bug)
+    {
+        return this._task.dissociateBug(bug).then(() =&gt; this.enqueueToRender(), (error) =&gt; {
+            this.enqueueToRender();
+            alert('Failed to dissociate the bug: ' + error);
+        });
+    }
+
+    static htmlTemplate() { return `&lt;mutable-list-view id=&quot;bug-list&quot;&gt;&lt;/mutable-list-view&gt;`; }
+
+}
+
+ComponentBase.defineElement('analysis-task-bug-list', AnalysisTaskBugList);
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentschartpanebasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/chart-pane-base.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -314,10 +314,12 @@
</span><span class="cx">                     &lt;/div&gt;
</span><span class="cx">                 &lt;/div&gt;
</span><span class="cx">             &lt;/section&gt;
</span><ins>+            ${this.paneFooterTemplate()}
</ins><span class="cx">         `;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static paneHeaderTemplate() { return ''; }
</span><ins>+    static paneFooterTemplate() { return ''; }
</ins><span class="cx"> 
</span><span class="cx">     static cssTemplate()
</span><span class="cx">     {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentscustomizabletestgroupformjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -5,48 +5,55 @@
</span><span class="cx">     {
</span><span class="cx">         super('customizable-test-group-form');
</span><span class="cx">         this._commitSetMap = null;
</span><del>-        this._renderedRepositorylist = null;
-        this._customized = false;
-        this._nameControl = this.content().querySelector('.name');
-        this._nameControl.oninput = () =&gt; { this.enqueueToRender(); }
-        this.content().querySelector('a').onclick = this._customize.bind(this);
</del><ins>+        this._name = null;
+        this._isCustomized = false;
+        this._revisionEditorMap = {};
+
+        this._renderCustomRevisionTableLazily = new LazilyEvaluatedFunction(this._renderCustomRevisionTable.bind(this));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     setCommitSetMap(map)
</span><span class="cx">     {
</span><span class="cx">         this._commitSetMap = map;
</span><del>-        this._customized = false;
</del><ins>+        this._isCustomized = false;
+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _submitted()
</del><ins>+    startTesting()
</ins><span class="cx">     {
</span><del>-        if (this._startCallback)
-            this._startCallback(this._nameControl.value, this._repetitionCount, this._computeCommitSetMap());
</del><ins>+        this.dispatchAction('startTesting', this._repetitionCount, this._name, this._computeCommitSetMap());
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _customize(event)
</del><ins>+    didConstructShadowTree()
</ins><span class="cx">     {
</span><del>-        event.preventDefault();
-        this._customized = true;
-        this.enqueueToRender();
</del><ins>+        super.didConstructShadowTree();
+
+        const nameControl = this.content('name');
+        nameControl.oninput = () =&gt; {
+            this._name = nameControl.value;
+            this.enqueueToRender();
+        }
+
+        this.content('customize-link').onclick = this.createEventHandler(() =&gt; {
+            this._isCustomized = true;
+            this.enqueueToRender();
+        });
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _computeCommitSetMap()
</span><span class="cx">     {
</span><span class="cx">         console.assert(this._commitSetMap);
</span><del>-        if (!this._customized)
</del><ins>+        if (!this._isCustomized)
</ins><span class="cx">             return this._commitSetMap;
</span><span class="cx"> 
</span><del>-        console.assert(this._renderedRepositorylist);
-        var map = {};
-        for (var label in this._commitSetMap) {
-            var customCommitSet = new CustomCommitSet;
-            for (var repository of this._renderedRepositorylist) {
-                var className = CustomizableTestGroupForm._classForLabelAndRepository(label, repository);
-                var revision = this.content().querySelector('.' + className).value;
-                console.assert(revision);
-                if (revision)
-                    customCommitSet.setRevisionForRepository(repository, revision);
</del><ins>+        const map = {};
+        for (const label in this._commitSetMap) {
+            const originalCommitSet = this._commitSetMap;
+            const customCommitSet = new CustomCommitSet;
+            for (let repository of this._commitSetMap[label].repositories()) {
+                const revisionEditor = this._revisionEditorMap[label].get(repository);
+                console.assert(revisionEditor);
+                customCommitSet.setRevisionForRepository(repository, revisionEditor.value);
</ins><span class="cx">             }
</span><span class="cx">             map[label] = customCommitSet;
</span><span class="cx">         }
</span><span class="lines">@@ -56,58 +63,61 @@
</span><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         super.render();
</span><del>-        var map = this._commitSetMap;
</del><span class="cx"> 
</span><del>-        this.content().querySelector('button').disabled = !(map &amp;&amp; this._nameControl.value);
-        this.content().querySelector('.customize-link').style.display = !map ? 'none' : null;
</del><ins>+        this.content('start-button').disabled = !(this._commitSetMap &amp;&amp; this._name);
+        this.content('customize-link-container').style.display = !this._commitSetMap ? 'none' : null;
</ins><span class="cx"> 
</span><del>-        if (!this._customized) {
-            this.renderReplace(this.content().querySelector('.custom-table-container'), []);
-            return;
</del><ins>+        this._renderCustomRevisionTableLazily.evaluate(this._commitSetMap, this._isCustomized);
+    }
+
+    _renderCustomRevisionTable(commitSetMap, isCustomized)
+    {
+        if (!commitSetMap || !isCustomized) {
+            this.renderReplace(this.content('custom-table'), []);
+            return null;
</ins><span class="cx">         }
</span><del>-        console.assert(map);
</del><span class="cx"> 
</span><del>-        var repositorySet = new Set;
-        var commitSetLabels = [];
-        for (var label in map) {
-            for (var repository of map[label].repositories())
</del><ins>+        const repositorySet = new Set;
+        const commitSetLabels = [];
+        this._revisionEditorMap = {};
+        for (const label in commitSetMap) {
+            for (const repository of commitSetMap[label].repositories())
</ins><span class="cx">                 repositorySet.add(repository);
</span><span class="cx">             commitSetLabels.push(label);
</span><ins>+            this._revisionEditorMap[label] = new Map;
</ins><span class="cx">         }
</span><span class="cx"> 
</span><del>-        this._renderedRepositorylist = Repository.sortByNamePreferringOnesWithURL(Array.from(repositorySet.values()));
</del><ins>+        const repositoryList = Repository.sortByNamePreferringOnesWithURL(Array.from(repositorySet.values()));
+        const element = ComponentBase.createElement;
+        this.renderReplace(this.content('custom-table'), [
+            element('thead',
+                element('tr',
+                    [element('td', 'Repository'), commitSetLabels.map((label) =&gt; element('td', {colspan: commitSetLabels.length + 1}, label))])),
+            element('tbody',
+                repositoryList.map((repository) =&gt; {
+                    const cells = [element('th', repository.label())];
+                    for (const label in commitSetMap)
+                        cells.push(this._constructRevisionRadioButtons(commitSetMap, repository, label));
+                    return element('tr', cells);
+                }))]);
</ins><span class="cx"> 
</span><del>-        var element = ComponentBase.createElement;
-        this.renderReplace(this.content().querySelector('.custom-table-container'),
-            element('table', {class: 'custom-table'}, [
-                element('thead',
-                    element('tr',
-                        [element('td', 'Repository'), commitSetLabels.map(function (label) {
-                            return element('td', {colspan: commitSetLabels.length + 1}, label);
-                        })])),
-                element('tbody',
-                    this._renderedRepositorylist.map(function (repository) {
-                        var cells = [element('th', repository.label())];
-                        for (var label in map)
-                            cells.push(CustomizableTestGroupForm._constructRevisionRadioButtons(map, repository, label));
-                        return element('tr', cells);
-                    }))]));
</del><ins>+        return repositoryList;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static _classForLabelAndRepository(label, repository) { return label + '-' + repository.id(); }
-
-    static _constructRevisionRadioButtons(commitSetMap, repository, rowLabel)
</del><ins>+    _constructRevisionRadioButtons(commitSetMap, repository, rowLabel)
</ins><span class="cx">     {
</span><del>-        var className = this._classForLabelAndRepository(rowLabel, repository);
-        var groupName = className + '-group';
-        var element = ComponentBase.createElement;
-        var revisionEditor = element('input', {class: className});
</del><ins>+        const element = ComponentBase.createElement;
+        const revisionEditor = element('input');
</ins><span class="cx"> 
</span><ins>+        this._revisionEditorMap[rowLabel].set(repository, revisionEditor);
+
</ins><span class="cx">         const nodes = [];
</span><span class="cx">         for (let labelToChoose in commitSetMap) {
</span><span class="cx">             const commit = commitSetMap[labelToChoose].commitForRepository(repository);
</span><span class="cx">             const checked = labelToChoose == rowLabel;
</span><del>-            const radioButton = this._createRadioButton(groupName, revisionEditor, commit, checked);
</del><ins>+            const radioButton = element('input', {type: 'radio', name: `${rowLabel}-${repository.id()}-radio`, checked,
+                onchange: () =&gt; { revisionEditor.value = commit ? commit.revision() : ''; }});
+
</ins><span class="cx">             if (checked)
</span><span class="cx">                 revisionEditor.value = commit ? commit.revision() : '';
</span><span class="cx">             nodes.push(element('td', element('label', [radioButton, labelToChoose])));
</span><span class="lines">@@ -117,36 +127,21 @@
</span><span class="cx">         return nodes;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static _createRadioButton(groupName, revisionEditor, commit, checked)
-    {
-        var button = ComponentBase.createElement('input', {
-            type: 'radio',
-            name: groupName + '-radio',
-            onchange: function () { revisionEditor.value = commit ? commit.revision() : ''; },
-        });
-        if (checked) // FIXME: createElement should be able to set boolean attribute properly.
-            button.checked = true;
-        return button;
-    }
-
</del><span class="cx">     static cssTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><del>-            .customize-link {
</del><ins>+            #customize-link-container,
+            #customize-link {
</ins><span class="cx">                 color: #333;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .customize-link a {
-                color: inherit;
-            }
-
-            .custom-table {
</del><ins>+            #custom-table:not(:empty) {
</ins><span class="cx">                 margin: 1rem 0;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .custom-table,
-            .custom-table td,
-            .custom-table th {
</del><ins>+            #custom-table,
+            #custom-table td,
+            #custom-table th {
</ins><span class="cx">                 font-weight: inherit;
</span><span class="cx">                 border-collapse: collapse;
</span><span class="cx">                 border-top: solid 1px #ddd;
</span><span class="lines">@@ -155,8 +150,8 @@
</span><span class="cx">                 font-size: 0.9rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .custom-table thead td,
-            .custom-table th {
</del><ins>+            #custom-table thead td,
+            #custom-table th {
</ins><span class="cx">                 text-align: center;
</span><span class="cx">             }
</span><span class="cx">             `;
</span><span class="lines">@@ -165,10 +160,10 @@
</span><span class="cx">     static formContent()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><del>-            &lt;input class=&quot;name&quot; type=&quot;text&quot; placeholder=&quot;Test group name&quot;&gt;
</del><ins>+            &lt;input id=&quot;name&quot; type=&quot;text&quot; placeholder=&quot;Test group name&quot;&gt;
</ins><span class="cx">             ${super.formContent()}
</span><del>-            &lt;span class=&quot;customize-link&quot;&gt;(&lt;a href=&quot;&quot;&gt;Customize&lt;/a&gt;)&lt;/span&gt;
-            &lt;div class=&quot;custom-table-container&quot;&gt;&lt;/div&gt;
</del><ins>+            &lt;span id=&quot;customize-link-container&quot;&gt;(&lt;a id=&quot;customize-link&quot; href=&quot;#&quot;&gt;Customize&lt;/a&gt;)&lt;/span&gt;
+            &lt;table id=&quot;custom-table&quot;&gt;&lt;/table&gt;
</ins><span class="cx">         `;
</span><span class="cx">     }
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsmutablelistviewjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/mutable-list-view.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/mutable-list-view.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/mutable-list-view.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -1,5 +1,4 @@
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> class MutableListView extends ComponentBase {
</span><span class="cx"> 
</span><span class="cx">     constructor()
</span><span class="lines">@@ -7,15 +6,22 @@
</span><span class="cx">         super('mutable-list-view');
</span><span class="cx">         this._list = [];
</span><span class="cx">         this._kindList = [];
</span><del>-        this._addCallback = null;
</del><span class="cx">         this._kindMap = new Map;
</span><span class="cx">         this.content().querySelector('form').onsubmit = this._submitted.bind(this);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    setList(list) { this._list = list; }
-    setKindList(list) { this._kindList = list; }
-    setAddCallback(callback) { this._addCallback = callback; }
</del><ins>+    setList(list)
+    {
+        this._list = list;
+        this.enqueueToRender();
+    }
</ins><span class="cx"> 
</span><ins>+    setKindList(list)
+    {
+        this._kindList = list;
+        this.enqueueToRender();
+    }
+
</ins><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         this.renderReplace(this.content().querySelector('.mutable-list'),
</span><span class="lines">@@ -37,8 +43,9 @@
</span><span class="cx">     _submitted(event)
</span><span class="cx">     {
</span><span class="cx">         event.preventDefault();
</span><del>-        if (this._addCallback)
-            this._addCallback(this._kindMap.get(this.content().querySelector('.kind').value), this.content().querySelector('.value').value);
</del><ins>+        const kind = this._kindMap.get(this.content().querySelector('.kind').value);
+        const item = this.content().querySelector('.value').value;
+        this.dispatchAction('addItem', kind, item);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static cssTemplate()
</span><span class="lines">@@ -91,13 +98,15 @@
</span><span class="cx"> 
</span><span class="cx">     content()
</span><span class="cx">     {
</span><del>-        var link = ComponentBase.createLink;
</del><ins>+        const link = ComponentBase.createLink;
+        const closeButton = new CloseButton;
+        closeButton.listenToAction('activate', this._removalLink);
</ins><span class="cx">         return ComponentBase.createElement('li', [
</span><span class="cx">             this._kind.label(),
</span><span class="cx">             ' ',
</span><span class="cx">             link(this._value, this._valueTitle, this._valueLink),
</span><span class="cx">             ' ',
</span><del>-            link(new CloseButton, this._removalTitle, this._removalLink)]);
</del><ins>+            link(closeButton, this._removalTitle, this._removalLink)]);
</ins><span class="cx">     }
</span><span class="cx"> }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsresultstablejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/results-table.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/results-table.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/results-table.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -3,61 +3,33 @@
</span><span class="cx">     {
</span><span class="cx">         super(name);
</span><span class="cx">         this._repositoryList = [];
</span><del>-        this._valueFormatter = null;
-        this._rangeSelectorLabels = null;
-        this._rangeSelectorCallback = null;
-        this._selectedRange = {};
</del><ins>+        this._analysisResultsView = null;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    setValueFormatter(valueFormatter) { this._valueFormatter = valueFormatter; }
-    setRangeSelectorLabels(labels) { this._rangeSelectorLabels = labels; }
-    setRangeSelectorCallback(callback) { this._rangeSelectorCallback = callback; }
-    selectedRange() { return this._selectedRange; }
-
-    _rangeSelectorClicked(label, row)
</del><ins>+    setAnalysisResultsView(analysisResultsView)
</ins><span class="cx">     {
</span><del>-        this._selectedRange[label] = row;
-        if (this._rangeSelectorCallback)
-            this._rangeSelectorCallback();
</del><ins>+        console.assert(analysisResultsView instanceof AnalysisResultsView);
+        this._analysisResultsView = analysisResultsView;
+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    render()
</del><ins>+    renderTable(valueFormatter, rowGroups, headingLabel, buildHeaders = (headers) =&gt; headers, buildColumns = (columns, row, rowIndex) =&gt; columns)
</ins><span class="cx">     {
</span><del>-        if (!this._valueFormatter)
-            return;
</del><ins>+        Instrumentation.startMeasuringTime('ResultsTable', 'renderTable');
</ins><span class="cx"> 
</span><del>-        Instrumentation.startMeasuringTime('ResultsTable', 'render');
</del><ins>+        const [repositoryList, constantCommits] = this._computeRepositoryList(rowGroups);
</ins><span class="cx"> 
</span><del>-        var rowGroups = this.buildRowGroups();
</del><ins>+        const barGraphGroup = new BarGraphGroup(valueFormatter);
+        const element = ComponentBase.createElement;
+        let hasGroupHeading = false;
+        const tableBodies = rowGroups.map((group) =&gt; {
+            const groupHeading = group.heading;
+            const revisionSupressionCount = {};
+            hasGroupHeading = hasGroupHeading || groupHeading;
</ins><span class="cx"> 
</span><del>-        var extraRepositories = [];
-        var repositoryList = this._computeRepositoryList(rowGroups, extraRepositories);
</del><ins>+            return element('tbody', group.rows.map((row, rowIndex) =&gt; {
+                const cells = [];
</ins><span class="cx"> 
</span><del>-        this._selectedRange = {};
-
-        var barGraphGroup = new BarGraphGroup(this._valueFormatter);
-        var element = ComponentBase.createElement;
-        var self = this;
-        var hasGroupHeading = false;
-        var tableBodies = rowGroups.map(function (group) {
-            var groupHeading = group.heading;
-            var revisionSupressionCount = {};
-            hasGroupHeading = !!groupHeading;
-
-            return element('tbody', group.rows.map(function (row, rowIndex) {
-                var cells = [];
-
-                if (self._rangeSelectorLabels) {
-                    for (var label of self._rangeSelectorLabels) {
-                        var content = '';
-                        if (row.commitSet()) {
-                            content = element('input',
-                                {type: 'radio', name: label, onchange: self._rangeSelectorClicked.bind(self, label, row)});
-                        }
-                        cells.push(element('td', content));
-                    }
-                }
-
</del><span class="cx">                 if (groupHeading !== undefined &amp;&amp; !rowIndex)
</span><span class="cx">                     cells.push(element('th', {rowspan: group.rows.length}, groupHeading));
</span><span class="cx">                 cells.push(element('td', row.heading()));
</span><span class="lines">@@ -66,39 +38,38 @@
</span><span class="cx">                     cells.push(element('td', {class: 'whole-row-label', colspan: repositoryList.length + 1}, row.labelForWholeRow()));
</span><span class="cx">                 else {
</span><span class="cx">                     cells.push(element('td', row.resultContent(barGraphGroup)));
</span><del>-                    cells.push(self._createRevisionListCells(repositoryList, revisionSupressionCount, group, row.commitSet(), rowIndex));
</del><ins>+                    cells.push(this._createRevisionListCells(repositoryList, revisionSupressionCount, group, row.commitSet(), rowIndex));
</ins><span class="cx">                 }
</span><span class="cx"> 
</span><del>-                return element('tr', [cells, row.additionalColumns()]);
</del><ins>+                return element('tr', buildColumns(cells, row, rowIndex));
</ins><span class="cx">             }));
</span><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         this.renderReplace(this.content().querySelector('table'), [
</span><span class="cx">             element('thead', [
</span><del>-                this._rangeSelectorLabels ? this._rangeSelectorLabels.map(function (label) { return element('th', label) }) : [],
-                this.heading(),
-                element('th', 'Result'),
-                repositoryList.map(function (repository) { return element('th', repository.label()); }),
-                this.additionalHeading(),
</del><ins>+                buildHeaders([
+                    ComponentBase.createElement('th', {colspan: hasGroupHeading ? 2 : 1}, headingLabel),
+                    element('th', 'Result'),
+                    repositoryList.map((repository) =&gt; element('th', repository.label())),
+                ]),
</ins><span class="cx">             ]),
</span><span class="cx">             tableBodies,
</span><span class="cx">         ]);
</span><span class="cx"> 
</span><del>-        this.renderReplace(this.content().querySelector('.results-table-extra-repositories'),
-            extraRepositories.map(function (commit) { return element('li', commit.title()); }));
</del><ins>+        this.renderReplace(this.content('constant-commits'), constantCommits.map((commit) =&gt; element('li', commit.title())));
</ins><span class="cx"> 
</span><span class="cx">         barGraphGroup.updateGroupRendering();
</span><span class="cx"> 
</span><del>-        Instrumentation.endMeasuringTime('ResultsTable', 'render');
</del><ins>+        Instrumentation.endMeasuringTime('ResultsTable', 'renderTable');
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _createRevisionListCells(repositoryList, revisionSupressionCount, testGroup, commitSet, rowIndex)
</span><span class="cx">     {
</span><del>-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        var cells = [];
-        for (var repository of repositoryList) {
-            var commit = commitSet ? commitSet.commitForRepository(repository) : null;
</del><ins>+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+        const cells = [];
+        for (const repository of repositoryList) {
+            const commit = commitSet ? commitSet.commitForRepository(repository) : null;
</ins><span class="cx"> 
</span><span class="cx">             if (revisionSupressionCount[repository.id()]) {
</span><span class="cx">                 revisionSupressionCount[repository.id()]--;
</span><span class="lines">@@ -112,8 +83,8 @@
</span><span class="cx">                     break;
</span><span class="cx">                 succeedingRowIndex++;
</span><span class="cx">             }
</span><del>-            var rowSpan = succeedingRowIndex - rowIndex;
-            var attributes = {class: 'revision'};
</del><ins>+            const rowSpan = succeedingRowIndex - rowIndex;
+            const attributes = {class: 'revision'};
</ins><span class="cx">             if (rowSpan &gt; 1) {
</span><span class="cx">                 revisionSupressionCount[repository.id()] = rowSpan - 1;
</span><span class="cx">                 attributes['rowspan'] = rowSpan;                       
</span><span class="lines">@@ -121,9 +92,9 @@
</span><span class="cx">             if (rowIndex + rowSpan &gt;= testGroup.rows.length)
</span><span class="cx">                 attributes['class'] += ' lastRevision';
</span><span class="cx"> 
</span><del>-            var content = 'Missing';
</del><ins>+            let content = 'Missing';
</ins><span class="cx">             if (commit) {
</span><del>-                var url = commit.url();
</del><ins>+                const url = commit.url();
</ins><span class="cx">                 content = url ? link(commit.label(), url) : commit.label();
</span><span class="cx">             }
</span><span class="cx"> 
</span><span class="lines">@@ -132,22 +103,9 @@
</span><span class="cx">         return cells;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    heading() { throw 'NotImplemented'; }
-    additionalHeading() { return []; }
-    buildRowGroups() { throw 'NotImplemented'; }
-
-    _computeRepositoryList(rowGroups, extraRepositories)
</del><ins>+    _computeRepositoryList(rowGroups)
</ins><span class="cx">     {
</span><del>-        var allRepositories = Repository.all().sort(function (a, b) {
-            if (a.hasUrlForRevision() == b.hasUrlForRevision()) {
-                if (a.name() &gt; b.name())
-                    return 1;
-                if (a.name() &lt; b.name())
-                    return -1;
-                return 0;
-            }
-            return a.hasUrlForRevision() ? -1 /* a &lt; b */ : 1; // a &gt; b
-        });
</del><ins>+        const allRepositories = Repository.sortByNamePreferringOnesWithURL(Repository.all());
</ins><span class="cx">         const commitSets = [];
</span><span class="cx">         for (let group of rowGroups) {
</span><span class="cx">             for (let row of group.rows) {
</span><span class="lines">@@ -159,20 +117,21 @@
</span><span class="cx">         if (!commitSets.length)
</span><span class="cx">             return [];
</span><span class="cx"> 
</span><del>-        const repositoryPresenceMap = {};
</del><ins>+        const changedRepositorySet = new Set;
+        const constantCommits = new Set;
</ins><span class="cx">         for (let repository of allRepositories) {
</span><span class="cx">             const someCommit = commitSets[0].commitForRepository(repository);
</span><span class="cx">             if (CommitSet.containsMultipleCommitsForRepository(commitSets, repository))
</span><del>-                repositoryPresenceMap[repository.id()] = true;
</del><ins>+                changedRepositorySet.add(repository);
</ins><span class="cx">             else if (someCommit)
</span><del>-                extraRepositories.push(someCommit);
</del><ins>+                constantCommits.add(someCommit);
</ins><span class="cx">         }
</span><del>-        return allRepositories.filter(function (repository) { return repositoryPresenceMap[repository.id()]; });
</del><ins>+        return [allRepositories.filter((repository) =&gt; changedRepositorySet.has(repository)), [...constantCommits]];
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><del>-        return `&lt;table class=&quot;results-table&quot;&gt;&lt;/table&gt;&lt;ul class=&quot;results-table-extra-repositories&quot;&gt;&lt;/ul&gt;`;
</del><ins>+        return `&lt;table class=&quot;results-table&quot;&gt;&lt;/table&gt;&lt;ul id=&quot;constant-commits&quot;&gt;&lt;/ul&gt;`;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static cssTemplate()
</span><span class="lines">@@ -248,7 +207,7 @@
</span><span class="cx">                 height: 1.2rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .results-table-extra-repositories {
</del><ins>+            #constant-commits {
</ins><span class="cx">                 list-style: none;
</span><span class="cx">                 margin: 0;
</span><span class="cx">                 padding: 0.5rem 0 0 0.5rem;
</span><span class="lines">@@ -255,15 +214,15 @@
</span><span class="cx">                 font-size: 0.8rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .results-table-extra-repositories:empty {
</del><ins>+            #constant-commits:empty {
</ins><span class="cx">                 padding: 0;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .results-table-extra-repositories li {
</del><ins>+            #constant-commits li {
</ins><span class="cx">                 display: inline;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .results-table-extra-repositories li:not(:last-child):after {
</del><ins>+            #constant-commits li:not(:last-child):after {
</ins><span class="cx">                 content: ', ';
</span><span class="cx">             }
</span><span class="cx">         `;
</span><span class="lines">@@ -278,7 +237,6 @@
</span><span class="cx">         this._link = null;
</span><span class="cx">         this._label = '-';
</span><span class="cx">         this._commitSet = commitSet;
</span><del>-        this._additionalColumns = [];
</del><span class="cx">         this._labelForWholeRow = null;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -294,9 +252,6 @@
</span><span class="cx">     setLabelForWholeRow(label) { this._labelForWholeRow = label; }
</span><span class="cx">     labelForWholeRow() { return this._labelForWholeRow; }
</span><span class="cx"> 
</span><del>-    additionalColumns() { return this._additionalColumns; }
-    setAdditionalColumns(additionalColumns) { this._additionalColumns = additionalColumns; }
-
</del><span class="cx">     resultContent(barGraphGroup)
</span><span class="cx">     {
</span><span class="cx">         var resultContent = this._result ? barGraphGroup.addBar(this._result.value, this._result.interval) : this._label;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentstestgroupformjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -4,45 +4,31 @@
</span><span class="cx">     constructor(name)
</span><span class="cx">     {
</span><span class="cx">         super(name || 'test-group-form');
</span><del>-        this._startCallback = null;
-        this._label = undefined;
</del><span class="cx">         this._repetitionCount = 4;
</span><ins>+    }
</ins><span class="cx"> 
</span><del>-        this._nameControl = this.content().querySelector('.name');
-        this._repetitionCountControl = this.content().querySelector('.repetition-count');
-        var self = this;
-        this._repetitionCountControl.onchange = function () {
-            self._repetitionCount = self._repetitionCountControl.value;
-        }
-
-        var boundSubmitted = this._submitted.bind(this);
-        this.content().querySelector('form').onsubmit = function (event) {
-            event.preventDefault();
-            boundSubmitted();
-        }
</del><ins>+    setRepetitionCount(count)
+    {
+        this.content('repetition-count').value = count;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    setStartCallback(callback) { this._startCallback = callback; }
-    setLabel(label) { this._label = label; }
-    setRepetitionCount(count) { this._repetitionCount = count; }
-
-    render()
</del><ins>+    didConstructShadowTree()
</ins><span class="cx">     {
</span><del>-        var button = this.content().querySelector('button');
-        if (this._label)
-            button.textContent = this._label;
-        this._repetitionCountControl.value = this._repetitionCount;
</del><ins>+        const repetitionCountSelect = this.content('repetition-count');
+        repetitionCountSelect.onchange = () =&gt; {
+            this._repetitionCount = repetitionCountSelect.value;
+        }
+        this.content('form').onsubmit = this.createEventHandler(() =&gt; this.startTesting());
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _submitted()
</del><ins>+    startTesting()
</ins><span class="cx">     {
</span><del>-        if (this._startCallback)
-            this._startCallback(this._repetitionCount);
</del><ins>+        this.dispatchAction('startTesting', this._repetitionCount);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><del>-        return `&lt;form&gt;&lt;button type=&quot;submit&quot;&gt;Start A/B testing&lt;/button&gt;${this.formContent()}&lt;/form&gt;`;
</del><ins>+        return `&lt;form id=&quot;form&quot;&gt;&lt;button id=&quot;start-button&quot; type=&quot;submit&quot;&gt;&lt;slot&gt;Start A/B testing&lt;/slot&gt;&lt;/button&gt;${this.formContent()}&lt;/form&gt;`;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static formContent()
</span><span class="lines">@@ -49,7 +35,7 @@
</span><span class="cx">     {
</span><span class="cx">         return `
</span><span class="cx">             with
</span><del>-            &lt;select class=&quot;repetition-count&quot;&gt;
</del><ins>+            &lt;select id=&quot;repetition-count&quot;&gt;
</ins><span class="cx">                 &lt;option&gt;1&lt;/option&gt;
</span><span class="cx">                 &lt;option&gt;2&lt;/option&gt;
</span><span class="cx">                 &lt;option&gt;3&lt;/option&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentstestgroupresultstablejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -4,30 +4,33 @@
</span><span class="cx">     {
</span><span class="cx">         super('test-group-results-table');
</span><span class="cx">         this._testGroup = null;
</span><del>-        this._renderedTestGroup = null;
</del><ins>+        this._renderTestGroupLazily = new LazilyEvaluatedFunction(this._renderTestGroup.bind(this));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    didUpdateResults() { this._renderedTestGroup = null; }
</del><span class="cx">     setTestGroup(testGroup)
</span><span class="cx">     {
</span><span class="cx">         this._testGroup = testGroup;
</span><del>-        this._renderedTestGroup = null;
</del><ins>+        this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    heading()
</del><ins>+    render()
</ins><span class="cx">     {
</span><del>-        return ComponentBase.createElement('th', {colspan: 2}, 'Configuration');
</del><ins>+        super.render();
+        this._renderTestGroupLazily.evaluate(this._testGroup, this._analysisResultsView);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    render()
</del><ins>+    _renderTestGroup(testGroup, analysisResults)
</ins><span class="cx">     {
</span><del>-        if (this._renderedTestGroup == this._testGroup)
</del><ins>+        if (!analysisResults)
</ins><span class="cx">             return;
</span><del>-        this._renderedTestGroup = this._testGroup;
-        super.render();
</del><ins>+        const rowGroups = this._buildRowGroups();
+        this.renderTable(
+            analysisResults.metric().makeFormatter(4),
+            rowGroups,
+            'Configuration');
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    buildRowGroups()
</del><ins>+    _buildRowGroups()
</ins><span class="cx">     {
</span><span class="cx">         const testGroup = this._testGroup;
</span><span class="cx">         if (!testGroup)
</span><span class="lines">@@ -34,43 +37,23 @@
</span><span class="cx">             return [];
</span><span class="cx"> 
</span><span class="cx">         const commitSets = this._testGroup.requestedCommitSets();
</span><del>-        const groups = commitSets.map(function (commitSet) {
-            const rows = [new ResultsTableRow('Mean', commitSet)];
-            var results = [];
-
-            for (var request of testGroup.requestsForCommitSet(commitSet)) {
-                var result = request.result();
-                // Call result.commitSet() for each result since the set of revisions used in testing maybe different from requested ones.
-                var row = new ResultsTableRow(1 + +request.order(), result ? result.commitSet() : null);
-                rows.push(row);
-                if (result) {
-                    row.setLink(result.build().url(), result.build().label());
-                    row.setResult(result);
-                    results.push(result);
-                } else
-                    row.setLink(request.statusUrl(), request.statusLabel());
-            }
-
-            var aggregatedResult = MeasurementAdaptor.aggregateAnalysisResults(results);
-            if (!isNaN(aggregatedResult.value))
-                rows[0].setResult(aggregatedResult);
-
-            return {heading: testGroup.labelForCommitSet(commitSet), rows};
</del><ins>+        const resultsByCommitSet = new Map;
+        const groups = commitSets.map((commitSet) =&gt; {
+            const group = this._buildRowGroupForCommitSet(testGroup, commitSet, resultsByCommitSet);
+            resultsByCommitSet.set(commitSet, group.results);
+            return group;
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         const comparisonRows = [];
</span><del>-        for (let i = 0; i &lt; commitSets.length; i++) {
</del><ins>+        for (let i = 0; i &lt; commitSets.length - 1; i++) {
+            const startCommit = commitSets[i];
</ins><span class="cx">             for (let j = i + 1; j &lt; commitSets.length; j++) {
</span><del>-                const startConfig = testGroup.labelForCommitSet(commitSets[i]);
-                const endConfig = testGroup.labelForCommitSet(commitSets[j]);
-
-                const result = this._testGroup.compareTestResults(commitSets[i], commitSets[j]);
-                if (result.changeType == null)
</del><ins>+                const endCommit = commitSets[j];
+                const startResults = resultsByCommitSet.get(startCommit) || [];
+                const endResults = resultsByCommitSet.get(endCommit) || [];
+                const row = this._buildComparisonRow(testGroup, startCommit, startResults, endCommit, endResults);
+                if (!row)
</ins><span class="cx">                     continue;
</span><del>-
-                var row = new ResultsTableRow(`${startConfig} to ${endConfig}`, null);
-                var element = ComponentBase.createElement;
-                row.setLabelForWholeRow(element('span', {class: 'results-label ' + result.status}, result.fullLabel));
</del><span class="cx">                 comparisonRows.push(row);
</span><span class="cx">             }
</span><span class="cx">         }
</span><span class="lines">@@ -80,6 +63,48 @@
</span><span class="cx">         return groups;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _buildRowGroupForCommitSet(testGroup, commitSet)
+    {
+        const rows = [new ResultsTableRow('Mean', commitSet)];
+        const results = [];
+
+        for (const request of testGroup.requestsForCommitSet(commitSet)) {
+            const result = this._analysisResultsView.resultForBuildId(request.buildId());
+            // Call result.commitSet() for each result since the set of revisions used in testing maybe different from requested ones.
+            const row = new ResultsTableRow(1 + +request.order(), result ? result.commitSet() : null);
+            rows.push(row);
+            if (result) {
+                row.setLink(result.build().url(), result.build().label());
+                row.setResult(result);
+                results.push(result);
+            } else
+                row.setLink(request.statusUrl(), request.statusLabel());
+        }
+
+        const aggregatedResult = MeasurementAdaptor.aggregateAnalysisResults(results);
+        if (!isNaN(aggregatedResult.value))
+            rows[0].setResult(aggregatedResult);
+
+        return {heading: testGroup.labelForCommitSet(commitSet), rows, results};
+    }
+
+    _buildComparisonRow(testGroup, startCommit, startResults, endCommit, endResults)
+    {
+        const startConfig = testGroup.labelForCommitSet(startCommit);
+        const endConfig = testGroup.labelForCommitSet(endCommit);
+
+        const result = this._testGroup.compareTestResults(
+            startResults.map((result) =&gt; result.value), endResults.map((result) =&gt; result.value));
+        if (result.changeType == null)
+            return null;
+
+        const row = new ResultsTableRow(`${startConfig} to ${endConfig}`, null);
+        const element = ComponentBase.createElement;
+        row.setLabelForWholeRow(element('span',
+            {class: 'results-label ' + result.status}, `${endConfig} is ${result.fullLabel} than ${startConfig}`));
+        return row;
+    }
+
</ins><span class="cx">     static cssTemplate()
</span><span class="cx">     {
</span><span class="cx">         return super.cssTemplate() + `
</span><span class="lines">@@ -89,17 +114,21 @@
</span><span class="cx">                 height: 100%;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .results-label .failed {
</del><ins>+            th {
+                vertical-align: top;
+            }
+
+            .failed {
</ins><span class="cx">                 color: rgb(128, 51, 128);
</span><span class="cx">             }
</span><del>-            .results-label .unchanged {
</del><ins>+            .unchanged {
</ins><span class="cx">                 color: rgb(128, 128, 128);
</span><span class="cx">             }
</span><del>-            .results-label.worse {
</del><ins>+            .worse {
</ins><span class="cx">                 color: rgb(255, 102, 102);
</span><span class="cx">                 font-weight: bold;
</span><span class="cx">             }
</span><del>-            .results-label.better {
</del><ins>+            .better {
</ins><span class="cx">                 color: rgb(102, 102, 255);
</span><span class="cx">                 font-weight: bold;
</span><span class="cx">             }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -89,6 +89,7 @@
</span><span class="cx">         &lt;script src=&quot;components/chart-revision-range.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/chart-pane-base.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/mutable-list-view.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;components/analysis-task-bug-list.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;components/ratio-bar-graph.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;pages/page.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsanalysisresultsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/models/analysis-results.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -2,27 +2,34 @@
</span><span class="cx"> class AnalysisResults {
</span><span class="cx">     constructor()
</span><span class="cx">     {
</span><del>-        this._buildToMetricsMap = {};
</del><ins>+        this._metricToBuildMap = {};
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    find(buildId, metric)
</del><ins>+    find(buildId, metricId)
</ins><span class="cx">     {
</span><del>-        var map = this._buildToMetricsMap[buildId];
</del><ins>+        const map = this._metricToBuildMap[metricId];
</ins><span class="cx">         if (!map)
</span><span class="cx">             return null;
</span><del>-        return map[metric.id()];
</del><ins>+        return map[buildId];
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     add(measurement)
</span><span class="cx">     {
</span><span class="cx">         console.assert(measurement.configType == 'current');
</span><del>-        if (!this._buildToMetricsMap[measurement.buildId])
-            this._buildToMetricsMap[measurement.buildId] = {};
-        var map = this._buildToMetricsMap[measurement.buildId];
-        console.assert(!map[measurement.metricId]);
-        map[measurement.metricId] = measurement;
</del><ins>+        const metricId = measurement.metricId;
+        if (!(metricId in this._metricToBuildMap))
+            this._metricToBuildMap[metricId] = {};
+        var map = this._metricToBuildMap[metricId];
+        console.assert(!map[measurement.buildId]);
+        map[measurement.buildId] = measurement;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    viewForMetric(metric)
+    {
+        console.assert(metric instanceof Metric);
+        return new AnalysisResultsView(this, metric);
+    }
+
</ins><span class="cx">     static fetch(taskId)
</span><span class="cx">     {
</span><span class="cx">         taskId = parseInt(taskId);
</span><span class="lines">@@ -30,9 +37,9 @@
</span><span class="cx"> 
</span><span class="cx">             Instrumentation.startMeasuringTime('AnalysisResults', 'fetch');
</span><span class="cx"> 
</span><del>-            var adaptor = new MeasurementAdaptor(response['formatMap']);
-            var results = new AnalysisResults;
-            for (var rawMeasurement of response['measurements'])
</del><ins>+            const adaptor = new MeasurementAdaptor(response['formatMap']);
+            const results = new AnalysisResults;
+            for (const rawMeasurement of response['measurements'])
</ins><span class="cx">                 results.add(adaptor.applyToAnalysisResults(rawMeasurement));
</span><span class="cx"> 
</span><span class="cx">             Instrumentation.endMeasuringTime('AnalysisResults', 'fetch');
</span><span class="lines">@@ -41,3 +48,20 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> }
</span><ins>+
+class AnalysisResultsView {
+    constructor(analysisResults, metric)
+    {
+        console.assert(analysisResults instanceof AnalysisResults);
+        console.assert(metric instanceof Metric);
+        this._results = analysisResults;
+        this._metric = metric;
+    }
+
+    metric() { return this._metric; }
+
+    resultForBuildId(buildId)
+    {
+        return this._results.find(buildId, this._metric.id());
+    }
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -108,13 +108,6 @@
</span><span class="cx">         return label;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    result() { return this._result; }
-    setResult(result)
-    {
-        this._result = result;
-        this._testGroup.didSetResult(this);
-    }
-
</del><span class="cx">     static fetchForTriggerable(triggerable)
</span><span class="cx">     {
</span><span class="cx">         return RemoteAPI.getJSONWithStatus('/api/build-requests/' + triggerable).then(function (data) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstestgroupjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/test-group.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/test-group.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/models/test-group.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -14,7 +14,6 @@
</span><span class="cx">         this._repositories = null;
</span><span class="cx">         this._requestedCommitSets = null;
</span><span class="cx">         this._commitSetToLabel = new Map;
</span><del>-        this._allCommitSets = null;
</del><span class="cx">         console.assert(!object.platform || object.platform instanceof Platform);
</span><span class="cx">         this._platform = object.platform;
</span><span class="cx">     }
</span><span class="lines">@@ -95,11 +94,6 @@
</span><span class="cx">         this._requestsAreInOrder = true;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    didSetResult(request)
-    {
-        this._allCommitSets = null;
-    }
-
</del><span class="cx">     hasFinished()
</span><span class="cx">     {
</span><span class="cx">         return this._buildRequests.every(function (request) { return request.hasFinished(); });
</span><span class="lines">@@ -115,10 +109,8 @@
</span><span class="cx">         return this._buildRequests.some(function (request) { return request.isPending(); });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    compareTestResults(commitSetA, commitSetB)
</del><ins>+    compareTestResults(beforeValues, afterValues)
</ins><span class="cx">     {
</span><del>-        const beforeValues = this._valuesForCommitSet(commitSetA);
-        const afterValues = this._valuesForCommitSet(commitSetB);
</del><span class="cx">         const beforeMean = Statistics.sum(beforeValues) / beforeValues.length;
</span><span class="cx">         const afterMean = Statistics.sum(afterValues) / afterValues.length;
</span><span class="cx"> 
</span><span class="lines">@@ -160,17 +152,6 @@
</span><span class="cx">         return result;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _valuesForCommitSet(commitSet)
-    {
-        const requests = this.requestsForCommitSet(commitSet);
-        const values = [];
-        for (let request of requests) {
-            if (request.result())
-                values.push(request.result().value);
-        }
-        return values;
-    }
-
</del><span class="cx">     updateName(newName)
</span><span class="cx">     {
</span><span class="cx">         var self = this;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -4,9 +4,15 @@
</span><span class="cx">     {
</span><span class="cx">         super('analysis-task-chart-pane');
</span><span class="cx">         this._page = null;
</span><ins>+        this._showForm = false;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     setPage(page) { this._page = page; }
</span><ins>+    setShowForm(show)
+    {
+        this._showForm = show;
+        this.enqueueToRender();
+    }
</ins><span class="cx">     router() { return this._page.router(); }
</span><span class="cx"> 
</span><span class="cx">     _mainSelectionDidChange(selection, didEndDrag)
</span><span class="lines">@@ -13,28 +19,316 @@
</span><span class="cx">     {
</span><span class="cx">         super._mainSelectionDidChange(selection);
</span><span class="cx">         if (didEndDrag)
</span><del>-            this._page._chartSelectionDidChange();
</del><ins>+            this.enqueueToRender();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    selectedPoints()
</del><ins>+    didConstructShadowTree()
</ins><span class="cx">     {
</span><del>-        return this._mainChart ? this._mainChart.selectedPoints('current') : null;
</del><ins>+        this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap) =&gt; {
+            this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap);
+        });
</ins><span class="cx">     }
</span><ins>+
+    render()
+    {
+        super.render();
+        const points = this._mainChart ? this._mainChart.selectedPoints('current') : null;
+
+        this.content('form').style.display = this._showForm ? null : 'none';
+        if (this._showForm) {
+            const form = this.part('form');
+            form.setCommitSetMap(points &amp;&amp; points.length() &gt;= 2 ? {'A': points.firstPoint().commitSet(), 'B': points.lastPoint().commitSet()} : null);
+            form.enqueueToRender();
+        }
+    }
+
+    static paneFooterTemplate() { return '&lt;customizable-test-group-form id=&quot;form&quot;&gt;&lt;/customizable-test-group-form&gt;'; }
+
+    static cssTemplate()
+    {
+        return super.cssTemplate() + `
+            #form {
+                margin: 0.5rem;
+            }
+        `;
+    }
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
</span><span class="cx"> 
</span><ins>+class AnalysisTaskResultsPane extends ComponentBase {
+    constructor()
+    {
+        super('analysis-task-results-pane');
+        this._showForm = false;
+    }
+
+    setPoints(startPoint, endPoint, metric)
+    {
+        const resultsViewer = this.part('results-viewer');
+        resultsViewer.setPoints(startPoint, endPoint, metric);
+        resultsViewer.enqueueToRender();
+    }
+
+    setTestGroups(testGroups, currentGroup)
+    {
+        this.part('results-viewer').setTestGroups(testGroups, currentGroup);
+        this.enqueueToRender();
+    }
+
+    setAnalysisResultsView(analysisResultsView)
+    {
+        this.part('results-viewer').setAnalysisResultsView(analysisResultsView);
+        this.enqueueToRender();
+    }
+
+    setShowForm(show)
+    {
+        this._showForm = show;
+        this.enqueueToRender();
+    }
+
+    didConstructShadowTree()
+    {
+        const resultsViewer = this.part('results-viewer');
+        resultsViewer.listenToAction('testGroupClick', (testGroup) =&gt; this.dispatchAction('showTestGroup', testGroup));
+        resultsViewer.setRangeSelectorLabels(['A', 'B']);
+        resultsViewer.listenToAction('rangeSelectorClick', () =&gt; this.enqueueToRender());
+
+        this.part('form').listenToAction('startTesting', (repetitionCount, name, commitSetMap) =&gt; {
+            this.dispatchAction('newTestGroup', name, repetitionCount, commitSetMap);
+        });
+    }
+
+    render()
+    {
+        this.part('results-viewer').enqueueToRender();
+
+        this.content('form').style.display = this._showForm ? null : 'none';
+        if (!this._showForm)
+            return;
+
+        const selectedRange = this.part('results-viewer').selectedRange();
+        const firstCommitSet = selectedRange['A'];
+        const secondCommitSet = selectedRange['B'];
+        const form = this.part('form');
+        form.setCommitSetMap(firstCommitSet &amp;&amp; secondCommitSet ? {'A': firstCommitSet, 'B': secondCommitSet} : null);
+        form.enqueueToRender();
+    }
+
+    static htmlTemplate()
+    {
+        return `&lt;analysis-results-viewer id=&quot;results-viewer&quot;&gt;&lt;/analysis-results-viewer&gt;&lt;customizable-test-group-form id=&quot;form&quot;&gt;&lt;/customizable-test-group-form&gt;`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            #form {
+                margin: 0.5rem;
+            }
+        `;
+    }
+}
+
+ComponentBase.defineElement('analysis-task-results-pane', AnalysisTaskResultsPane);
+
+class AnalysisTaskTestGroupPane extends ComponentBase {
+
+    constructor()
+    {
+        super('analysis-task-test-group-pane');
+        this._renderTestGroupsLazily = new LazilyEvaluatedFunction(this._renderTestGroups.bind(this));
+        this._renderTestGroupVisibilityLazily = new LazilyEvaluatedFunction(this._renderTestGroupVisibility.bind(this));
+        this._renderTestGroupNamesLazily = new LazilyEvaluatedFunction(this._renderTestGroupNames.bind(this));
+        this._renderCurrentTestGroupLazily = new LazilyEvaluatedFunction(this._renderCurrentTestGroup.bind(this));
+        this._testGroupMap = new Map;
+        this._testGroups = [];
+        this._currentTestGroup = null;
+        this._showHiddenGroups = false;
+    }
+
+    didConstructShadowTree()
+    {
+        this.content('hide-button').onclick = () =&gt; this.dispatchAction('toggleTestGroupVisibility', this._currentTestGroup);
+        this.part('retry-form').listenToAction('startTesting', (repetitionCount) =&gt; {
+            this.dispatchAction('retryTestGroup', this._currentTestGroup, repetitionCount);
+        });
+    }
+
+    setTestGroups(testGroups, currentTestGroup, showHiddenGroups)
+    {
+        this._testGroups = testGroups;
+        this._currentTestGroup = currentTestGroup;
+        this._showHiddenGroups = showHiddenGroups;
+        this.part('results-table').setTestGroup(currentTestGroup);
+        this.enqueueToRender();
+    }
+
+    setAnalysisResultsView(analysisResultsView)
+    {
+        this.part('results-table').setAnalysisResultsView(analysisResultsView);
+        this.enqueueToRender();
+    }
+
+    render()
+    {
+        this._renderTestGroupsLazily.evaluate(this._showHiddenGroups, ...this._testGroups);
+        this._renderTestGroupVisibilityLazily.evaluate(...this._testGroups.map((group) =&gt; group.isHidden() ? 'hidden' : 'visible'));
+        this._renderTestGroupNamesLazily.evaluate(...this._testGroups.map((group) =&gt; group.label()));
+        this._renderCurrentTestGroup(this._currentTestGroup);
+        this.part('results-table').enqueueToRender();
+        this.part('retry-form').enqueueToRender();
+    }
+
+    _renderTestGroups(showHiddenGroups, ...testGroups)
+    {
+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+
+        this._testGroupMap = new Map;
+        const testGroupItems = testGroups.map((group) =&gt; {
+            const text = new EditableText(group.label());
+            text.listenToAction('update', () =&gt; this.dispatchAction('renameTestGroup', group, text.editedText()));
+
+            const listItem = element('li', link(text, group.label(), () =&gt; this.dispatchAction('showTestGroup', group)));
+
+            this._testGroupMap.set(group, {text, listItem});
+            return listItem;
+        });
+
+        this.renderReplace(this.content('test-group-list'), [testGroupItems,
+            showHiddenGroups ? [] : element('li', {class: 'test-group-list-show-all'}, link('Show hidden tests', () =&gt; {
+                this.dispatchAction('showHiddenTestGroups');
+            }))]);
+    }
+
+    _renderTestGroupVisibility(...groupVisibilities)
+    {
+        for (let i = 0; i &lt; groupVisibilities.length; i++)
+            this._testGroupMap.get(this._testGroups[i]).listItem.className = groupVisibilities[i];
+    }
+
+    _renderTestGroupNames(...groupNames)
+    {
+        for (let i = 0; i &lt; groupNames.length; i++)
+            this._testGroupMap.get(this._testGroups[i]).text.setText(groupNames[i]);
+    }
+
+    _renderCurrentTestGroup(currentGroup)
+    {
+        const selected = this.content('test-group-list').querySelector('.selected');
+        if (selected)
+            selected.classList.remove('selected');
+        if (currentGroup)
+            this._testGroupMap.get(currentGroup).listItem.classList.add('selected');
+
+        if (currentGroup)
+            this.part('retry-form').setRepetitionCount(currentGroup.repetitionCount());
+        this.content('retry-form').style.display = currentGroup ? null : 'none';
+
+        const hideButton = this.content('hide-button');
+        hideButton.textContent = currentGroup &amp;&amp; currentGroup.isHidden() ? 'Unhide' : 'Hide';
+        hideButton.style.display = currentGroup ? null : 'none';
+
+        this.content('pending-request-cancel-warning').style.display = currentGroup &amp;&amp; currentGroup.hasPending() ? null : 'none';
+    }
+
+    static htmlTemplate()
+    {
+        return `
+            &lt;ul id=&quot;test-group-list&quot;&gt;&lt;/ul&gt;
+            &lt;div id=&quot;test-group-details&quot;&gt;
+                &lt;test-group-results-table id=&quot;results-table&quot;&gt;&lt;/test-group-results-table&gt;
+                &lt;test-group-form id=&quot;retry-form&quot;&gt;Retry&lt;/test-group-form&gt;
+                &lt;button id=&quot;hide-button&quot;&gt;Hide&lt;/button&gt;
+                &lt;span id=&quot;pending-request-cancel-warning&quot;&gt;(cancels pending requests)&lt;/span&gt;
+            &lt;/div&gt;`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            :host {
+                display: flex !important;
+            }
+
+            #test-group-list {
+                margin: 0;
+                padding: 0.2rem 0;
+                list-style: none;
+                border-right: solid 1px #ccc;
+                white-space: nowrap;
+                min-width: 8rem;
+            }
+
+            li {
+                display: block;
+                font-size: 0.9rem;
+            }
+
+            li &gt; a {
+                display: block;
+                color: inherit;
+                text-decoration: none;
+                margin: 0;
+                padding: 0.2rem;
+            }
+
+            li.test-group-list-show-all {
+                font-size: 0.8rem;
+                margin-top: 0.5rem;
+                padding-right: 1rem;
+                text-align: center;
+                color: #999;
+            }
+
+            li.test-group-list-show-all:not(.selected) a:hover {
+                background: inherit;
+            }
+
+            li.selected &gt; a {
+                background: rgba(204, 153, 51, 0.1);
+            }
+
+            li.hidden {
+                color: #999;
+            }
+
+            li:not(.selected) &gt; a:hover {
+                background: #eee;
+            }
+
+            #test-group-details {
+                display: table-cell;
+                margin-bottom: 1rem;
+                padding: 0;
+                margin: 0;
+            }
+
+            #retry-form {
+                display: block;
+                margin: 0.5rem;
+            }
+
+            #hide-button {
+                margin: 0.5rem;
+            }`;
+    }
+}
+
+ComponentBase.defineElement('analysis-task-test-group-pane', AnalysisTaskTestGroupPane);
+
</ins><span class="cx"> class AnalysisTaskPage extends PageWithHeading {
</span><span class="cx">     constructor()
</span><span class="cx">     {
</span><span class="cx">         super('Analysis Task');
</span><span class="cx">         this._task = null;
</span><ins>+        this._metric = null;
</ins><span class="cx">         this._triggerable = null;
</span><span class="cx">         this._relatedTasks = null;
</span><span class="cx">         this._testGroups = null;
</span><del>-        this._renderedTestGroups = null;
</del><span class="cx">         this._testGroupLabelMap = new Map;
</span><del>-        this._renderedCurrentTestGroup = undefined;
</del><span class="cx">         this._analysisResults = null;
</span><span class="cx">         this._measurementSet = null;
</span><span class="cx">         this._startPoint = null;
</span><span class="lines">@@ -43,42 +337,10 @@
</span><span class="cx">         this._currentTestGroup = null;
</span><span class="cx">         this._filteredTestGroups = null;
</span><span class="cx">         this._showHiddenTestGroups = false;
</span><del>-        this._selectionWasModifiedByUser = false;
</del><span class="cx"> 
</span><del>-        this._chartPane = this.content().querySelector('analysis-task-chart-pane').component();
-        this._chartPane.setPage(this);
-        this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
-        this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
-        this._analysisResultsViewer.setRangeSelectorLabels(['A', 'B']);
-        this._analysisResultsViewer.setRangeSelectorCallback(this._selectedRowInAnalysisResultsViewer.bind(this));
-        this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
-
-        this._taskNameLabel = this.content().querySelector('.analysis-task-name editable-text').component();
-        this._taskNameLabel.listenToAction('update', () =&gt; this._updateTaskName());
-
-        this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
-        this._taskStatusControl = this.content().querySelector('.change-type-form select');
-
-        this._bugList = this.content().querySelector('.associated-bugs mutable-list-view').component();
-        this._bugList.setKindList(BugTracker.all());
-        this._bugList.setAddCallback(this._associateBug.bind(this));
-
-        this._causeList = this.content().querySelector('.cause-list mutable-list-view').component();
-        this._causeList.setAddCallback(this._associateCommit.bind(this, 'cause'));
-
-        this._fixList = this.content().querySelector('.fix-list mutable-list-view').component();
-        this._fixList.setAddCallback(this._associateCommit.bind(this, 'fix'));
-
-        this._newTestGroupFormForChart = this.content().querySelector('.overview-chart customizable-test-group-form').component();
-        this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
-
-        this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view customizable-test-group-form').component();
-        this._newTestGroupFormForViewer.setStartCallback(this._createNewTestGroupFromViewer.bind(this));
-
-        this._retryForm = this.content().querySelector('.test-group-retry-form test-group-form').component();
-        this._retryForm.setStartCallback(this._retryCurrentTestGroup.bind(this));
-        this._hideButton = this.content().querySelector('.test-group-hide-button');
-        this._hideButton.onclick = this._hideCurrentTestGroup.bind(this);
</del><ins>+        this._renderTaskNameAndStatusLazily = new LazilyEvaluatedFunction(this._renderTaskNameAndStatus.bind(this));
+        this._renderCauseAndFixesLazily = new LazilyEvaluatedFunction(this._renderCauseAndFixes.bind(this));
+        this._renderRelatedTasksLazily = new LazilyEvaluatedFunction(this._renderRelatedTasks.bind(this));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     title() { return this._task ? this._task.label() : 'Analysis Task'; }
</span><span class="lines">@@ -105,11 +367,41 @@
</span><span class="cx">         }
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    didConstructShadowTree()
+    {
+        this.part('analysis-task-name').listenToAction('update', () =&gt; this._updateTaskName(this.part('analysis-task-name').editedText()));
+
+        this.content('change-type-form').onsubmit = ComponentBase.createEventHandler((event) =&gt; this._updateChangeType(event));
+
+        this.part('chart-pane').listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
+
+        const resultsPane = this.part('results-pane');
+        resultsPane.listenToAction('showTestGroup', (testGroup) =&gt; this._showTestGroup(testGroup));
+        resultsPane.listenToAction('newTestGroup', this._createTestGroupAfterVerifyingCommitSetList.bind(this));
+
+        const groupPane = this.part('group-pane');
+        groupPane.listenToAction('showTestGroup', (testGroup) =&gt; this._showTestGroup(testGroup));
+        groupPane.listenToAction('showHiddenTestGroups', () =&gt; this._showAllTestGroups());
+        groupPane.listenToAction('renameTestGroup', (testGroup, newName) =&gt; this._updateTestGroupName(testGroup, newName));
+        groupPane.listenToAction('toggleTestGroupVisibility', (testGroup) =&gt; this._hideCurrentTestGroup(testGroup));
+        groupPane.listenToAction('retryTestGroup', (testGroup, repetitionCount) =&gt; this._retryCurrentTestGroup(testGroup, repetitionCount));
+
+        this.part('cause-list').listenToAction('addItem', (repository, revision) =&gt; {
+            this._associateCommit('cause', repository, revision);
+        });
+        this.part('fix-list').listenToAction('addItem', (repository, revision) =&gt; {
+            this._associateCommit('fix', repository, revision);
+        });
+    }
+
</ins><span class="cx">     _fetchRelatedInfoForTaskId(taskId)
</span><span class="cx">     {
</span><span class="cx">         TestGroup.fetchByTask(taskId).then(this._didFetchTestGroups.bind(this));
</span><span class="cx">         AnalysisResults.fetch(taskId).then(this._didFetchAnalysisResults.bind(this));
</span><del>-        AnalysisTask.fetchRelatedTasks(taskId).then(this._didFetchRelatedAnalysisTasks.bind(this));
</del><ins>+        AnalysisTask.fetchRelatedTasks(taskId).then((relatedTasks) =&gt; {
+            this._relatedTasks = relatedTasks;
+            this.enqueueToRender();
+        });
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _didFetchTask(task)
</span><span class="lines">@@ -117,36 +409,30 @@
</span><span class="cx">         console.assert(!this._task);
</span><span class="cx"> 
</span><span class="cx">         this._task = task;
</span><ins>+
</ins><span class="cx">         const platform = task.platform();
</span><span class="cx">         const metric = task.metric();
</span><span class="cx">         const lastModified = platform.lastModified(metric);
</span><del>-
</del><span class="cx">         this._triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
</span><ins>+        this._metric = metric;
</ins><span class="cx"> 
</span><span class="cx">         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
</span><span class="cx">         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
</span><span class="cx"> 
</span><del>-        const formatter = metric.makeFormatter(4);
-        this._analysisResultsViewer.setValueFormatter(formatter);
-        this._testGroupResultsTable.setValueFormatter(formatter);
</del><ins>+        const chart = this.part('chart-pane');
+        const domain = ChartsPage.createDomainForAnalysisTask(task);
+        chart.configure(platform.id(), metric.id());
+        chart.setOverviewDomain(domain[0], domain[1]);
+        chart.setMainDomain(domain[0], domain[1]);
</ins><span class="cx"> 
</span><del>-        this._chartPane.configure(platform.id(), metric.id());
</del><ins>+        const bugList = this.part('bug-list');
+        this.part('bug-list').setTask(this._task);
</ins><span class="cx"> 
</span><del>-        const domain = ChartsPage.createDomainForAnalysisTask(task);
-        this._chartPane.setOverviewDomain(domain[0], domain[1]);
-        this._chartPane.setMainDomain(domain[0], domain[1]);
-
</del><span class="cx">         this.enqueueToRender();
</span><span class="cx"> 
</span><span class="cx">         return task;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _didFetchRelatedAnalysisTasks(relatedTasks)
-    {
-        this._relatedTasks = relatedTasks;
-        this.enqueueToRender();
-    }
-
</del><span class="cx">     _didFetchMeasurement()
</span><span class="cx">     {
</span><span class="cx">         console.assert(this._task);
</span><span class="lines">@@ -154,10 +440,11 @@
</span><span class="cx">         var series = this._measurementSet.fetchedTimeSeries('current', false, false);
</span><span class="cx">         var startPoint = series.findById(this._task.startMeasurementId());
</span><span class="cx">         var endPoint = series.findById(this._task.endMeasurementId());
</span><ins>+
</ins><span class="cx">         if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
</span><span class="cx">             return;
</span><span class="cx"> 
</span><del>-        this._analysisResultsViewer.setPoints(startPoint, endPoint);
</del><ins>+        this.part('results-pane').setPoints(startPoint, endPoint, this._task.metric());
</ins><span class="cx"> 
</span><span class="cx">         this._startPoint = startPoint;
</span><span class="cx">         this._endPoint = endPoint;
</span><span class="lines">@@ -181,15 +468,11 @@
</span><span class="cx"> 
</span><span class="cx">     _didUpdateTestGroupHiddenState()
</span><span class="cx">     {
</span><del>-        this._renderedCurrentTestGroup = null;
-        this._renderedTestGroups = null;
</del><span class="cx">         if (!this._showHiddenTestGroups)
</span><span class="cx">             this._filteredTestGroups = this._testGroups.filter(function (group) { return !group.isHidden(); });
</span><span class="cx">         else
</span><span class="cx">             this._filteredTestGroups = this._testGroups;
</span><del>-        this._currentTestGroup = this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null;
-        this._analysisResultsViewer.setTestGroups(this._filteredTestGroups);
-        this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
</del><ins>+        this._showTestGroup(this._filteredTestGroups ? this._filteredTestGroups[this._filteredTestGroups.length - 1] : null);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _didFetchAnalysisResults(results)
</span><span class="lines">@@ -201,17 +484,13 @@
</span><span class="cx"> 
</span><span class="cx">     _assignTestResultsIfPossible()
</span><span class="cx">     {
</span><del>-        if (!this._task || !this._testGroups || !this._analysisResults)
</del><ins>+        if (!this._task || !this._metric || !this._testGroups || !this._analysisResults)
</ins><span class="cx">             return false;
</span><span class="cx"> 
</span><del>-        for (var group of this._testGroups) {
-            for (var request of group.buildRequests())
-                request.setResult(this._analysisResults.find(request.buildId(), this._task.metric()));
-        }
</del><ins>+        const view = this._analysisResults.viewForMetric(this._metric);
+        this.part('group-pane').setAnalysisResultsView(view);
+        this.part('results-pane').setAnalysisResultsView(view);
</ins><span class="cx"> 
</span><del>-        this._analysisResultsViewer.didUpdateResults();
-        this._testGroupResultsTable.didUpdateResults();
-
</del><span class="cx">         return true;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -223,196 +502,87 @@
</span><span class="cx"> 
</span><span class="cx">         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
</span><span class="cx"> 
</span><del>-        this._chartPane.enqueueToRender();
</del><ins>+        this._renderTaskNameAndStatusLazily.evaluate(this._task, this._task ? this._task.name() : null, this._task ? this._task.changeType() : null);
+        this._renderCauseAndFixesLazily.evaluate(this._startPoint, this._task);
+        this._renderRelatedTasksLazily.evaluate(this._task, this._relatedTasks);
</ins><span class="cx"> 
</span><del>-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        if (this._task) {
-            this._taskNameLabel.setText(this._task.name());
-            var platform = this._task.platform();
-            var metric = this._task.metric();
-            var anchor = this.content().querySelector('.platform-metric-names a');
-            this.renderReplace(anchor, metric.fullName() + ' on ' + platform.label());
-            anchor.href = this.router().url('charts', ChartsPage.createStateForAnalysisTask(this._task));
</del><ins>+        this.content('chart-pane').style.display = this._task ? null : 'none';
+        this.part('chart-pane').setShowForm(!!this._triggerable);
</ins><span class="cx"> 
</span><del>-            var self = this;
-            this._bugList.setList(this._task.bugs().map(function (bug) {
-                return new MutableListItem(bug.bugTracker(), bug.label(), bug.title(), bug.url(),
-                    'Dissociate this bug', self._dissociateBug.bind(self, bug));
-            }));
</del><ins>+        this.content('results-pane').style.display = this._task ? null : 'none';
+        this.part('results-pane').setShowForm(!!this._triggerable);
</ins><span class="cx"> 
</span><del>-            this._causeList.setList(this._task.causes().map(this._makeCommitListItem.bind(this)));
-            this._fixList.setList(this._task.fixes().map(this._makeCommitListItem.bind(this)));
-
-            this._taskStatusControl.value = this._task.changeType() || 'unconfirmed';
-        }
-
-        var repositoryList;
-        if (this._startPoint) {
-            var commitSet = this._startPoint.commitSet();
-            repositoryList = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());
-        } else
-            repositoryList = Repository.sortByNamePreferringOnesWithURL(Repository.all());
-
-        this._bugList.enqueueToRender();
-
-        this._causeList.setKindList(repositoryList);
-        this._causeList.enqueueToRender();
-
-        this._fixList.setKindList(repositoryList);
-        this._fixList.enqueueToRender();
-
-        this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
-        this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
-        this.content().querySelector('.test-group-view').style.display = this._task &amp;&amp; this._testGroups &amp;&amp; this._testGroups.length ? null : 'none';
-        this._taskNameLabel.enqueueToRender();
-
-        if (this._relatedTasks &amp;&amp; this._task) {
-            var router = this.router();
-            var link = ComponentBase.createLink;
-            var thisTask = this._task;
-            this.renderReplace(this.content().querySelector('.related-tasks-list'),
-                this._relatedTasks.map(function (otherTask) {
-                    console.assert(otherTask.metric() == thisTask.metric());
-                    var suffix = '';
-                    var taskLabel = otherTask.label();
-                    if (otherTask.platform() != thisTask.platform() &amp;&amp; taskLabel.indexOf(otherTask.platform().label()) &lt; 0)
-                        suffix = ` on ${otherTask.platform().label()}`;
-                    return element('li', [link(taskLabel, router.url(`analysis/task/${otherTask.id()}`)), suffix]);
-                }));
-        }
-
-        var selectedRange = this._analysisResultsViewer.selectedRange();
-        var a = selectedRange['A'];
-        var b = selectedRange['B'];
-        this._newTestGroupFormForViewer.setCommitSetMap(a &amp;&amp; b ? {'A': a.commitSet(), 'B': b.commitSet()} : null);
-        this._newTestGroupFormForViewer.enqueueToRender();
-        this._newTestGroupFormForViewer.element().style.display = this._triggerable ? null : 'none';
-
-        this._renderTestGroupList();
-        this._renderTestGroupDetails();
-
-        if (!this._renderedCurrentTestGroup &amp;&amp; !this._selectionWasModifiedByUser &amp;&amp; this._startPoint &amp;&amp; this._endPoint)
-            this._chartPane.setMainSelection([this._startPoint.time, this._endPoint.time]);
-
-        var points = this._chartPane.selectedPoints();
-        this._newTestGroupFormForChart.setCommitSetMap(points &amp;&amp; points.length() &gt;= 2 ?
-                {'A': points.firstPoint().commitSet(), 'B': points.lastPoint().commitSet()} : null);
-        this._newTestGroupFormForChart.enqueueToRender();
-        this._newTestGroupFormForChart.element().style.display = this._triggerable ? null : 'none';
-
-        this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
-        this._analysisResultsViewer.enqueueToRender();
-
-        this._testGroupResultsTable.enqueueToRender();
-
</del><span class="cx">         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _makeCommitListItem(commit)
</del><ins>+    _renderTaskNameAndStatus(task, taskName, changeType)
</ins><span class="cx">     {
</span><del>-        return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
-            'Disassociate this commit', this._dissociateCommit.bind(this, commit));
-    }
-
-    _renderTestGroupList()
-    {
-        var element = ComponentBase.createElement;
-        var link = ComponentBase.createLink;
-        if (this._testGroups != this._renderedTestGroups) {
-            this._renderedTestGroups = this._testGroups;
-            this._testGroupLabelMap.clear();
-
-            var unhiddenTestGroups = this._filteredTestGroups.filter(function (group) { return !group.isHidden(); });
-            var hiddenTestGroups = this._filteredTestGroups.filter(function (group) { return group.isHidden(); });
-
-            var listItems = [];
-            for (var group of hiddenTestGroups)
-                listItems.unshift(this._createTestGroupListItem(group));
-            for (var group of unhiddenTestGroups)
-                listItems.unshift(this._createTestGroupListItem(group));
-
-            if (this._testGroups.length != this._filteredTestGroups.length) {
-                listItems.push(element('li', {class: 'test-group-list-show-all'},
-                    link('Show hidden tests', this._showAllTestGroups.bind(this))));
-            }
-
-            this.renderReplace(this.content().querySelector('.test-group-list'), listItems);
-
-            this._renderedCurrentTestGroup = null;
</del><ins>+        this.part('analysis-task-name').setText(taskName);
+        if (task) {
+            const link = ComponentBase.createLink;
+            const platform = task.platform();
+            const metric = task.metric();
+            const subtitle = `${metric.fullName()} on ${platform.label()}`;
+            this.renderReplace(this.content('platform-metric-names'), 
+                link(subtitle, this.router().url('charts', ChartsPage.createStateForAnalysisTask(task))));
</ins><span class="cx">         }
</span><del>-
-        if (this._testGroups) {
-            for (var testGroup of this._filteredTestGroups) {
-                var label = this._testGroupLabelMap.get(testGroup);
-                label.setText(testGroup.label());
-                label.enqueueToRender();
-            }
-        }
</del><ins>+        this.content('change-type').value = changeType || 'unconfirmed';
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _createTestGroupListItem(group)
</del><ins>+    _renderRelatedTasks(task, relatedTasks)
</ins><span class="cx">     {
</span><del>-        var text = new EditableText(group.label());
-        text.listenToAction('update', () =&gt; this._updateTestGroupName(group));
-
-        this._testGroupLabelMap.set(group, text);
-        return ComponentBase.createElement('li', {class: 'test-group-list-' + group.id()},
-            ComponentBase.createLink(text, group.label(), this._showTestGroup.bind(this, group)));
</del><ins>+        const element = ComponentBase.createElement;
+        const link = ComponentBase.createLink;
+        this.renderReplace(this.content('related-tasks-list'), (task &amp;&amp; relatedTasks ? relatedTasks : []).map((otherTask) =&gt; {
+                let suffix = '';
+                const taskLabel = otherTask.label();
+                if (otherTask.metric() != task.metric() &amp;&amp; taskLabel.indexOf(otherTask.metric().label()) &lt; 0)
+                    suffix += ` with &quot;${otherTask.metric().label()}&quot;`;
+                if (otherTask.platform() != task.platform() &amp;&amp; taskLabel.indexOf(otherTask.platform().label()) &lt; 0)
+                    suffix += ` on ${otherTask.platform().label()}`;
+                return element('li', [link(taskLabel, this.router().url(`analysis/task/${otherTask.id()}`)), suffix]);
+            }));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _renderTestGroupDetails()
</del><ins>+    _renderCauseAndFixes(startPoint, task)
</ins><span class="cx">     {
</span><del>-        if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
-            if (this._renderedCurrentTestGroup) {
-                var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
-                if (element)
-                    element.classList.remove('selected');
-            }
-            if (this._currentTestGroup) {
-                var element = this.content().querySelector('.test-group-list-' + this._currentTestGroup.id());
-                if (element)
-                    element.classList.add('selected');
-            }
</del><ins>+        const hasData = startPoint &amp;&amp; task;
+        this.content('cause-fix').style.display = hasData ? null : 'none';
+        if (!hasData)
+            return;
</ins><span class="cx"> 
</span><del>-            this._chartPane.setMainSelection(null);
-            if (this._currentTestGroup) {
-                const commitSetsInTestGroup = this._currentTestGroup.requestedCommitSets();
-                const startTime = commitSetsInTestGroup[0].latestCommitTime();
-                const endTime = commitSetsInTestGroup[commitSetsInTestGroup.length - 1].latestCommitTime();
-                if (startTime != endTime)
-                    this._chartPane.setMainSelection([startTime, endTime]);
-            }
</del><ins>+        const commitSet = startPoint.commitSet();
+        const repositoryList = Repository.sortByNamePreferringOnesWithURL(commitSet.repositories());
</ins><span class="cx"> 
</span><del>-            this._retryForm.setLabel('Retry');
-            if (this._currentTestGroup)
-                this._retryForm.setRepetitionCount(this._currentTestGroup.repetitionCount());
-            this._retryForm.element().style.display = this._currentTestGroup ? null : 'none';
</del><ins>+        const makeItem = (commit) =&gt; {
+            return new MutableListItem(commit.repository(), commit.label(), commit.title(), commit.url(),
+                'Disassociate this commit', this._dissociateCommit.bind(this, commit));
+        }
</ins><span class="cx"> 
</span><del>-            this.content().querySelector('.test-group-hide-button').textContent
-                = this._currentTestGroup &amp;&amp; this._currentTestGroup.isHidden() ? 'Unhide' : 'Hide';
</del><ins>+        const causeList = this.part('cause-list');
+        causeList.setKindList(repositoryList);
+        causeList.setList(task.causes().map((commit) =&gt; makeItem(commit)));
</ins><span class="cx"> 
</span><del>-            this.content().querySelector('.pending-request-cancel-warning').style.display
-                = this._currentTestGroup &amp;&amp; this._currentTestGroup.hasPending() ? null : 'none';
-
-            this._renderedCurrentTestGroup = this._currentTestGroup;
-        }
-        this._retryForm.enqueueToRender();
</del><ins>+        const fixList = this.part('fix-list');
+        fixList.setKindList(repositoryList);
+        fixList.setList(task.fixes().map((commit) =&gt; makeItem(commit)));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _showTestGroup(testGroup)
</span><span class="cx">     {
</span><del>-        this._currentTestGroup = testGroup;        
-        this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
</del><ins>+        this._currentTestGroup = testGroup;
+        this.part('results-pane').setTestGroups(this._filteredTestGroups, this._currentTestGroup);
+        const groupsInReverseChronology = this._filteredTestGroups.slice(0).reverse();
+        const showHiddenGroups = !this._testGroups.some((group) =&gt; group.isHidden()) || this._showHiddenTestGroups;
+        this.part('group-pane').setTestGroups(groupsInReverseChronology, this._currentTestGroup, showHiddenGroups);
</ins><span class="cx">         this.enqueueToRender();
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _updateTaskName()
</del><ins>+    _updateTaskName(newName)
</ins><span class="cx">     {
</span><span class="cx">         console.assert(this._task);
</span><del>-        this._taskNameLabel.enqueueToRender();
</del><span class="cx"> 
</span><del>-        return this._task.updateName(this._taskNameLabel.editedText()).then(() =&gt; {
</del><ins>+        return this._task.updateName(newName).then(() =&gt; {
</ins><span class="cx">             this.enqueueToRender();
</span><span class="cx">         }, (error) =&gt; {
</span><span class="cx">             this.enqueueToRender();
</span><span class="lines">@@ -420,12 +590,10 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _updateTestGroupName(testGroup)
</del><ins>+    _updateTestGroupName(testGroup, newName)
</ins><span class="cx">     {
</span><del>-        var label = this._testGroupLabelMap.get(testGroup);
-        label.enqueueToRender();
-
-        return testGroup.updateName(label.editedText()).then(() =&gt; {
</del><ins>+        return testGroup.updateName(newName).then(() =&gt; {
+            this._showTestGroup(this._currentTestGroup);
</ins><span class="cx">             this.enqueueToRender();
</span><span class="cx">         }, (error) =&gt; {
</span><span class="cx">             this.enqueueToRender();
</span><span class="lines">@@ -433,10 +601,9 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _hideCurrentTestGroup()
</del><ins>+    _hideCurrentTestGroup(testGroup)
</ins><span class="cx">     {
</span><del>-        console.assert(this._currentTestGroup);
-        return this._currentTestGroup.updateHiddenFlag(!this._currentTestGroup.isHidden()).then(() =&gt; {
</del><ins>+        return testGroup.updateHiddenFlag(!testGroup.isHidden()).then(() =&gt; {
</ins><span class="cx">             this._didUpdateTestGroupHiddenState();
</span><span class="cx">             this.enqueueToRender();
</span><span class="cx">         }, function (error) {
</span><span class="lines">@@ -451,7 +618,7 @@
</span><span class="cx">         event.preventDefault();
</span><span class="cx">         console.assert(this._task);
</span><span class="cx"> 
</span><del>-        var newChangeType = this._taskStatusControl.value;
</del><ins>+        let newChangeType = this.content('change-type').value;
</ins><span class="cx">         if (newChangeType == 'unconfirmed')
</span><span class="cx">             newChangeType = null;
</span><span class="cx"> 
</span><span class="lines">@@ -465,27 +632,6 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _associateBug(tracker, bugNumber)
-    {
-        console.assert(tracker instanceof BugTracker);
-        bugNumber = parseInt(bugNumber);
-
-        const updateRendering = () =&gt; { this.enqueueToRender(); };
-        return this._task.associateBug(tracker, bugNumber).then(updateRendering, (error) =&gt; {
-            updateRendering();
-            alert('Failed to associate the bug: ' + error);
-        });
-    }
-
-    _dissociateBug(bug)
-    {
-        const updateRendering = () =&gt; { this.enqueueToRender(); };
-        return this._task.dissociateBug(bug).then(updateRendering, (error) =&gt; {
-            updateRendering();
-            alert('Failed to dissociate the bug: ' + error);
-        });
-    }
-
</del><span class="cx">     _associateCommit(kind, repository, revision)
</span><span class="cx">     {
</span><span class="cx">         const updateRendering = () =&gt; { this.enqueueToRender(); };
</span><span class="lines">@@ -509,10 +655,8 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _retryCurrentTestGroup(repetitionCount)
</del><ins>+    _retryCurrentTestGroup(testGroup, repetitionCount)
</ins><span class="cx">     {
</span><del>-        console.assert(this._currentTestGroup);
-        const testGroup = this._currentTestGroup;
</del><span class="cx">         const newName = this._createRetryNameForTestGroup(testGroup.name());
</span><span class="cx">         const commitSetList = testGroup.requestedCommitSets();
</span><span class="cx"> 
</span><span class="lines">@@ -523,27 +667,6 @@
</span><span class="cx">         return this._createTestGroupAfterVerifyingCommitSetList(newName, repetitionCount, commitSetMap);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _chartSelectionDidChange()
-    {
-        this._selectionWasModifiedByUser = true;
-        this.enqueueToRender();
-    }
-
-    _createNewTestGroupFromChart(name, repetitionCount, commitSetMap)
-    {
-        return this._createTestGroupAfterVerifyingCommitSetList(name, repetitionCount, commitSetMap);
-    }
-
-    _selectedRowInAnalysisResultsViewer()
-    {
-        this.enqueueToRender();
-    }
-
-    _createNewTestGroupFromViewer(name, repetitionCount, commitSetMap)
-    {
-        return this._createTestGroupAfterVerifyingCommitSetList(name, repetitionCount, commitSetMap);
-    }
-
</del><span class="cx">     _createTestGroupAfterVerifyingCommitSetList(testGroupName, repetitionCount, commitSetMap)
</span><span class="cx">     {
</span><span class="cx">         if (this._hasDuplicateTestGroupName(testGroupName)) {
</span><span class="lines">@@ -608,14 +731,14 @@
</span><span class="cx">     {
</span><span class="cx">         return `
</span><span class="cx">             &lt;div class=&quot;analysis-task-page&quot;&gt;
</span><del>-                &lt;h2 class=&quot;analysis-task-name&quot;&gt;&lt;editable-text&gt;&lt;/editable-text&gt;&lt;/h2&gt;
-                &lt;h3 class=&quot;platform-metric-names&quot;&gt;&lt;a href=&quot;&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
</del><ins>+                &lt;h2 class=&quot;analysis-task-name&quot;&gt;&lt;editable-text id=&quot;analysis-task-name&quot;&gt;&lt;/editable-text&gt;&lt;/h2&gt;
+                &lt;h3 id=&quot;platform-metric-names&quot;&gt;&lt;/h3&gt;
</ins><span class="cx">                 &lt;p class=&quot;error-message&quot;&gt;&lt;/p&gt;
</span><span class="cx">                 &lt;div class=&quot;analysis-task-status&quot;&gt;
</span><span class="cx">                     &lt;section&gt;
</span><span class="cx">                         &lt;h3&gt;Status&lt;/h3&gt;
</span><del>-                        &lt;form class=&quot;change-type-form&quot;&gt;
-                            &lt;select&gt;
</del><ins>+                        &lt;form id=&quot;change-type-form&quot;&gt;
+                            &lt;select id=&quot;change-type&quot;&gt;
</ins><span class="cx">                                 &lt;option value=&quot;unconfirmed&quot;&gt;Unconfirmed&lt;/option&gt;
</span><span class="cx">                                 &lt;option value=&quot;regression&quot;&gt;Definite regression&lt;/option&gt;
</span><span class="cx">                                 &lt;option value=&quot;progression&quot;&gt;Definite progression&lt;/option&gt;
</span><span class="lines">@@ -627,36 +750,22 @@
</span><span class="cx">                     &lt;/section&gt;
</span><span class="cx">                     &lt;section class=&quot;associated-bugs&quot;&gt;
</span><span class="cx">                         &lt;h3&gt;Associated Bugs&lt;/h3&gt;
</span><del>-                        &lt;mutable-list-view&gt;&lt;/mutable-list-view&gt;
</del><ins>+                        &lt;analysis-task-bug-list id=&quot;bug-list&quot;&gt;&lt;/analysis-task-bug-list&gt;
</ins><span class="cx">                     &lt;/section&gt;
</span><del>-                    &lt;section class=&quot;cause-fix&quot;&gt;
</del><ins>+                    &lt;section id=&quot;cause-fix&quot;&gt;
</ins><span class="cx">                         &lt;h3&gt;Caused by&lt;/h3&gt;
</span><del>-                        &lt;span class=&quot;cause-list&quot;&gt;&lt;mutable-list-view&gt;&lt;/mutable-list-view&gt;&lt;/span&gt;
</del><ins>+                        &lt;mutable-list-view id=&quot;cause-list&quot;&gt;&lt;/mutable-list-view&gt;
</ins><span class="cx">                         &lt;h3&gt;Fixed by&lt;/h3&gt;
</span><del>-                        &lt;span class=&quot;fix-list&quot;&gt;&lt;mutable-list-view&gt;&lt;/mutable-list-view&gt;&lt;/span&gt;
</del><ins>+                        &lt;mutable-list-view id=&quot;fix-list&quot;&gt;&lt;/mutable-list-view&gt;
</ins><span class="cx">                     &lt;/section&gt;
</span><span class="cx">                     &lt;section class=&quot;related-tasks&quot;&gt;
</span><span class="cx">                         &lt;h3&gt;Related Tasks&lt;/h3&gt;
</span><del>-                        &lt;ul class=&quot;related-tasks-list&quot;&gt;&lt;/ul&gt;
</del><ins>+                        &lt;ul id=&quot;related-tasks-list&quot;&gt;&lt;/ul&gt;
</ins><span class="cx">                     &lt;/section&gt;
</span><span class="cx">                 &lt;/div&gt;
</span><del>-                &lt;section class=&quot;overview-chart&quot;&gt;
-                    &lt;analysis-task-chart-pane&gt;&lt;/analysis-task-chart-pane&gt;
-                    &lt;div class=&quot;new-test-group-form&quot;&gt;&lt;customizable-test-group-form&gt;&lt;/customizable-test-group-form&gt;&lt;/div&gt;
-                &lt;/section&gt;
-                &lt;section class=&quot;analysis-results-view&quot;&gt;
-                    &lt;analysis-results-viewer&gt;&lt;/analysis-results-viewer&gt;
-                    &lt;div class=&quot;new-test-group-form&quot;&gt;&lt;customizable-test-group-form&gt;&lt;/customizable-test-group-form&gt;&lt;/div&gt;
-                &lt;/section&gt;
-                &lt;section class=&quot;test-group-view&quot;&gt;
-                    &lt;ul class=&quot;test-group-list&quot;&gt;&lt;/ul&gt;
-                    &lt;div class=&quot;test-group-details&quot;&gt;
-                        &lt;test-group-results-table&gt;&lt;/test-group-results-table&gt;
-                        &lt;div class=&quot;test-group-retry-form&quot;&gt;&lt;test-group-form&gt;&lt;/test-group-form&gt;&lt;/div&gt;
-                        &lt;button class=&quot;test-group-hide-button&quot;&gt;Hide&lt;/button&gt;
-                        &lt;span class=&quot;pending-request-cancel-warning&quot;&gt;(cancels pending requests)&lt;/span&gt;
-                    &lt;/div&gt;
-                &lt;/section&gt;
</del><ins>+                &lt;analysis-task-chart-pane id=&quot;chart-pane&quot;&gt;&lt;/analysis-task-chart-pane&gt;
+                &lt;analysis-task-results-pane id=&quot;results-pane&quot;&gt;&lt;/analysis-task-results-pane&gt;
+                &lt;analysis-task-test-group-pane id=&quot;group-pane&quot;&gt;&lt;/analysis-task-test-group-pane&gt;
</ins><span class="cx">             &lt;/div&gt;
</span><span class="cx"> `;
</span><span class="cx">     }
</span><span class="lines">@@ -675,7 +784,7 @@
</span><span class="cx">                 padding: 0;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .platform-metric-names {
</del><ins>+            #platform-metric-names {
</ins><span class="cx">                 font-size: 1rem;
</span><span class="cx">                 font-weight: inherit;
</span><span class="cx">                 color: #c93;
</span><span class="lines">@@ -683,24 +792,40 @@
</span><span class="cx">                 padding: 0;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .platform-metric-names a {
</del><ins>+            #platform-metric-names a {
</ins><span class="cx">                 text-decoration: none;
</span><span class="cx">                 color: inherit;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .platform-metric-names:empty {
-                margin: 0;
</del><ins>+            #platform-metric-names:empty {
+                display: none;
</ins><span class="cx">             }
</span><span class="cx"> 
</span><ins>+            .error-message:empty {
+                display: none;
+            }
+
</ins><span class="cx">             .error-message:not(:empty) {
</span><span class="cx">                 margin: 1rem;
</span><span class="cx">                 padding: 0;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-            .overview-chart {
-                margin: 0 1rem;
</del><ins>+            #chart-pane,
+            #results-pane {
+                display: block;
+                padding: 0 1rem;
+                border-bottom: solid 1px #ccc;
</ins><span class="cx">             }
</span><ins>+            
+            #results-pane {
+                margin-top: 1rem;
+            }
</ins><span class="cx"> 
</span><ins>+            #group-pane {
+                margin: 1rem;
+                margin-bottom: 2rem;
+            }
+
</ins><span class="cx">             .analysis-task-status {
</span><span class="cx">                 margin: 0;
</span><span class="cx">                 display: flex;
</span><span class="lines">@@ -743,14 +868,6 @@
</span><span class="cx">                 overflow-y: scroll;
</span><span class="cx">             }
</span><span class="cx"> 
</span><del>-
-            .analysis-results-view {
-                border-top: solid 1px #ccc;
-                border-bottom: solid 1px #ccc;
-                margin: 1rem 0;
-                padding: 1rem;
-            }
-
</del><span class="cx">             .test-configuration h3 {
</span><span class="cx">                 font-size: 1rem;
</span><span class="cx">                 font-weight: inherit;
</span><span class="lines">@@ -757,79 +874,6 @@
</span><span class="cx">                 color: inherit;
</span><span class="cx">                 margin: 0 1rem;
</span><span class="cx">                 padding: 0;
</span><del>-            }
-
-            .test-group-view {
-                display: table;
-                margin: 0 1rem;
-                margin-bottom: 2rem;
-            }
-
-            .test-group-details {
-                display: table-cell;
-                margin-bottom: 1rem;
-                padding: 0;
-                margin: 0;
-            }
-
-            .new-test-group-form,
-            .test-group-retry-form {
-                padding: 0;
-                margin: 0.5rem;
-            }
-
-            .test-group-hide-button {
-                margin: 0.5rem;
-            }
-
-            .test-group-list {
-                display: table-cell;
-                margin: 0;
-                padding: 0.2rem 0;
-                list-style: none;
-                border-right: solid 1px #ccc;
-                white-space: nowrap;
-                min-width: 8rem;
-            }
-
-            .test-group-list:empty {
-                margin: 0;
-                padding: 0;
-                border-right: none;
-            }
-
-            .test-group-list &gt; li {
-                display: block;
-                font-size: 0.9rem;
-            }
-
-            .test-group-list &gt; li &gt; a {
-                display: block;
-                color: inherit;
-                text-decoration: none;
-                margin: 0;
-                padding: 0.2rem;
-            }
-            
-            .test-group-list &gt; li.test-group-list-show-all {
-                font-size: 0.8rem;
-                margin-top: 0.5rem;
-                padding-right: 1rem;
-                text-align: center;
-                color: #999;
-            }
-
-            .test-group-list &gt; li.test-group-list-show-all:not(.selected) a:hover {
-                background: inherit;
-            }
-
-            .test-group-list &gt; li.selected &gt; a {
-                background: rgba(204, 153, 51, 0.1);
-            }
-
-            .test-group-list &gt; li:not(.selected) &gt; a:hover {
-                background: #eee;
-            }
-`;
</del><ins>+            }`;
</ins><span class="cx">     }
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunitteststestgroupstestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js (214501 => 214502)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js        2017-03-28 22:48:26 UTC (rev 214501)
+++ trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js        2017-03-28 23:00:36 UTC (rev 214502)
</span><span class="lines">@@ -142,7 +142,6 @@
</span><span class="cx">             assert.ok(buildRequests[0].isPending());
</span><span class="cx">             assert.equal(buildRequests[0].statusLabel(), 'Waiting');
</span><span class="cx">             assert.equal(buildRequests[0].buildId(), null);
</span><del>-            assert.equal(buildRequests[0].result(), null);
</del><span class="cx"> 
</span><span class="cx">             assert.equal(buildRequests[1].id(), 16986);
</span><span class="cx">             assert.equal(buildRequests[1].order(), 1);
</span><span class="lines">@@ -151,7 +150,6 @@
</span><span class="cx">             assert.ok(buildRequests[1].isPending());
</span><span class="cx">             assert.equal(buildRequests[1].statusLabel(), 'Waiting');
</span><span class="cx">             assert.equal(buildRequests[1].buildId(), null);
</span><del>-            assert.equal(buildRequests[1].result(), null);
</del><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         it('should create root sets for each group', function () {
</span></span></pre>
</div>
</div>

</body>
</html>