<!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>[196768] 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/196768">196768</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-02-18 12:07:54 -0800 (Thu, 18 Feb 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>v3 UI should allow custom revisions for A/B testing
https://bugs.webkit.org/show_bug.cgi?id=154379

Reviewed by Chris Dumez.

Added the capability to customize revisions selected in the overview chart and the results viewer.

Newly added CustomizableTestGroupForm is responsible for allowing users to modify the set of revisions in
a new A/B testing group. Unlike TestGroupForm which doesn't know anything about which revisions are selected
for each project/repository, CustomizableTestGroupForm is aware of the list of revisions used in each set.

The list of revisions used in each set is represented by RootSet if users had not customized them, and
CustomRootSet otherwise; the latter was added since regular RootSet object requires CommitLog and other
DataModelObjects which are hard to create without corresponding database entries.

* public/v3/components/customizable-test-group-form.js: Added.
(CustomizableTestGroupForm): Added.
(CustomizableTestGroupForm.prototype.setRootSetMap): Added.
(CustomizableTestGroupForm.prototype._submitted): Overrides the superclass' method.
(CustomizableTestGroupForm.prototype._customize): Ditto. Unlike TestGroupForm's callback, this class'
callback passes in a root set map as the third argument.
(CustomizableTestGroupForm.prototype._computeRootSetMap): Added. Returns this._rootSetMap, which is set by
AnalysisTaskPage if user had not customized the root sets. Otherwise return a new map with CustomRootSet's.
(CustomizableTestGroupForm.prototype.render): Added. Creates a table to allow customization of root sets.
(CustomizableTestGroupForm._constructRevisionRadioButtons): Added.
(CustomizableTestGroupForm._createRadioButton): Added.
(CustomizableTestGroupForm.cssTemplate): Added.
(CustomizableTestGroupForm.formContent): Added. This method is called by TestGroupForm.htmlTemplate.
* public/v3/components/test-group-form.js:
(TestGroupForm): Updated the various methods to not directly mutate DOM. Store the state in instance
variables and update DOM in render() as done elsewhere.
(TestGroupForm.prototype.setNeedsName): Deleted. We no longer need this flag since TestGroupForm which is
used for retries never needs a name and CustomizableTestGroupForm which is used to create a new test group
always requires a name.
(TestGroupForm.prototype.setDisabled):
(TestGroupForm.prototype.setLabel):
(TestGroupForm.prototype.setRepetitionCount):
(TestGroupForm.prototype.render): Added.
(TestGroupForm.prototype._submitted): Moved the code to prevent the default action has been moved to the
constructor since this method is overridden by CustomizableTestGroupForm.
(TestGroupForm.cssTemplate): Added.
(TestGroupForm.htmlTemplate):
(TestGroupForm.formContent): Extracted from htmlTemplate.
* public/v3/index.html:
* public/v3/models/repository.js:
(Repository.sortByNamePreferringOnesWithURL): Added.
* public/v3/models/root-set.js:
(RootSet.prototype.revisionForRepository): Added so that _createTestGroupAfterVerifyingRootSetList can retrieve
the revision information from CustomRootSet without going through CommitLog objects since CustomRootSet doesn't
have associated CommitLog objects.
(CustomRootSet): Added. Used by CustomizableTestGroupForm to create a custom root map since regular RootSet
requires CommitLog and other related objects which are hard to create without database entries.
(CustomRootSet.prototype.setRevisionForRepository): Added.
(CustomRootSet.prototype.repositories): Added.
(CustomRootSet.prototype.revisionForRepository): Added.
* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage):
(AnalysisTaskPage.prototype.render): Removed the reference to v2 UI since v3 UI is now strictly more powerful
than v2 UI. Also update the root set maps in each form here.
(AnalysisTaskPage.prototype._retryCurrentTestGroup): No longer takes unused name argument as it got removed
from TestGroupForm.
(AnalysisTaskPage.prototype._chartSelectionDidChange): No longer updates the disabled-ness here since it's now
done in render() via setRootSetMap().
(AnalysisTaskPage.prototype._createNewTestGroupFromChart): Now takes rootSetMap as an argument.
(AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): No longer updates the disabled-ness here
since it's now done in render() via setRootSetMap().
(AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Now takes rootSetMap as an argument.
(AnalysisTaskPage.prototype._createTestGroupAfterVerifyingRootSetList): Take a dictionary of root set labels
such as A and B, which maps to a RootSet or a newly-added CustomRootSet.
(AnalysisTaskPage.htmlTemplate): Use customizable-test-group-form for creating a new A/B testing group. Retry
form will continue to use TestGroupForm since customizing revisions is non-sensical in retries.
(AnalysisTaskPage.cssTemplate): Updated the style.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstestgroupformjs">trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsrepositoryjs">trunk/Websites/perf.webkit.org/public/v3/models/repository.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsrootsetjs">trunk/Websites/perf.webkit.org/public/v3/models/root-set.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentscustomizabletestgroupformjs">trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.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 (196767 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-02-18 19:06:50 UTC (rev 196767)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -1,3 +1,78 @@
</span><ins>+2016-02-17  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        v3 UI should allow custom revisions for A/B testing
+        https://bugs.webkit.org/show_bug.cgi?id=154379
+
+        Reviewed by Chris Dumez.
+
+        Added the capability to customize revisions selected in the overview chart and the results viewer.
+
+        Newly added CustomizableTestGroupForm is responsible for allowing users to modify the set of revisions in
+        a new A/B testing group. Unlike TestGroupForm which doesn't know anything about which revisions are selected
+        for each project/repository, CustomizableTestGroupForm is aware of the list of revisions used in each set.
+
+        The list of revisions used in each set is represented by RootSet if users had not customized them, and
+        CustomRootSet otherwise; the latter was added since regular RootSet object requires CommitLog and other
+        DataModelObjects which are hard to create without corresponding database entries.
+
+        * public/v3/components/customizable-test-group-form.js: Added.
+        (CustomizableTestGroupForm): Added.
+        (CustomizableTestGroupForm.prototype.setRootSetMap): Added.
+        (CustomizableTestGroupForm.prototype._submitted): Overrides the superclass' method.
+        (CustomizableTestGroupForm.prototype._customize): Ditto. Unlike TestGroupForm's callback, this class'
+        callback passes in a root set map as the third argument.
+        (CustomizableTestGroupForm.prototype._computeRootSetMap): Added. Returns this._rootSetMap, which is set by
+        AnalysisTaskPage if user had not customized the root sets. Otherwise return a new map with CustomRootSet's.
+        (CustomizableTestGroupForm.prototype.render): Added. Creates a table to allow customization of root sets.
+        (CustomizableTestGroupForm._constructRevisionRadioButtons): Added.
+        (CustomizableTestGroupForm._createRadioButton): Added.
+        (CustomizableTestGroupForm.cssTemplate): Added.
+        (CustomizableTestGroupForm.formContent): Added. This method is called by TestGroupForm.htmlTemplate.
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm): Updated the various methods to not directly mutate DOM. Store the state in instance
+        variables and update DOM in render() as done elsewhere.
+        (TestGroupForm.prototype.setNeedsName): Deleted. We no longer need this flag since TestGroupForm which is
+        used for retries never needs a name and CustomizableTestGroupForm which is used to create a new test group
+        always requires a name.
+        (TestGroupForm.prototype.setDisabled):
+        (TestGroupForm.prototype.setLabel):
+        (TestGroupForm.prototype.setRepetitionCount):
+        (TestGroupForm.prototype.render): Added.
+        (TestGroupForm.prototype._submitted): Moved the code to prevent the default action has been moved to the
+        constructor since this method is overridden by CustomizableTestGroupForm.
+        (TestGroupForm.cssTemplate): Added.
+        (TestGroupForm.htmlTemplate):
+        (TestGroupForm.formContent): Extracted from htmlTemplate.
+        * public/v3/index.html:
+        * public/v3/models/repository.js:
+        (Repository.sortByNamePreferringOnesWithURL): Added.
+        * public/v3/models/root-set.js:
+        (RootSet.prototype.revisionForRepository): Added so that _createTestGroupAfterVerifyingRootSetList can retrieve
+        the revision information from CustomRootSet without going through CommitLog objects since CustomRootSet doesn't
+        have associated CommitLog objects.
+        (CustomRootSet): Added. Used by CustomizableTestGroupForm to create a custom root map since regular RootSet
+        requires CommitLog and other related objects which are hard to create without database entries.
+        (CustomRootSet.prototype.setRevisionForRepository): Added.
+        (CustomRootSet.prototype.repositories): Added.
+        (CustomRootSet.prototype.revisionForRepository): Added.
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage):
+        (AnalysisTaskPage.prototype.render): Removed the reference to v2 UI since v3 UI is now strictly more powerful
+        than v2 UI. Also update the root set maps in each form here.
+        (AnalysisTaskPage.prototype._retryCurrentTestGroup): No longer takes unused name argument as it got removed
+        from TestGroupForm.
+        (AnalysisTaskPage.prototype._chartSelectionDidChange): No longer updates the disabled-ness here since it's now
+        done in render() via setRootSetMap().
+        (AnalysisTaskPage.prototype._createNewTestGroupFromChart): Now takes rootSetMap as an argument.
+        (AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): No longer updates the disabled-ness here
+        since it's now done in render() via setRootSetMap().
+        (AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Now takes rootSetMap as an argument.
+        (AnalysisTaskPage.prototype._createTestGroupAfterVerifyingRootSetList): Take a dictionary of root set labels
+        such as A and B, which maps to a RootSet or a newly-added CustomRootSet.
+        (AnalysisTaskPage.htmlTemplate): Use customizable-test-group-form for creating a new A/B testing group. Retry
+        form will continue to use TestGroupForm since customizing revisions is non-sensical in retries.
+        (AnalysisTaskPage.cssTemplate): Updated the style.
+
</ins><span class="cx"> 2016-02-16  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         v3 UI has the capability to schedule an A/B testing in a specific range
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentscustomizabletestgroupformjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js (0 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -0,0 +1,174 @@
</span><ins>+
+class CustomizableTestGroupForm extends TestGroupForm {
+
+    constructor()
+    {
+        super('customizable-test-group-form');
+        this._rootSetMap = null;
+        this._disabled = true;
+        this._renderedRepositorylist = null;
+        this._customized = false;
+        this.content().querySelector('a').onclick = this._customize.bind(this);
+    }
+
+    setRootSetMap(map)
+    {
+        this._rootSetMap = map;
+        this._customized = false;
+        this.setDisabled(!map);
+    }
+
+    _submitted()
+    {
+        if (this._startCallback)
+            this._startCallback(this.content().querySelector('.name').value, this._repetitionCount, this._computeRootSetMap());
+    }
+
+    _customize(event)
+    {
+        event.preventDefault();
+        this._customized = true;
+        this.render();
+    }
+
+    _computeRootSetMap()
+    {
+        console.assert(this._rootSetMap);
+        if (!this._customized)
+            return this._rootSetMap;
+
+        console.assert(this._renderedRepositorylist);
+        var map = {};
+        for (var label in this._rootSetMap) {
+            var customRootSet = new CustomRootSet;
+            for (var repository of this._renderedRepositorylist) {
+                var id = CustomizableTestGroupForm._idForLabelAndRepository(label, repository);
+                var revision = this.content().getElementById(id).value;
+                console.assert(revision);
+                if (revision)
+                    customRootSet.setRevisionForRepository(repository, revision);
+            }
+            map[label] = customRootSet;
+        }
+        return map;
+    }
+
+    render()
+    {
+        super.render();
+        this.content().querySelector('.customize-link').style.display = this._disabled ? 'none' : null;
+
+        if (!this._customized) {
+            this.renderReplace(this.content().querySelector('.custom-table-container'), []);
+            return;
+        }
+        var map = this._rootSetMap;
+        console.assert(map);
+
+        var repositorySet = new Set;
+        var rootSetLabels = [];
+        for (var label in map) {
+            for (var repository of map[label].repositories())
+                repositorySet.add(repository);
+            rootSetLabels.push(label);
+        }
+
+        this._renderedRepositorylist = Repository.sortByNamePreferringOnesWithURL(Array.from(repositorySet.values()));
+
+        var element = ComponentBase.createElement;
+        this.renderReplace(this.content().querySelector('.custom-table-container'),
+            element('table', {class: 'custom-table'}, [
+                element('thead',
+                    element('tr',
+                        [element('td', 'Repository'), rootSetLabels.map(function (label) {
+                            return element('td', {colspan: rootSetLabels.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);
+                    }))]));
+    }
+
+    static _idForLabelAndRepository(label, repository) { return label + '-' + repository.id(); }
+
+    static _constructRevisionRadioButtons(rootSetMap, repository, rowLabel)
+    {
+        var id = this._idForLabelAndRepository(rowLabel, repository);
+        var groupName = id + '-group';
+        var element = ComponentBase.createElement;
+        var revisionEditor = element('input', {id: id});
+
+        var nodes = [];
+        for (var labelToChoose in rootSetMap) {
+            var commit = rootSetMap[labelToChoose].commitForRepository(repository);
+            var checked = labelToChoose == rowLabel;
+            var radioButton = this._createRadioButton(groupName, revisionEditor, commit, checked);
+            if (checked)
+                revisionEditor.value = commit ? commit.revision() : '';
+            nodes.push(element('td', element('label', [radioButton, labelToChoose])));
+        }
+        nodes.push(element('td', revisionEditor));
+
+        return nodes;
+    }
+
+    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;
+    }
+
+    static cssTemplate()
+    {
+        return super.cssTemplate() + `
+            .customize-link {
+                color: #333;
+            }
+
+            .customize-link a {
+                color: inherit;
+            }
+
+            .custom-table {
+                margin: 1rem 0;
+            }
+
+            .custom-table,
+            .custom-table td,
+            .custom-table th {
+                font-weight: inherit;
+                border-collapse: collapse;
+                border-top: solid 1px #ddd;
+                border-bottom: solid 1px #ddd;
+                padding: 0.4rem 0.2rem;
+                font-size: 0.9rem;
+            }
+
+            .custom-table thead td,
+            .custom-table th {
+                text-align: center;
+            }
+            `;
+    }
+
+    static formContent()
+    {
+        return `
+            &lt;input class=&quot;name&quot; type=&quot;text&quot; placeholder=&quot;Test group name&quot;&gt;
+            ${super.formContent()}
+            &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;
+        `;
+    }
+}
+
+ComponentBase.defineElement('customizable-test-group-form', CustomizableTestGroupForm);
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentstestgroupformjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js (196767 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js        2016-02-18 19:06:50 UTC (rev 196767)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -1,52 +1,70 @@
</span><span class="cx"> 
</span><span class="cx"> class TestGroupForm extends ComponentBase {
</span><span class="cx"> 
</span><del>-    constructor()
</del><ins>+    constructor(name)
</ins><span class="cx">     {
</span><del>-        super('test-group-form');
</del><ins>+        super(name || 'test-group-form');
</ins><span class="cx">         this._startCallback = null;
</span><ins>+        this._disabled = false;
+        this._label = undefined;
+        this._repetitionCount = 4;
+
+        this._nameControl = this.content().querySelector('.name');
</ins><span class="cx">         this._repetitionCountControl = this.content().querySelector('.repetition-count');
</span><del>-        this._repetitionCountControl.value = 4;
-        this._buttonControl = this.content().querySelector('button');
-        this._nameControl = this.content().querySelector('.name');
-        this.content().querySelector('form').onsubmit = this._submitted.bind(this);
</del><ins>+        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();
+        }
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     setStartCallback(callback) { this._startCallback = callback; }
</span><del>-    setNeedsName(needsName) { this._nameControl.style.display = needsName ? null : 'none'; }
-    setDisabled(disabled) { this._buttonControl.disabled = disabled; }
</del><ins>+    setDisabled(disabled) { this._disabled = !!disabled; }
+    setLabel(label) { this._label = label; }
+    setRepetitionCount(count) { this._repetitionCount = count; }
</ins><span class="cx"> 
</span><del>-    setLabel(label) { this._buttonControl.textContent = label; }
-    setRepetitionCount(count) { this._repetitionCountControl.value = count; }
</del><ins>+    render()
+    {
+        var button = this.content().querySelector('button');
+        if (this._label)
+            button.textContent = this._label;
+        button.disabled = this._disabled;
+        this._repetitionCountControl.value = this._repetitionCount;
+    }
</ins><span class="cx"> 
</span><del>-    _submitted(event)
</del><ins>+    _submitted()
</ins><span class="cx">     {
</span><del>-        event.preventDefault();
</del><span class="cx">         if (this._startCallback)
</span><del>-            this._startCallback(this._nameControl.value, this._repetitionCountControl.value);
</del><ins>+            this._startCallback(this._repetitionCount);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><ins>+        return `&lt;form&gt;&lt;button type=&quot;submit&quot;&gt;Start A/B testing&lt;/button&gt;${this.formContent()}&lt;/form&gt;`;
+    }
+
+    static formContent()
+    {
</ins><span class="cx">         return `
</span><del>-            &lt;form&gt;
-                &lt;button type=&quot;submit&quot;&gt;Start A/B testing&lt;/button&gt;
-                &lt;input class=&quot;name&quot; type=&quot;text&quot;&gt;
-                with
-                &lt;select class=&quot;repetition-count&quot;&gt;
-                    &lt;option&gt;1&lt;/option&gt;
-                    &lt;option&gt;2&lt;/option&gt;
-                    &lt;option&gt;3&lt;/option&gt;
-                    &lt;option&gt;4&lt;/option&gt;
-                    &lt;option&gt;5&lt;/option&gt;
-                    &lt;option&gt;6&lt;/option&gt;
-                    &lt;option&gt;7&lt;/option&gt;
-                    &lt;option&gt;8&lt;/option&gt;
-                    &lt;option&gt;9&lt;/option&gt;
-                    &lt;option&gt;10&lt;/option&gt;
-                &lt;/select&gt;
-                iterations per set
-            &lt;/form&gt;
</del><ins>+            with
+            &lt;select class=&quot;repetition-count&quot;&gt;
+                &lt;option&gt;1&lt;/option&gt;
+                &lt;option&gt;2&lt;/option&gt;
+                &lt;option&gt;3&lt;/option&gt;
+                &lt;option&gt;4&lt;/option&gt;
+                &lt;option&gt;5&lt;/option&gt;
+                &lt;option&gt;6&lt;/option&gt;
+                &lt;option&gt;7&lt;/option&gt;
+                &lt;option&gt;8&lt;/option&gt;
+                &lt;option&gt;9&lt;/option&gt;
+                &lt;option&gt;10&lt;/option&gt;
+            &lt;/select&gt;
+            iterations per set
</ins><span class="cx">         `;
</span><span class="cx">     }
</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 (196767 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2016-02-18 19:06:50 UTC (rev 196767)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -76,6 +76,7 @@
</span><span class="cx">         &lt;script src=&quot;components/analysis-results-viewer.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/test-group-results-table.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;components/test-group-form.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;components/customizable-test-group-form.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;components/chart-styles.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;pages/page.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsrepositoryjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/repository.js (196767 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/repository.js        2016-02-18 19:06:50 UTC (rev 196767)
+++ trunk/Websites/perf.webkit.org/public/v3/models/repository.js        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -19,4 +19,20 @@
</span><span class="cx">     {
</span><span class="cx">         return (this._blameUrl || '').replace(/\$1/g, from).replace(/\$2/g, to);
</span><span class="cx">     }
</span><ins>+
+    static sortByNamePreferringOnesWithURL(repositories)
+    {
+        return repositories.sort(function (a, b) {
+            if (!!a._blameUrl == !!b._blameUrl) {
+                if (a.name() &gt; b.name())
+                    return 1;
+                else if (a.name() &lt; b.name())
+                    return -1;
+                return 0;
+            } else if (b._blameUrl) // a &gt; b
+                return 1;
+            return -1;
+        });
+    }
+
</ins><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsrootsetjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/root-set.js (196767 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/root-set.js        2016-02-18 19:06:50 UTC (rev 196767)
+++ trunk/Websites/perf.webkit.org/public/v3/models/root-set.js        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -24,6 +24,12 @@
</span><span class="cx">     repositories() { return this._repositories; }
</span><span class="cx">     commitForRepository(repository) { return this._repositoryToCommitMap[repository.id()]; }
</span><span class="cx"> 
</span><ins>+    revisionForRepository(repository)
+    {
+        var commit = this._repositoryToCommitMap[repository.id()];
+        return commit ? commit.revision() : null;
+    }
+
</ins><span class="cx">     latestCommitTime()
</span><span class="cx">     {
</span><span class="cx">         if (this._latestCommitTime == null) {
</span><span class="lines">@@ -81,3 +87,22 @@
</span><span class="cx">         return RootSet.findById(rootSetId) || (new MeasurementRootSet(rootSetId, revisionList));
</span><span class="cx">     }
</span><span class="cx"> }
</span><ins>+
+class CustomRootSet {
+
+    constructor()
+    {
+        this._revisionListByRepository = new Map;
+    }
+
+    setRevisionForRepository(repository, revision)
+    {
+        console.assert(repository instanceof Repository);
+        this._revisionListByRepository.set(repository, revision);
+    }
+
+    repositories() { return Array.from(this._revisionListByRepository.keys()); }
+    revisionForRepository(repository) { return this._revisionListByRepository.get(repository); }
+
+}
+
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (196767 => 196768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2016-02-18 19:06:50 UTC (rev 196767)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2016-02-18 20:07:54 UTC (rev 196768)
</span><span class="lines">@@ -63,16 +63,14 @@
</span><span class="cx">         this._bugTrackerControl = this.content().querySelector('.bug-tracker-control');
</span><span class="cx">         this._bugNumberControl = this.content().querySelector('.bug-number-control');
</span><span class="cx"> 
</span><del>-        this._newTestGroupFormForChart = this.content().querySelector('.overview-chart test-group-form').component();
</del><ins>+        this._newTestGroupFormForChart = this.content().querySelector('.overview-chart customizable-test-group-form').component();
</ins><span class="cx">         this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
</span><span class="cx"> 
</span><del>-        this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view test-group-form').component();
</del><ins>+        this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view customizable-test-group-form').component();
</ins><span class="cx">         this._newTestGroupFormForViewer.setStartCallback(this._createNewTestGroupFromViewer.bind(this));
</span><del>-        this._selectedRowInAnalysisResultsViewer();
</del><span class="cx"> 
</span><span class="cx">         this._retryForm = this.content().querySelector('.test-group-retry-form').firstChild.component();
</span><span class="cx">         this._retryForm.setStartCallback(this._retryCurrentTestGroup.bind(this));
</span><del>-        this._retryForm.setNeedsName(false);
</del><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     title() { return this._task ? this._task.label() : 'Analysis Task'; }
</span><span class="lines">@@ -198,12 +196,6 @@
</span><span class="cx"> 
</span><span class="cx">         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
</span><span class="cx"> 
</span><del>-        if (this._task) {
-            var v2URL = `/v2/#/analysis/task/${this._task.id()}`;
-            this.content().querySelector('.error-message').innerHTML =
-                `&lt;p&gt;To schedule a custom A/B testing, use &lt;a href=&quot;${v2URL}&quot;&gt;v2 UI&lt;/a&gt;.&lt;/p&gt;`;
-        }
-
</del><span class="cx">         this._chartPane.render();
</span><span class="cx"> 
</span><span class="cx">         var element = ComponentBase.createElement;
</span><span class="lines">@@ -255,14 +247,23 @@
</span><span class="cx">                 }));
</span><span class="cx">         }
</span><span class="cx"> 
</span><ins>+        var selectedRange = this._analysisResultsViewer.selectedRange();
+        var a = selectedRange['A'];
+        var b = selectedRange['B'];
+        this._newTestGroupFormForViewer.setRootSetMap(a &amp;&amp; b ? {'A': a.rootSet(), 'B': b.rootSet()} : null);
+        this._newTestGroupFormForViewer.render();
+
+        this._renderTestGroupList();
+        this._renderTestGroupDetails();
+
+        var points = this._chartPane.selectedPoints();
+        this._newTestGroupFormForChart.setRootSetMap(points &amp;&amp; points.length &gt;= 2 ?
+                {'A': points[0].rootSet(), 'B': points[points.length - 1].rootSet()} : null);
</ins><span class="cx">         this._newTestGroupFormForChart.render();
</span><span class="cx"> 
</span><span class="cx">         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
</span><span class="cx">         this._analysisResultsViewer.render();
</span><span class="cx"> 
</span><del>-        this._renderTestGroupList();
-        this._renderTestGroupDetails();
-
</del><span class="cx">         this._testGroupResultsTable.render();
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
</span><span class="lines">@@ -417,68 +418,70 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _retryCurrentTestGroup(unusedName, repetitionCount)
</del><ins>+    _retryCurrentTestGroup(repetitionCount)
</ins><span class="cx">     {
</span><span class="cx">         console.assert(this._currentTestGroup);
</span><span class="cx">         var testGroup = this._currentTestGroup;
</span><span class="cx">         var newName = this._createRetryNameForTestGroup(testGroup.name());
</span><span class="cx">         var rootSetList = testGroup.requestedRootSets();
</span><del>-        var rootSetLabels = rootSetList.map(function (rootSet) { return testGroup.labelForRootSet(rootSet); });
-        return this._createTestGroupAfterVerifyingRootSetList(newName, repetitionCount, rootSetList, rootSetLabels);
</del><ins>+
+        var rootSetMap = {};
+        for (var rootSet of rootSetList)
+            rootSetMap[testGroup.labelForRootSet(rootSet)] = rootSet;
+
+        return this._createTestGroupAfterVerifyingRootSetList(newName, repetitionCount, rootSetMap);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _chartSelectionDidChange()
</span><span class="cx">     {
</span><del>-        var points = this._chartPane.selectedPoints();
-        this._newTestGroupFormForChart.setDisabled(!points || points.length &lt; 2);
</del><ins>+        this.render();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _createNewTestGroupFromChart(name, repetitionCount)
</del><ins>+    _createNewTestGroupFromChart(name, repetitionCount, rootSetMap)
</ins><span class="cx">     {
</span><del>-        var points = this._chartPane.selectedPoints();
-        console.assert(points &amp;&amp; points.length &gt;= 2);
-        return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount,
-            [points[0].rootSet(), points[points.length - 1].rootSet()], ['A', 'B']);
</del><ins>+        return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _selectedRowInAnalysisResultsViewer()
</span><span class="cx">     {
</span><del>-        var selectedRange = this._analysisResultsViewer.selectedRange();
-        this._newTestGroupFormForViewer.setDisabled(!selectedRange['A'] || !selectedRange['B']);
</del><ins>+        this.render();
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _createNewTestGroupFromViewer(name, repetitionCount)
</del><ins>+    _createNewTestGroupFromViewer(name, repetitionCount, rootSetMap)
</ins><span class="cx">     {
</span><del>-        var selectedRange = this._analysisResultsViewer.selectedRange();
-        console.assert(selectedRange &amp;&amp; selectedRange['A'] &amp;&amp; selectedRange['B']);
-        return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount,
-            [selectedRange['A'].rootSet(), selectedRange['B'].rootSet()], ['A', 'B']);
</del><ins>+        return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount, rootSetMap);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _createTestGroupAfterVerifyingRootSetList(testGroupName, repetitionCount, rootSetList, rootSetLabels)
</del><ins>+    _createTestGroupAfterVerifyingRootSetList(testGroupName, repetitionCount, rootSetMap)
</ins><span class="cx">     {
</span><span class="cx">         if (this._hasDuplicateTestGroupName(testGroupName))
</span><span class="cx">             alert(`There is already a test group named &quot;${testGroupName}&quot;`);
</span><span class="cx"> 
</span><span class="cx">         var rootSetsByName = {};
</span><del>-        for (var repository of rootSetList[0].repositories())
-            rootSetsByName[repository.name()] = [];
</del><ins>+        var firstLabel;
+        for (firstLabel in rootSetMap) {
+            var rootSet = rootSetMap[firstLabel];
+            for (var repository of rootSet.repositories())
+                rootSetsByName[repository.name()] = [];
+            break;
+        }
</ins><span class="cx"> 
</span><span class="cx">         var setIndex = 0;
</span><del>-        for (var rootSet of rootSetList) {
</del><ins>+        for (var label in rootSetMap) {
+            var rootSet = rootSetMap[label];
</ins><span class="cx">             for (var repository of rootSet.repositories()) {
</span><span class="cx">                 var list = rootSetsByName[repository.name()];
</span><span class="cx">                 if (!list) {
</span><del>-                    alert(`Set ${rootSetLabels[setIndex]} specifies ${repository.label()} but set ${rootSetLabels[0]} does not.`);
</del><ins>+                    alert(`Set ${label} specifies ${repository.label()} but set ${firstLabel} does not.`);
</ins><span class="cx">                     return null;
</span><span class="cx">                 }
</span><del>-                list.push(rootSet.commitForRepository(repository).revision());
</del><ins>+                list.push(rootSet.revisionForRepository(repository));
</ins><span class="cx">             }
</span><span class="cx">             setIndex++;
</span><span class="cx">             for (var name in rootSetsByName) {
</span><span class="cx">                 var list = rootSetsByName[name];
</span><span class="cx">                 if (list.length &lt; setIndex) {
</span><del>-                    alert(`Set ${rootSetLabels[0]} specifies ${repository.label()} but set ${rootSetLabels[setIndex]} does not.`);
</del><ins>+                    alert(`Set ${firstLabel} specifies ${repository.label()} but set ${label} does not.`);
</ins><span class="cx">                     return null;
</span><span class="cx">                 }
</span><span class="cx">             }
</span><span class="lines">@@ -555,11 +558,11 @@
</span><span class="cx">                 &lt;/div&gt;
</span><span class="cx">                 &lt;section class=&quot;overview-chart&quot;&gt;
</span><span class="cx">                     &lt;analysis-task-chart-pane&gt;&lt;/analysis-task-chart-pane&gt;
</span><del>-                    &lt;div class=&quot;new-test-group-form&quot;&gt;&lt;test-group-form&gt;&lt;/test-group-form&gt;&lt;/div&gt;
</del><ins>+                    &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;
</ins><span class="cx">                 &lt;/section&gt;
</span><span class="cx">                 &lt;section class=&quot;analysis-results-view&quot;&gt;
</span><span class="cx">                     &lt;analysis-results-viewer&gt;&lt;/analysis-results-viewer&gt;
</span><del>-                    &lt;div class=&quot;new-test-group-form&quot;&gt;&lt;test-group-form&gt;&lt;/test-group-form&gt;&lt;/div&gt;
</del><ins>+                    &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;
</ins><span class="cx">                 &lt;/section&gt;
</span><span class="cx">                 &lt;section class=&quot;test-group-view&quot;&gt;
</span><span class="cx">                     &lt;ul class=&quot;test-group-list&quot;&gt;&lt;/ul&gt;
</span><span class="lines">@@ -615,7 +618,9 @@
</span><span class="cx">             .analysis-task-status {
</span><span class="cx">                 margin: 0;
</span><span class="cx">                 display: flex;
</span><del>-                margin-bottom: 1.5rem;
</del><ins>+                padding-bottom: 1rem;
+                margin-bottom: 1rem;
+                border-bottom: solid 1px #ccc;
</ins><span class="cx">             }
</span><span class="cx"> 
</span><span class="cx">             .analysis-task-status &gt; section {
</span><span class="lines">@@ -650,8 +655,12 @@
</span><span class="cx">                 overflow-y: scroll;
</span><span class="cx">             }
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx">             .analysis-results-view {
</span><del>-                margin: 2.5rem 1rem;
</del><ins>+                border-top: solid 1px #ccc;
+                border-bottom: solid 1px #ccc;
+                margin: 1rem 0;
+                padding: 1rem;
</ins><span class="cx">             }
</span><span class="cx"> 
</span><span class="cx">             .test-configuration h3 {
</span></span></pre>
</div>
</div>

</body>
</html>