v3 UI has the capability to schedule an A/B testing in a specific range
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 17 Feb 2016 20:18:05 +0000 (20:18 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 17 Feb 2016 20:18:05 +0000 (20:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=154329

Reviewed by Chris Dumez.

Extended AnalysisTaskChartPane and ResultsTable so that users can select a range of points in either
the overview chart pane and the results viewer table. Extracted TestGroupForm out of the analysis task
page and used right below those two components in the analysis task page.

* public/v3/components/results-table.js:
(ResultsTable):
(ResultsTable.prototype.setRangeSelectorLabels): Added.
(ResultsTable.prototype.setRangeSelectorCallback): Added.
(ResultsTable.prototype.selectedRange): Added.
(ResultsTable.prototype._rangeSelectorClicked): Added.
(ResultsTable.prototype.render): Generate radio boxes to select a range.

* public/v3/components/test-group-form.js:
(TestGroupForm):
(TestGroupForm.prototype.setStartCallback): Added.
(TestGroupForm.prototype.setNeedsName): Added.
(TestGroupForm.prototype.setDisabled): Added.
(TestGroupForm.prototype.setLabel): Added.
(TestGroupForm.prototype.setRepetitionCount): Added.
(TestGroupForm.prototype._submitted): Added.
(TestGroupForm.htmlTemplate): Extracted from AnalysisTaskPage.htmlTemplate.

* public/v3/index.html:

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype._mainSelectionDidChange): Added. Delegates the work to AnalysisTaskPage.
(AnalysisTaskChartPane.prototype.selectedPoints): Added.
(AnalysisTaskPage):
(AnalysisTaskPage.prototype.title):
(AnalysisTaskPage.prototype.render):
(AnalysisTaskPage.prototype._renderTestGroupDetails): Use TestGroupForm's methods instead of mutating DOM.
(AnalysisTaskPage.prototype._retryCurrentTestGroup):
(AnalysisTaskPage.prototype._chartSelectionDidChange): Added.
(AnalysisTaskPage.prototype._createNewTestGroupFromChart): Added.
(AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): Added.
(AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Added.
(AnalysisTaskPage.prototype._createRetryNameForTestGroup):
(AnalysisTaskPage.prototype._createTestGroupAfterVerifyingRootSetList): Extracted from _retryCurrentTestGroup
so that we can call it in _createNewTestGroupFromChart and _createNewTestGroupFromViewer.
(AnalysisTaskPage.htmlTemplate):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@196708 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/v3/components/results-table.js
Websites/perf.webkit.org/public/v3/components/test-group-form.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js

index f377496..ba56eaa 100644 (file)
@@ -1,3 +1,51 @@
+2016-02-16  Ryosuke Niwa  <rniwa@webkit.org>
+
+        v3 UI has the capability to schedule an A/B testing in a specific range
+        https://bugs.webkit.org/show_bug.cgi?id=154329
+
+        Reviewed by Chris Dumez.
+
+        Extended AnalysisTaskChartPane and ResultsTable so that users can select a range of points in either
+        the overview chart pane and the results viewer table. Extracted TestGroupForm out of the analysis task
+        page and used right below those two components in the analysis task page.
+
+        * public/v3/components/results-table.js:
+        (ResultsTable):
+        (ResultsTable.prototype.setRangeSelectorLabels): Added.
+        (ResultsTable.prototype.setRangeSelectorCallback): Added.
+        (ResultsTable.prototype.selectedRange): Added.
+        (ResultsTable.prototype._rangeSelectorClicked): Added.
+        (ResultsTable.prototype.render): Generate radio boxes to select a range.
+
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm):
+        (TestGroupForm.prototype.setStartCallback): Added.
+        (TestGroupForm.prototype.setNeedsName): Added.
+        (TestGroupForm.prototype.setDisabled): Added.
+        (TestGroupForm.prototype.setLabel): Added.
+        (TestGroupForm.prototype.setRepetitionCount): Added.
+        (TestGroupForm.prototype._submitted): Added.
+        (TestGroupForm.htmlTemplate): Extracted from AnalysisTaskPage.htmlTemplate.
+
+        * public/v3/index.html:
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype._mainSelectionDidChange): Added. Delegates the work to AnalysisTaskPage.
+        (AnalysisTaskChartPane.prototype.selectedPoints): Added.
+        (AnalysisTaskPage):
+        (AnalysisTaskPage.prototype.title):
+        (AnalysisTaskPage.prototype.render):
+        (AnalysisTaskPage.prototype._renderTestGroupDetails): Use TestGroupForm's methods instead of mutating DOM. 
+        (AnalysisTaskPage.prototype._retryCurrentTestGroup):
+        (AnalysisTaskPage.prototype._chartSelectionDidChange): Added.
+        (AnalysisTaskPage.prototype._createNewTestGroupFromChart): Added.
+        (AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer): Added.
+        (AnalysisTaskPage.prototype._createNewTestGroupFromViewer): Added.
+        (AnalysisTaskPage.prototype._createRetryNameForTestGroup):
+        (AnalysisTaskPage.prototype._createTestGroupAfterVerifyingRootSetList): Extracted from _retryCurrentTestGroup
+        so that we can call it in _createNewTestGroupFromChart and _createNewTestGroupFromViewer.
+        (AnalysisTaskPage.htmlTemplate):
+
 2016-02-15  Ryosuke Niwa  <rniwa@webkit.org>
 
         Extract the code specific to v2 UI out of shared statistics.js
index 22a4dda..0c838f3 100644 (file)
@@ -4,9 +4,22 @@ class ResultsTable extends ComponentBase {
         super(name);
         this._repositoryList = [];
         this._valueFormatter = null;
+        this._rangeSelectorLabels = null;
+        this._rangeSelectorCallback = null;
+        this._selectedRange = {};
     }
 
     setValueFormatter(valueFormatter) { this._valueFormatter = valueFormatter; }
+    setRangeSelectorLabels(labels) { this._rangeSelectorLabels = labels; }
+    setRangeSelectorCallback(callback) { this._rangeSelectorCallback = callback; }
+    selectedRange() { return this._selectedRange; }
+
+    _rangeSelectorClicked(label, row)
+    {
+        this._selectedRange[label] = row;
+        if (this._rangeSelectorCallback)
+            this._rangeSelectorCallback();
+    }
 
     render()
     {
@@ -23,13 +36,22 @@ class ResultsTable extends ComponentBase {
         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) {
+                    self._selectedRange = {};
+                    for (var label of self._rangeSelectorLabels)
+                        cells.push(element('td', element('input',
+                            {type: 'radio', name: label, onchange: self._rangeSelectorClicked.bind(self, label, row)})));
+                }
+
                 if (groupHeading !== undefined && !rowIndex)
                     cells.push(element('th', {rowspan: group.rows.length}, groupHeading));
                 cells.push(element('td', row.heading()));
@@ -47,6 +69,7 @@ class ResultsTable extends ComponentBase {
 
         this.renderReplace(this.content().querySelector('table'), [
             element('thead', [
+                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()); }),
diff --git a/Websites/perf.webkit.org/public/v3/components/test-group-form.js b/Websites/perf.webkit.org/public/v3/components/test-group-form.js
new file mode 100644 (file)
index 0000000..25dffce
--- /dev/null
@@ -0,0 +1,55 @@
+
+class TestGroupForm extends ComponentBase {
+
+    constructor()
+    {
+        super('test-group-form');
+        this._startCallback = null;
+        this._repetitionCountControl = this.content().querySelector('.repetition-count');
+        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);
+    }
+
+    setStartCallback(callback) { this._startCallback = callback; }
+    setNeedsName(needsName) { this._nameControl.style.display = needsName ? null : 'none'; }
+    setDisabled(disabled) { this._buttonControl.disabled = disabled; }
+
+    setLabel(label) { this._buttonControl.textContent = label; }
+    setRepetitionCount(count) { this._repetitionCountControl.value = count; }
+
+    _submitted(event)
+    {
+        event.preventDefault();
+        if (this._startCallback)
+            this._startCallback(this._nameControl.value, this._repetitionCountControl.value);
+    }
+
+    static htmlTemplate()
+    {
+        return `
+            <form>
+                <button type="submit">Start A/B testing</button>
+                <input class="name" type="text">
+                with
+                <select class="repetition-count">
+                    <option>1</option>
+                    <option>2</option>
+                    <option>3</option>
+                    <option>4</option>
+                    <option>5</option>
+                    <option>6</option>
+                    <option>7</option>
+                    <option>8</option>
+                    <option>9</option>
+                    <option>10</option>
+                </select>
+                iterations per set
+            </form>
+        `;
+    }
+
+}
+
+ComponentBase.defineElement('test-group-form', TestGroupForm);
index 4df655c..d2738e8 100644 (file)
@@ -75,6 +75,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="components/results-table.js"></script>
         <script src="components/analysis-results-viewer.js"></script>
         <script src="components/test-group-results-table.js"></script>
+        <script src="components/test-group-form.js"></script>
         <script src="components/chart-styles.js"></script>
         <script src="components/chart-pane-base.js"></script>
         <script src="pages/page.js"></script>
index b80e9b1..1cbddb1 100644 (file)
@@ -8,6 +8,22 @@ class AnalysisTaskChartPane extends ChartPaneBase {
 
     setPage(page) { this._page = page; }
     router() { return this._page.router(); }
+
+    _mainSelectionDidChange(selection, didEndDrag)
+    {
+        super._mainSelectionDidChange(selection);
+        if (didEndDrag)
+            this._page._chartSelectionDidChange();
+    }
+
+    selectedPoints()
+    {
+        var selection = this._mainChart ? this._mainChart.currentSelection() : null;
+        if (!selection)
+            return null;
+
+        return this._mainChart.sampledDataBetween('current', selection[0], selection[1]);
+    }
 }
 
 ComponentBase.defineElement('analysis-task-chart-pane', AnalysisTaskChartPane);
@@ -33,6 +49,8 @@ class AnalysisTaskPage extends PageWithHeading {
         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.setStartedEditingCallback(this._didStartEditingTaskName.bind(this));
@@ -45,7 +63,16 @@ class AnalysisTaskPage extends PageWithHeading {
         this._bugTrackerControl = this.content().querySelector('.bug-tracker-control');
         this._bugNumberControl = this.content().querySelector('.bug-number-control');
 
-        this.content().querySelector('.test-group-retry-form').onsubmit = this._retryCurrentTestGroup.bind(this);
+        this._newTestGroupFormForChart = this.content().querySelector('.overview-chart test-group-form').component();
+        this._newTestGroupFormForChart.setStartCallback(this._createNewTestGroupFromChart.bind(this));
+
+        this._newTestGroupFormForViewer = this.content().querySelector('.analysis-results-view test-group-form').component();
+        this._newTestGroupFormForViewer.setStartCallback(this._createNewTestGroupFromViewer.bind(this));
+        this._selectedRowInAnalysisResultsViewer();
+
+        this._retryForm = this.content().querySelector('.test-group-retry-form').firstChild.component();
+        this._retryForm.setStartCallback(this._retryCurrentTestGroup.bind(this));
+        this._retryForm.setNeedsName(false);
     }
 
     title() { return this._task ? this._task.label() : 'Analysis Task'; }
@@ -228,6 +255,8 @@ class AnalysisTaskPage extends PageWithHeading {
                 }));
         }
 
+        this._newTestGroupFormForChart.render();
+
         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
         this._analysisResultsViewer.render();
 
@@ -297,16 +326,14 @@ class AnalysisTaskPage extends PageWithHeading {
                     this._chartPane.setMainSelection([startTime, endTime]);
             }
 
-            this.content().querySelector('.test-group-retry-button').textContent = this._currentTestGroup ? 'Retry' : 'Confirm the change';
-
-            var repetitionCount = this._currentTestGroup ? this._currentTestGroup.repetitionCount() : 4;
-            var repetitionCountController = this.content().querySelector('.test-group-retry-repetition-count');
-            repetitionCountController.value = repetitionCount;
+            this._retryForm.setLabel('Retry');
+            if (this._currentTestGroup)
+                this._retryForm.setRepetitionCount(this._currentTestGroup.repetitionCount());
+            this._retryForm.element().style.display = this._currentTestGroup ? null : 'none';
 
             this._renderedCurrentTestGroup = this._currentTestGroup;
         }
-
-        this.content().querySelector('.test-group-retry-button').disabled = !(this._currentTestGroup || this._startPoint);
+        this._retryForm.render();
     }
 
     _showTestGroup(testGroup)
@@ -390,25 +417,48 @@ class AnalysisTaskPage extends PageWithHeading {
         });
     }
 
-    _retryCurrentTestGroup(event)
+    _retryCurrentTestGroup(unusedName, repetitionCount)
     {
-        event.preventDefault();
-        console.assert(this._currentTestGroup || this._startPoint);
-
-        var testGroupName;
-        var rootSetList;
-        var rootSetLabels;
-
-        if (this._currentTestGroup) {
-            var testGroup = this._currentTestGroup;
-            testGroupName = this._createRetryNameForTestGroup(testGroup.name());
-            rootSetList = testGroup.requestedRootSets();
-            rootSetLabels = rootSetList.map(function (rootSet) { return testGroup.labelForRootSet(rootSet); });
-        } else {
-            testGroupName = 'Confirming the change';
-            rootSetList = [this._startPoint.rootSet(), this._endPoint.rootSet()];
-            rootSetLabels = ['Point 0', `Point ${this._endPoint.seriesIndex - this._startPoint.seriesIndex}`];
-        }
+        console.assert(this._currentTestGroup);
+        var testGroup = this._currentTestGroup;
+        var newName = this._createRetryNameForTestGroup(testGroup.name());
+        var rootSetList = testGroup.requestedRootSets();
+        var rootSetLabels = rootSetList.map(function (rootSet) { return testGroup.labelForRootSet(rootSet); });
+        return this._createTestGroupAfterVerifyingRootSetList(newName, repetitionCount, rootSetList, rootSetLabels);
+    }
+
+    _chartSelectionDidChange()
+    {
+        var points = this._chartPane.selectedPoints();
+        this._newTestGroupFormForChart.setDisabled(!points || points.length < 2);
+    }
+
+    _createNewTestGroupFromChart(name, repetitionCount)
+    {
+        var points = this._chartPane.selectedPoints();
+        console.assert(points && points.length >= 2);
+        return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount,
+            [points[0].rootSet(), points[points.length - 1].rootSet()], ['A', 'B']);
+    }
+
+    _selectedRowInAnalysisResultsViewer()
+    {
+        var selectedRange = this._analysisResultsViewer.selectedRange();
+        this._newTestGroupFormForViewer.setDisabled(!selectedRange['A'] || !selectedRange['B']);
+    }
+
+    _createNewTestGroupFromViewer(name, repetitionCount)
+    {
+        var selectedRange = this._analysisResultsViewer.selectedRange();
+        console.assert(selectedRange && selectedRange['A'] && selectedRange['B']);
+        return this._createTestGroupAfterVerifyingRootSetList(name, repetitionCount,
+            [selectedRange['A'].rootSet(), selectedRange['B'].rootSet()], ['A', 'B']);
+    }
+
+    _createTestGroupAfterVerifyingRootSetList(testGroupName, repetitionCount, rootSetList, rootSetLabels)
+    {
+        if (this._hasDuplicateTestGroupName(testGroupName))
+            alert(`There is already a test group named "${testGroupName}"`);
 
         var rootSetsByName = {};
         for (var repository of rootSetList[0].repositories())
@@ -434,8 +484,6 @@ class AnalysisTaskPage extends PageWithHeading {
             }
         }
 
-        var repetitionCount = this.content().querySelector('.test-group-retry-repetition-count').value;
-
         TestGroup.createAndRefetchTestGroups(this._task, testGroupName, repetitionCount, rootSetsByName)
             .then(this._didFetchTestGroups.bind(this), function (error) {
             alert('Failed to create a new test group: ' + error);
@@ -507,31 +555,17 @@ class AnalysisTaskPage extends PageWithHeading {
                 </div>
                 <section class="overview-chart">
                     <analysis-task-chart-pane></analysis-task-chart-pane>
+                    <div class="new-test-group-form"><test-group-form></test-group-form></div>
                 </section>
                 <section class="analysis-results-view">
                     <analysis-results-viewer></analysis-results-viewer>
+                    <div class="new-test-group-form"><test-group-form></test-group-form></div>
                 </section>
                 <section class="test-group-view">
                     <ul class="test-group-list"></ul>
                     <div class="test-group-details">
                         <test-group-results-table></test-group-results-table>
-                        <form class="test-group-retry-form">
-                            <button class="test-group-retry-button" type="submit">Retry</button>
-                            with
-                            <select class="test-group-retry-repetition-count">
-                                <option>1</option>
-                                <option>2</option>
-                                <option>3</option>
-                                <option>4</option>
-                                <option>5</option>
-                                <option>6</option>
-                                <option>7</option>
-                                <option>8</option>
-                                <option>9</option>
-                                <option>10</option>
-                            </select>
-                            iterations per set
-                        </form>
+                        <div class="test-group-retry-form"><test-group-form></test-group-form></div>
                     </div>
                 </section>
             </div>
@@ -617,7 +651,7 @@ class AnalysisTaskPage extends PageWithHeading {
             }
 
             .analysis-results-view {
-                margin: 1rem;
+                margin: 2.5rem 1rem;
             }
 
             .test-configuration h3 {
@@ -641,6 +675,7 @@ class AnalysisTaskPage extends PageWithHeading {
                 margin: 0;
             }
 
+            .new-test-group-form,
             .test-group-retry-form {
                 padding: 0;
                 margin: 0.5rem;