<!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>[196440] 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/196440">196440</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-02-11 14:17:55 -0800 (Thu, 11 Feb 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Perf dashboard should have UI to retry A/B testing
https://bugs.webkit.org/show_bug.cgi?id=154090

Reviewed by Chris Dumez.

Added a button to re-try an existing A/B testing group with a custom repetition count. The same button functions
as a way of confirming the progression/regression when there have been no A/B testing scheduled in the task.

Also fixed the bug that A/B testing groups that have been waiting for other test groups will be shown as &quot;running&quot;.

* public/v3/components/results-table.js:
(ResultsTable.cssTemplate): Don't pad the list of extra repositories when it's empty.

* public/v3/components/test-group-results-table.js:
(TestGroupResultsTable.prototype.buildRowGroups): Use TestGroup.labelForRootSet instead of manually
computing the letter for each configuration set.

* public/v3/models/build-request.js:
(BuildRequest.prototype.hasStarted): Added.

* public/v3/models/data-model.js:
(DataModelObject.ensureSingleton): Added.
(DataModelObject.cachedFetch): Added noCache option. This is used when re-fetching the test groups after
creating one.

* public/v3/models/measurement-cluster.js:
(MeasurementCluster.prototype.startTime): Added.

* public/v3/models/measurement-set.js:
(MeasurementSet.prototype.hasFetchedRange): Added. Returns true only if there are no &quot;holes&quot; (cluster
yet to be fetched) between the specified time range. This was added to fix a bug in AnalysisTaskPage's
_didFetchMeasurement.

* public/v3/models/test-group.js:
(TestGroup): Added this._rootSetToLabel.
(TestGroup.prototype.addBuildRequest): Reset this._rootSetToLabel along with this._requestedRootSets. 
(TestGroup.prototype.repetitionCount): Added. Returns the number of iterations executed per set. We assume that
every root set in the test group shares a single repetition count.
(TestGroup.prototype.requestedRootSets): Now populates this._rootSetToLabel for labelForRootSet.
(TestGroup.prototype.labelForRootSet): Added.
(TestGroup.prototype.hasStarted): Added.
(TestGroup.prototype.compareTestResults): Use 'running' and 'pending' to differentiate test groups that are waiting
for other groups to finish running from the ones that are actually running ('incomplete' before this patch).
(TestGroup.fetchByTask):
(TestGroup.createAndRefetchTestGroups): Added. Creates a new test group using the privileged-api/create-test-group
and fetches the list of test groups for the specified analysis task.
(TestGroup._createModelsFromFetchedTestGroups): Extracted from TestGroup.fetchByTask.

* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage): Initialize _renderedCurrentTestGroup to undefined so that we'd always can differentiate
the initial call to AnalysisTaskPage.render and subsequent calls in which it's identical to _currentTestGroup.
(AnalysisTaskPage.prototype._didFetchMeasurement): Fixed a bug that we don't exit early even when some
clusters in between startPoint and endPoint are still being fetched via newly added hasFetchedRange.
(AnalysisTaskPage.prototype.render): Update the default repetition count based on the current test group.
Also update the label of the button to &quot;Confirm the change&quot; if there is no A/B testing in this task.
(AnalysisTaskPage.prototype._retryCurrentTestGroup): Added. Re-triggers an existing A/B testing group or creates
the A/B testing for the entire range of the analysis task.
(AnalysisTaskPage.prototype._hasDuplicateTestGroupName): Added.
(AnalysisTaskPage.prototype._createRetryNameForTestGroup): Added.
(AnalysisTaskPage.htmlTemplate): Added form controls to re-trigger A/B testing.
(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="#trunkWebsitesperfwebkitorgpublicv3componentsresultstablejs">trunk/Websites/perf.webkit.org/public/v3/components/results-table.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="#trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs">trunk/Websites/perf.webkit.org/public/v3/models/build-request.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsdatamodeljs">trunk/Websites/perf.webkit.org/public/v3/models/data-model.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementclusterjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmeasurementsetjs">trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.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>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -1,5 +1,69 @@
</span><span class="cx"> 2016-02-10  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><ins>+        Perf dashboard should have UI to retry A/B testing
+        https://bugs.webkit.org/show_bug.cgi?id=154090
+
+        Reviewed by Chris Dumez.
+
+        Added a button to re-try an existing A/B testing group with a custom repetition count. The same button functions
+        as a way of confirming the progression/regression when there have been no A/B testing scheduled in the task.
+
+        Also fixed the bug that A/B testing groups that have been waiting for other test groups will be shown as &quot;running&quot;.
+
+        * public/v3/components/results-table.js:
+        (ResultsTable.cssTemplate): Don't pad the list of extra repositories when it's empty.
+
+        * public/v3/components/test-group-results-table.js:
+        (TestGroupResultsTable.prototype.buildRowGroups): Use TestGroup.labelForRootSet instead of manually
+        computing the letter for each configuration set.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest.prototype.hasStarted): Added.
+
+        * public/v3/models/data-model.js:
+        (DataModelObject.ensureSingleton): Added.
+        (DataModelObject.cachedFetch): Added noCache option. This is used when re-fetching the test groups after
+        creating one.
+
+        * public/v3/models/measurement-cluster.js:
+        (MeasurementCluster.prototype.startTime): Added.
+
+        * public/v3/models/measurement-set.js:
+        (MeasurementSet.prototype.hasFetchedRange): Added. Returns true only if there are no &quot;holes&quot; (cluster
+        yet to be fetched) between the specified time range. This was added to fix a bug in AnalysisTaskPage's
+        _didFetchMeasurement.
+
+        * public/v3/models/test-group.js:
+        (TestGroup): Added this._rootSetToLabel.
+        (TestGroup.prototype.addBuildRequest): Reset this._rootSetToLabel along with this._requestedRootSets. 
+        (TestGroup.prototype.repetitionCount): Added. Returns the number of iterations executed per set. We assume that
+        every root set in the test group shares a single repetition count.
+        (TestGroup.prototype.requestedRootSets): Now populates this._rootSetToLabel for labelForRootSet.
+        (TestGroup.prototype.labelForRootSet): Added.
+        (TestGroup.prototype.hasStarted): Added.
+        (TestGroup.prototype.compareTestResults): Use 'running' and 'pending' to differentiate test groups that are waiting
+        for other groups to finish running from the ones that are actually running ('incomplete' before this patch).
+        (TestGroup.fetchByTask):
+        (TestGroup.createAndRefetchTestGroups): Added. Creates a new test group using the privileged-api/create-test-group
+        and fetches the list of test groups for the specified analysis task.
+        (TestGroup._createModelsFromFetchedTestGroups): Extracted from TestGroup.fetchByTask.
+
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage): Initialize _renderedCurrentTestGroup to undefined so that we'd always can differentiate
+        the initial call to AnalysisTaskPage.render and subsequent calls in which it's identical to _currentTestGroup.
+        (AnalysisTaskPage.prototype._didFetchMeasurement): Fixed a bug that we don't exit early even when some
+        clusters in between startPoint and endPoint are still being fetched via newly added hasFetchedRange.
+        (AnalysisTaskPage.prototype.render): Update the default repetition count based on the current test group.
+        Also update the label of the button to &quot;Confirm the change&quot; if there is no A/B testing in this task.
+        (AnalysisTaskPage.prototype._retryCurrentTestGroup): Added. Re-triggers an existing A/B testing group or creates
+        the A/B testing for the entire range of the analysis task.
+        (AnalysisTaskPage.prototype._hasDuplicateTestGroupName): Added.
+        (AnalysisTaskPage.prototype._createRetryNameForTestGroup): Added.
+        (AnalysisTaskPage.htmlTemplate): Added form controls to re-trigger A/B testing.
+        (AnalysisTaskPage.cssTemplate): Updated the style.
+
+2016-02-10  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
</ins><span class="cx">         Removed the duplicated definition of ChartPaneBase.
</span><span class="cx"> 
</span><span class="cx">         * public/v3/components/chart-pane-base.js:
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsresultstablejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/results-table.js (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/results-table.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/components/results-table.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -226,6 +226,10 @@
</span><span class="cx">                 font-size: 0.8rem;
</span><span class="cx">             }
</span><span class="cx"> 
</span><ins>+            .results-table-extra-repositories:empty {
+                padding: 0;
+            }
+
</ins><span class="cx">             .results-table-extra-repositories li {
</span><span class="cx">                 display: inline;
</span><span class="cx">             }
</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 (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-results-table.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -30,7 +30,7 @@
</span><span class="cx">             return [];
</span><span class="cx"> 
</span><span class="cx">         var rootSets = this._testGroup.requestedRootSets();
</span><del>-        var groups = rootSets.map(function (rootSet, setIndex) {
</del><ins>+        var groups = rootSets.map(function (rootSet) {
</ins><span class="cx">             var rows = [new ResultsTableRow('Mean', rootSet)];
</span><span class="cx">             var results = [];
</span><span class="cx"> 
</span><span class="lines">@@ -51,17 +51,17 @@
</span><span class="cx">             if (!isNaN(aggregatedResult.value))
</span><span class="cx">                 rows[0].setResult(aggregatedResult);
</span><span class="cx"> 
</span><del>-            return {heading: String.fromCharCode('A'.charCodeAt(0) + setIndex), rows:rows};
</del><ins>+            return {heading: testGroup.labelForRootSet(rootSet), rows:rows};
</ins><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         var comparisonRows = [];
</span><span class="cx">         for (var i = 0; i &lt; rootSets.length; i++) {
</span><span class="cx">             for (var j = i + 1; j &lt; rootSets.length; j++) {
</span><del>-                var startConfig = String.fromCharCode('A'.charCodeAt(0) + i);
-                var endConfig = String.fromCharCode('A'.charCodeAt(0) + j);
</del><ins>+                var startConfig = testGroup.labelForRootSet(rootSets[i]);
+                var endConfig = testGroup.labelForRootSet(rootSets[j]);
</ins><span class="cx"> 
</span><span class="cx">                 var result = this._testGroup.compareTestResults(rootSets[i], rootSets[j]);
</span><del>-                if (result.status == 'incomplete' || result.status == 'failed')
</del><ins>+                if (result.status == 'pending' || result.status == 'running' || result.status == 'failed')
</ins><span class="cx">                     continue;
</span><span class="cx"> 
</span><span class="cx">                 var row = new ResultsTableRow(`${startConfig} to ${endConfig}`, null);
</span><span class="lines">@@ -70,7 +70,7 @@
</span><span class="cx">             }
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        groups.push({heading: '', rows: comparisonRows});
</del><ins>+        groups.unshift({heading: '', rows: comparisonRows});
</ins><span class="cx"> 
</span><span class="cx">         return groups;
</span><span class="cx">     }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -21,6 +21,7 @@
</span><span class="cx">     rootSet() { return this._rootSet; }
</span><span class="cx"> 
</span><span class="cx">     hasCompleted() { return this._status == 'failed' || this._status == 'completed'; }
</span><ins>+    hasStarted() { return this._status != 'pending'; }
</ins><span class="cx">     statusLabel()
</span><span class="cx">     {
</span><span class="cx">         switch (this._status) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsdatamodeljs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/data-model.js (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/data-model.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/models/data-model.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -7,6 +7,14 @@
</span><span class="cx">     }
</span><span class="cx">     id() { return this._id; }
</span><span class="cx"> 
</span><ins>+    static ensureSingleton(id, object)
+    {
+        var singleton = this.findById(id);
+        if (singleton)
+            return singleton;
+        return new (this)(id, object);
+    }
+
</ins><span class="cx">     static namedStaticMap(name)
</span><span class="cx">     {
</span><span class="cx">         var staticMap = this[DataModelObject.StaticMapSymbol];
</span><span class="lines">@@ -43,7 +51,7 @@
</span><span class="cx">         return list;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    static cachedFetch(path, params)
</del><ins>+    static cachedFetch(path, params, noCache)
</ins><span class="cx">     {
</span><span class="cx">         var query = [];
</span><span class="cx">         if (params) {
</span><span class="lines">@@ -53,6 +61,9 @@
</span><span class="cx">         if (query.length)
</span><span class="cx">             path += '?' + query.join('&amp;');
</span><span class="cx"> 
</span><ins>+        if (noCache)
+            return getJSONWithStatus(path);
+
</ins><span class="cx">         var cacheMap = this.ensureNamedStaticMap(DataModelObject.CacheMapSymbol);
</span><span class="cx">         if (!cacheMap[path])
</span><span class="cx">             cacheMap[path] = getJSONWithStatus(path);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementclusterjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-cluster.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -7,6 +7,7 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     startTime() { return this._response['startTime']; }
</span><ins>+    endTime() { return this._response['endTime']; }
</ins><span class="cx"> 
</span><span class="cx">     addToSeries(series, configType, includeOutliers, idMap)
</span><span class="cx">     {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmeasurementsetjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/models/measurement-set.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -191,6 +191,23 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    hasFetchedRange(startTime, endTime)
+    {
+        console.assert(startTime &lt; endTime);
+        var hasHole = false;
+        var previousEndTime = null;
+        for (var cluster of this._sortedClusters) {
+            if (cluster.startTime() &lt; startTime &amp;&amp; startTime &lt; cluster.endTime())
+                hasHole = false;
+            if (previousEndTime !== null &amp;&amp; previousEndTime != cluster.startTime())
+                hasHole = true;
+            if (cluster.startTime() &lt; endTime &amp;&amp; endTime &lt; cluster.endTime())
+                break;
+            previousEndTime = cluster.endTime();
+        }
+        return !hasHole;
+    }
+
</ins><span class="cx">     fetchedTimeSeries(configType, includeOutliers, extendToFuture)
</span><span class="cx">     {
</span><span class="cx">         Instrumentation.startMeasuringTime('MeasurementSet', 'fetchedTimeSeries');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstestgroupjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/test-group.js (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/test-group.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/models/test-group.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -11,6 +11,7 @@
</span><span class="cx">         this._requestsAreInOrder = false;
</span><span class="cx">         this._repositories = null;
</span><span class="cx">         this._requestedRootSets = null;
</span><ins>+        this._rootSetToLabel = new Map;
</ins><span class="cx">         this._allRootSets = null;
</span><span class="cx">         console.assert(!object.platform || object.platform instanceof Platform);
</span><span class="cx">         this._platform = object.platform;
</span><span class="lines">@@ -23,8 +24,22 @@
</span><span class="cx">         this._buildRequests.push(request);
</span><span class="cx">         this._requestsAreInOrder = false;
</span><span class="cx">         this._requestedRootSets = null;
</span><ins>+        this._rootSetToLabel = null;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    repetitionCount()
+    {
+        if (!this._buildRequests.length)
+            return 0;
+        var rootSet = this._buildRequests[0].rootSet();
+        var count = 0;
+        for (var request of this._buildRequests) {
+            if (request.rootSet() == rootSet)
+                count++;
+        }
+        return count;
+    }
+
</ins><span class="cx">     requestedRootSets()
</span><span class="cx">     {
</span><span class="cx">         if (!this._requestedRootSets) {
</span><span class="lines">@@ -36,6 +51,12 @@
</span><span class="cx">                     this._requestedRootSets.push(set);
</span><span class="cx">             }
</span><span class="cx">             this._requestedRootSets.sort(function (a, b) { return a.latestCommitTime() - b.latestCommitTime(); });
</span><ins>+            var setIndex = 0;
+            for (var set of this._requestedRootSets) {
+                this._rootSetToLabel.set(set, String.fromCharCode('A'.charCodeAt(0) + setIndex));
+                setIndex++;
+            }
+
</ins><span class="cx">         }
</span><span class="cx">         return this._requestedRootSets;
</span><span class="cx">     }
</span><span class="lines">@@ -46,6 +67,12 @@
</span><span class="cx">         return this._buildRequests.filter(function (request) { return request.rootSet() == rootSet; });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    labelForRootSet(rootSet)
+    {
+        console.assert(this._requestedRootSets);
+        return this._rootSetToLabel.get(rootSet);
+    }
+
</ins><span class="cx">     _orderBuildRequests()
</span><span class="cx">     {
</span><span class="cx">         if (this._requestsAreInOrder)
</span><span class="lines">@@ -64,6 +91,11 @@
</span><span class="cx">         return this._buildRequests.every(function (request) { return request.hasCompleted(); });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    hasStarted()
+    {
+        return this._buildRequests.some(function (request) { return request.hasStarted(); });
+    }
+
</ins><span class="cx">     compareTestResults(rootSetA, rootSetB)
</span><span class="cx">     {
</span><span class="cx">         var beforeValues = this._valuesForRootSet(rootSetA);
</span><span class="lines">@@ -85,9 +117,15 @@
</span><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         if (!this.hasCompleted()) {
</span><del>-            result.status = 'incomplete';
-            result.label = 'Running';
-            result.fullLabel = 'Running';
</del><ins>+            if (this.hasStarted()) {
+                result.status = 'running';
+                result.label = 'Running';
+                result.fullLabel = 'Running';
+            } else {
+                result.status = 'pending';
+                result.label = 'Pending';
+                result.fullLabel = 'Pending';
+            }
</ins><span class="cx">         } else if (result.changeType) {
</span><span class="cx">             var significance = result.isStatisticallySignificant ? 'significant' : 'insignificant';
</span><span class="cx">             result.fullLabel = `${result.label} (statistically ${significance})`;
</span><span class="lines">@@ -107,32 +145,47 @@
</span><span class="cx">         return values;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static createAndRefetchTestGroups(task, name, repetitionCount, rootSets)
+    {
+        var self = this;
+        return PrivilegedAPI.sendRequest('create-test-group', {
+            task: task.id(),
+            name: name,
+            repetitionCount: repetitionCount,
+            rootSets: rootSets,
+        }).then(function (data) {
+            return self.cachedFetch('../api/test-groups', {task: task.id()}, true).then(self._createModelsFromFetchedTestGroups.bind(self));
+        });
+    }
+
</ins><span class="cx">     static fetchByTask(taskId)
</span><span class="cx">     {
</span><del>-        return this.cachedFetch('../api/test-groups', {task: taskId}).then(function (data) {
-            var testGroups = data['testGroups'].map(function (row) {
-                row.platform = Platform.findById(row.platform);
-                return new TestGroup(row.id, row);
-            });
</del><ins>+        return this.cachedFetch('../api/test-groups', {task: taskId}).then(this._createModelsFromFetchedTestGroups.bind(this));
+    }
</ins><span class="cx"> 
</span><del>-            var rootIdMap = {};
-            for (var root of data['roots'])
-                rootIdMap[root.id] = root;
</del><ins>+    static _createModelsFromFetchedTestGroups(data)
+    {
+        var testGroups = data['testGroups'].map(function (row) {
+            row.platform = Platform.findById(row.platform);
+            return TestGroup.ensureSingleton(row.id, row);
+        });
</ins><span class="cx"> 
</span><del>-            var rootSets = data['rootSets'].map(function (row) {
-                row.roots = row.roots.map(function (rootId) { return rootIdMap[rootId]; });
-                row.testGroup = RootSet.findById(row.testGroup);
-                return new RootSet(row.id, row);
-            });
</del><ins>+        var rootIdMap = {};
+        for (var root of data['roots'])
+            rootIdMap[root.id] = root;
</ins><span class="cx"> 
</span><del>-            var buildRequests = data['buildRequests'].map(function (rawData) {
-                rawData.testGroup = TestGroup.findById(rawData.testGroup);
-                rawData.rootSet = RootSet.findById(rawData.rootSet);
-                return new BuildRequest(rawData.id, rawData);
-            });
</del><ins>+        var rootSets = data['rootSets'].map(function (row) {
+            row.roots = row.roots.map(function (rootId) { return rootIdMap[rootId]; });
+            row.testGroup = RootSet.findById(row.testGroup);
+            return RootSet.ensureSingleton(row.id, row);
+        });
</ins><span class="cx"> 
</span><del>-            return testGroups;
</del><ins>+        var buildRequests = data['buildRequests'].map(function (rawData) {
+            rawData.testGroup = TestGroup.findById(rawData.testGroup);
+            rawData.rootSet = RootSet.findById(rawData.rootSet);
+            return BuildRequest.ensureSingleton(rawData.id, rawData);
</ins><span class="cx">         });
</span><ins>+
+        return testGroups;
</ins><span class="cx">     }
</span><del>-
</del><span class="cx"> }
</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 (196439 => 196440)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2016-02-11 22:15:45 UTC (rev 196439)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2016-02-11 22:17:55 UTC (rev 196440)
</span><span class="lines">@@ -13,7 +13,7 @@
</span><span class="cx">         this._task = null;
</span><span class="cx">         this._testGroups = null;
</span><span class="cx">         this._renderedTestGroups = null;
</span><del>-        this._renderedCurrentTestGroup = null;
</del><ins>+        this._renderedCurrentTestGroup = undefined;
</ins><span class="cx">         this._analysisResults = null;
</span><span class="cx">         this._measurementSet = null;
</span><span class="cx">         this._startPoint = null;
</span><span class="lines">@@ -24,6 +24,8 @@
</span><span class="cx">         this._analysisResultsViewer = this.content().querySelector('analysis-results-viewer').component();
</span><span class="cx">         this._analysisResultsViewer.setTestGroupCallback(this._showTestGroup.bind(this));
</span><span class="cx">         this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
</span><ins>+
+        this.content().querySelector('.test-group-retry-form').onsubmit = this._retryCurrentTestGroup.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">@@ -85,7 +87,7 @@
</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><del>-        if (!startPoint || !endPoint)
</del><ins>+        if (!startPoint || !endPoint || !this._measurementSet.hasFetchedRange(startPoint.time, endPoint.time))
</ins><span class="cx">             return;
</span><span class="cx"> 
</span><span class="cx">         this._analysisResultsViewer.setPoints(startPoint, endPoint);
</span><span class="lines">@@ -139,7 +141,7 @@
</span><span class="cx"> 
</span><span class="cx">         var v2URL = `/v2/#/analysis/task/${this._taskId}`;
</span><span class="cx">         this.content().querySelector('.error-message').innerHTML +=
</span><del>-            `&lt;p&gt;This page is read only for now. To schedule a new A/B testing job, use &lt;a href=&quot;${v2URL}&quot;&gt;v2 page&lt;/a&gt;.&lt;/p&gt;`;
</del><ins>+            `&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;`;
</ins><span class="cx"> 
</span><span class="cx">          this._chartPane.render();
</span><span class="cx"> 
</span><span class="lines">@@ -168,7 +170,8 @@
</span><span class="cx">                 }));
</span><span class="cx">             this._renderedCurrentTestGroup = null;
</span><span class="cx">         }
</span><del>-        if (this._renderedCurrentTestGroup != this._currentTestGroup) {
</del><ins>+
+        if (this._renderedCurrentTestGroup !== this._currentTestGroup) {
</ins><span class="cx">             if (this._renderedCurrentTestGroup) {
</span><span class="cx">                 var element = this.content().querySelector('.test-group-list-' + this._renderedCurrentTestGroup.id());
</span><span class="cx">                 if (element)
</span><span class="lines">@@ -179,9 +182,18 @@
</span><span class="cx">                 if (element)
</span><span class="cx">                     element.classList.add('selected');
</span><span class="cx">             }
</span><ins>+
+            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;
+
</ins><span class="cx">             this._renderedCurrentTestGroup = this._currentTestGroup;
</span><span class="cx">         }
</span><span class="cx"> 
</span><ins>+        this.content().querySelector('.test-group-retry-button').disabled = !(this._currentTestGroup || this._startPoint);
+
</ins><span class="cx">         this._testGroupResultsTable.render();
</span><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
</span><span class="lines">@@ -194,6 +206,86 @@
</span><span class="cx">         this.render();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _retryCurrentTestGroup(event)
+    {
+        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}`];
+        }
+
+        var rootSetsByName = {};
+        for (var repository of rootSetList[0].repositories())
+            rootSetsByName[repository.name()] = [];
+
+        var setIndex = 0;
+        for (var rootSet of rootSetList) {
+            for (var repository of rootSet.repositories()) {
+                var list = rootSetsByName[repository.name()];
+                if (!list) {
+                    alert(`Set ${rootSetLabels[setIndex]} specifies ${repository.label()} but set ${rootSetLabels[0]} does not.`);
+                    return null;
+                }
+                list.push(rootSet.commitForRepository(repository).revision());
+            }
+            setIndex++;
+            for (var name in rootSetsByName) {
+                var list = rootSetsByName[name];
+                if (list.length &lt; setIndex) {
+                    alert(`Set ${rootSetLabels[0]} specifies ${repository.label()} but set ${rootSetLabels[setIndex]} does not.`);
+                    return null;
+                }
+            }
+        }
+
+        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);
+        });
+    }
+
+    _createRetryNameForTestGroup(name)
+    {
+        var nameWithNumberMatch = name.match(/(.+?)\s*\(\s*(\d+)\s*\)\s*$/);
+        var number = 1;
+        if (nameWithNumberMatch) {
+            name = nameWithNumberMatch[1];
+            number = parseInt(nameWithNumberMatch[2]);
+        }
+
+        var newName;
+        do {
+            number++;
+            newName = `${name} (${number})`;
+        } while (this._hasDuplicateTestGroupName(newName));
+
+        return newName;
+    }
+
+    _hasDuplicateTestGroupName(name)
+    {
+        console.assert(this._testGroups);
+        for (var group of this._testGroups) {
+            if (group.name() == name)
+                return true;
+        }
+        return false;
+    }
+
</ins><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><span class="lines">@@ -208,7 +300,26 @@
</span><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><del>-                    &lt;div class=&quot;test-group-details&quot;&gt;&lt;test-group-results-table&gt;&lt;/test-group-results-table&gt;&lt;/div&gt;
</del><ins>+                    &lt;div class=&quot;test-group-details&quot;&gt;
+                        &lt;test-group-results-table&gt;&lt;/test-group-results-table&gt;
+                        &lt;form class=&quot;test-group-retry-form&quot;&gt;
+                            &lt;button class=&quot;test-group-retry-button&quot; type=&quot;submit&quot;&gt;Retry&lt;/button&gt;
+                            with
+                            &lt;select class=&quot;test-group-retry-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;
+                    &lt;/div&gt;
</ins><span class="cx">                 &lt;/section&gt;
</span><span class="cx">             &lt;/div&gt;
</span><span class="cx">         &lt;/div&gt;
</span><span class="lines">@@ -268,18 +379,23 @@
</span><span class="cx">             .test-group-view {
</span><span class="cx">                 display: table;
</span><span class="cx">                 margin: 0 1rem;
</span><ins>+                margin-bottom: 2rem;
</ins><span class="cx">             }
</span><span class="cx"> 
</span><span class="cx">             .test-group-details {
</span><span class="cx">                 display: table-cell;
</span><span class="cx">                 margin-bottom: 1rem;
</span><ins>+                padding: 0;
+                margin: 0;
</ins><span class="cx">             }
</span><span class="cx"> 
</span><ins>+            .test-group-retry-form {
+                padding: 0;
+                margin: 0.5rem;
+            }
+
</ins><span class="cx">             .test-group-list {
</span><span class="cx">                 display: table-cell;
</span><del>-            }
-
-            .test-group-list:not(:empty) {
</del><span class="cx">                 margin: 0;
</span><span class="cx">                 padding: 0.2rem 0;
</span><span class="cx">                 list-style: none;
</span><span class="lines">@@ -287,6 +403,12 @@
</span><span class="cx">                 white-space: nowrap;
</span><span class="cx">             }
</span><span class="cx"> 
</span><ins>+            .test-group-list:empty {
+                margin: 0;
+                padding: 0;
+                border-right: none;
+            }
+
</ins><span class="cx">             .test-group-list li {
</span><span class="cx">                 display: block;
</span><span class="cx">             }
</span></span></pre>
</div>
</div>

</body>
</html>