<!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>[180333] 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/180333">180333</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2015-02-18 18:41:45 -0800 (Wed, 18 Feb 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Analysis task pages are unusable
https://bugs.webkit.org/show_bug.cgi?id=141786

Reviewed by Andreas Kling.

This patch makes following improvements to analysis task pages:
1. Making the main chart interactive. This change required the use of App.Pane as well as moving the code to
compute the data for the details pane from PaneController.
2. Moving the form to add a new test group to the top of test groups instead of the bottom of them.
3. Grouping the build requests in each test group by root sets instead of the order by which they were ran.
This change required the creation of App.TestGroupPane as well as its methods.
4. Show a box plot for each root set configuration as well as each build request. This change required
App.BoxPlotComponent.
5. Show revisions of each repository (e.g. WebKit) for each root set and build request.

* public/api/build-requests.php:
(main): Update per the rename of BuildRequestsFetcher::root_sets to BuildRequestsFetcher::root_sets_by_id.

* public/api/test-groups.php:
(main): Include root sets and roots in the response.
(format_test_group):

* public/include/build-requests-fetcher.php:
(BuildRequestsFetcher::root_sets_by_id): Renamed from root_sets.
(BuildRequestsFetcher::root_sets): Added.
(BuildRequestsFetcher::roots): Added.
(BuildRequestsFetcher::fetch_roots_for_set): Takes a boolean argument $resolve_ids. This flag is only set to
true in /api/build-requests/ (as done prior to this patch) to use repository names as identifiers since
tools/sync-with-buildbot.py can't convert repository names to their ids.

* public/v2/analysis.js:
(App.Root): Added.
(App.RootSet): Added.
(App.RootSet.revisionForRepository): Added.
(App.TestGroup.rootSets): Deleted the code to compute root set ids from build requests now that the JSON
response at /api/test-groups will include them.
(App.BuildRequest): Ditto. Also deleted 'configLetter' property, which has been moved to a proxy created by
_createConfigurationSummary.
(App.BuildRequest.statusLabel): Use 'Completed' as the human readable label for 'completed' status.
(App.BuildRequest.aggregateStatuses): Added. Generates a human readable status for a set of build requests.

* public/v2/app.css: Updated style rules for analysis task pages.

* public/v2/app.js:
(App.Pane): This class is now used in analysis task pages to make the main chart interactive.
(App.Pane._updateDetails): Moved from App.PaneController.

(App.PaneController._updateCanAnalyze): Updated the code per the move of selectedPoints.

(App.AnalysisTaskController): Added 'details'.
(App.AnalysisTaskController._taskUpdated):
(App.AnalysisTaskController.paneDomain):Renamed from _fetchedRuns.
(App.AnalysisTaskController.updateTestGroupPanes): Added. Creates App.TestGroupPane for each test group.
(App.AnalysisTaskController.actions.toggleShowRequestList): Added.

(App.TestGroupPane): Added.
(App.TestGroupPane._populate): Added. Group build requests by root sets and create a summary for each group.
(App.TestGroupPane._computeRepositoryList): Added. Returns a sorted list of repositories which is the union
of all repositories appearing in root sets and builds associated with A/B testing results.
(App.TestGroupPane._groupRequestsByConfigurations): Added. Groups build requests by root sets.
(App.TestGroupPane._createConfigurationSummary): Added. Creates a summary for a group of build requests that
use the same root set. We start by wrapping &quot;raw&quot; build requests in a proxy with formatted values,
build numbers, etc... obtained from the fetched chart data. The list of revisions shown in the group summary
is a union of revisions in the root set and the first build request in the group. We null-out revision info
for a build request if it is identical to the one in the summary. The range of values is expanded as needed
by the values in the group as well as 95% percentile confidence interval.

(App.BoxPlotComponent): Added. Controls a box plot shown for each test group summary and build request.
(App.BoxPlotComponent.didInsertElement): Added. Inserts a SVG element as well as two indicator rects to show
the mean and the confidence interval.
(App.BoxPlotComponent._updateBars): Added. Updates the dimensions of the indicator rects.
(App.BoxPlotComponent.valueChanged): Added. Computes the relative dimensions of the indicator rects and
calls _updateBars to update the rects.

* public/v2/chart-pane.css: Added some style rules to be used in the details pane in analysis task pages.

* public/v2/data.js:
(Measurement.prototype.formattedRevisions):
(Measurement.formatRevisionRange): Renamed from Measurement.prototype._formatRevisionRange so that it can be
called in _createConfigurationSummary.

* public/v2/index.html: Updated the templates for analysis task pages. Moved the form to create a new test
group above all test groups, and replaced the list of data points by &quot;details&quot; pane used in the charts page.
Also made the fetching of chartData no longer block showing of test groups.

* public/v2/interactive-chart.js:
(App.InteractiveChartComponent._updateDomain): Added an early exit to fix a newly revealed race condition.
(App.InteractiveChartComponent._domainChanged): Ditto.
(App.InteractiveChartComponent._updateSelectionToolbar): Made it respect 'zoomable' boolean property.

* public/v2/js/statistics.js:
(Statistics.min): Added.
(Statistics.max): Added.

* public/v2/manifest.js:
(App.Manifest.fetchRunsWithPlatformAndMetric): Added formatWithDeltaAndUnit to be used in _createConfigurationSummary.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapibuildrequestsphp">trunk/Websites/perf.webkit.org/public/api/build-requests.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapitestgroupsphp">trunk/Websites/perf.webkit.org/public/api/test-groups.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludebuildrequestsfetcherphp">trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2analysisjs">trunk/Websites/perf.webkit.org/public/v2/analysis.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appcss">trunk/Websites/perf.webkit.org/public/v2/app.css</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appjs">trunk/Websites/perf.webkit.org/public/v2/app.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2chartpanecss">trunk/Websites/perf.webkit.org/public/v2/chart-pane.css</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2datajs">trunk/Websites/perf.webkit.org/public/v2/data.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2indexhtml">trunk/Websites/perf.webkit.org/public/v2/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2interactivechartjs">trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2jsstatisticsjs">trunk/Websites/perf.webkit.org/public/v2/js/statistics.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2manifestjs">trunk/Websites/perf.webkit.org/public/v2/manifest.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 (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -1,3 +1,102 @@
</span><ins>+2015-02-18  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Analysis task pages are unusable
+        https://bugs.webkit.org/show_bug.cgi?id=141786
+
+        Reviewed by Andreas Kling.
+
+        This patch makes following improvements to analysis task pages:
+        1. Making the main chart interactive. This change required the use of App.Pane as well as moving the code to
+        compute the data for the details pane from PaneController.
+        2. Moving the form to add a new test group to the top of test groups instead of the bottom of them.
+        3. Grouping the build requests in each test group by root sets instead of the order by which they were ran.
+        This change required the creation of App.TestGroupPane as well as its methods.
+        4. Show a box plot for each root set configuration as well as each build request. This change required
+        App.BoxPlotComponent.
+        5. Show revisions of each repository (e.g. WebKit) for each root set and build request.
+
+        * public/api/build-requests.php:
+        (main): Update per the rename of BuildRequestsFetcher::root_sets to BuildRequestsFetcher::root_sets_by_id.
+
+        * public/api/test-groups.php:
+        (main): Include root sets and roots in the response.
+        (format_test_group):
+
+        * public/include/build-requests-fetcher.php:
+        (BuildRequestsFetcher::root_sets_by_id): Renamed from root_sets.
+        (BuildRequestsFetcher::root_sets): Added.
+        (BuildRequestsFetcher::roots): Added.
+        (BuildRequestsFetcher::fetch_roots_for_set): Takes a boolean argument $resolve_ids. This flag is only set to
+        true in /api/build-requests/ (as done prior to this patch) to use repository names as identifiers since
+        tools/sync-with-buildbot.py can't convert repository names to their ids.
+
+        * public/v2/analysis.js:
+        (App.Root): Added.
+        (App.RootSet): Added.
+        (App.RootSet.revisionForRepository): Added.
+        (App.TestGroup.rootSets): Deleted the code to compute root set ids from build requests now that the JSON
+        response at /api/test-groups will include them.
+        (App.BuildRequest): Ditto. Also deleted 'configLetter' property, which has been moved to a proxy created by
+        _createConfigurationSummary.
+        (App.BuildRequest.statusLabel): Use 'Completed' as the human readable label for 'completed' status.
+        (App.BuildRequest.aggregateStatuses): Added. Generates a human readable status for a set of build requests.
+
+        * public/v2/app.css: Updated style rules for analysis task pages.
+
+        * public/v2/app.js:
+        (App.Pane): This class is now used in analysis task pages to make the main chart interactive.
+        (App.Pane._updateDetails): Moved from App.PaneController.
+
+        (App.PaneController._updateCanAnalyze): Updated the code per the move of selectedPoints.
+
+        (App.AnalysisTaskController): Added 'details'.
+        (App.AnalysisTaskController._taskUpdated):
+        (App.AnalysisTaskController.paneDomain):Renamed from _fetchedRuns.
+        (App.AnalysisTaskController.updateTestGroupPanes): Added. Creates App.TestGroupPane for each test group.
+        (App.AnalysisTaskController.actions.toggleShowRequestList): Added.
+
+        (App.TestGroupPane): Added.
+        (App.TestGroupPane._populate): Added. Group build requests by root sets and create a summary for each group.
+        (App.TestGroupPane._computeRepositoryList): Added. Returns a sorted list of repositories which is the union
+        of all repositories appearing in root sets and builds associated with A/B testing results.
+        (App.TestGroupPane._groupRequestsByConfigurations): Added. Groups build requests by root sets.
+        (App.TestGroupPane._createConfigurationSummary): Added. Creates a summary for a group of build requests that
+        use the same root set. We start by wrapping &quot;raw&quot; build requests in a proxy with formatted values,
+        build numbers, etc... obtained from the fetched chart data. The list of revisions shown in the group summary
+        is a union of revisions in the root set and the first build request in the group. We null-out revision info
+        for a build request if it is identical to the one in the summary. The range of values is expanded as needed
+        by the values in the group as well as 95% percentile confidence interval.
+
+        (App.BoxPlotComponent): Added. Controls a box plot shown for each test group summary and build request.
+        (App.BoxPlotComponent.didInsertElement): Added. Inserts a SVG element as well as two indicator rects to show
+        the mean and the confidence interval.
+        (App.BoxPlotComponent._updateBars): Added. Updates the dimensions of the indicator rects.
+        (App.BoxPlotComponent.valueChanged): Added. Computes the relative dimensions of the indicator rects and
+        calls _updateBars to update the rects.
+
+        * public/v2/chart-pane.css: Added some style rules to be used in the details pane in analysis task pages.
+
+        * public/v2/data.js:
+        (Measurement.prototype.formattedRevisions):
+        (Measurement.formatRevisionRange): Renamed from Measurement.prototype._formatRevisionRange so that it can be
+        called in _createConfigurationSummary.
+
+        * public/v2/index.html: Updated the templates for analysis task pages. Moved the form to create a new test
+        group above all test groups, and replaced the list of data points by &quot;details&quot; pane used in the charts page.
+        Also made the fetching of chartData no longer block showing of test groups.
+
+        * public/v2/interactive-chart.js:
+        (App.InteractiveChartComponent._updateDomain): Added an early exit to fix a newly revealed race condition.
+        (App.InteractiveChartComponent._domainChanged): Ditto.
+        (App.InteractiveChartComponent._updateSelectionToolbar): Made it respect 'zoomable' boolean property.
+
+        * public/v2/js/statistics.js:
+        (Statistics.min): Added.
+        (Statistics.max): Added.
+
+        * public/v2/manifest.js:
+        (App.Manifest.fetchRunsWithPlatformAndMetric): Added formatWithDeltaAndUnit to be used in _createConfigurationSummary.
+
</ins><span class="cx"> 2015-02-14  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Build URL on new perf dashboard doesn't resolve $builderName
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapibuildrequestsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/build-requests.php (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/build-requests.php        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/api/build-requests.php        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -45,7 +45,7 @@
</span><span class="cx"> 
</span><span class="cx">     exit_with_success(array(
</span><span class="cx">         'buildRequests' =&gt; $requests_fetcher-&gt;results_with_resolved_ids(),
</span><del>-        'rootSets' =&gt; $requests_fetcher-&gt;root_sets(),
</del><ins>+        'rootSets' =&gt; $requests_fetcher-&gt;root_sets_by_id(),
</ins><span class="cx">         'updates' =&gt; $updates,
</span><span class="cx">     ));
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapitestgroupsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/test-groups.php (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/test-groups.php        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/api/test-groups.php        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -39,10 +39,16 @@
</span><span class="cx">         $group_by_id[$group['id']] = &amp;$group;
</span><span class="cx"> 
</span><span class="cx">     $build_requests = $build_requests_fetcher-&gt;results();
</span><del>-    foreach ($build_requests as $request)
-        array_push($group_by_id[$request['testGroup']]['buildRequests'], $request['id']);
</del><ins>+    foreach ($build_requests as $request) {
+        $request_group = &amp;$group_by_id[$request['testGroup']];
+        array_push($request_group['buildRequests'], $request['id']);
+        array_push($request_group['rootSets'], $request['rootSet']);
+    }
</ins><span class="cx"> 
</span><del>-    exit_with_success(array('testGroups' =&gt; $test_groups, 'buildRequests' =&gt; $build_requests));
</del><ins>+    exit_with_success(array('testGroups' =&gt; $test_groups,
+        'buildRequests' =&gt; $build_requests,
+        'rootSets' =&gt; $build_requests_fetcher-&gt;root_sets(),
+        'roots' =&gt; $build_requests_fetcher-&gt;roots()));
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> function format_test_group($group_row) {
</span><span class="lines">@@ -53,6 +59,7 @@
</span><span class="cx">         'author' =&gt; $group_row['testgroup_author'],
</span><span class="cx">         'createdAt' =&gt; strtotime($group_row['testgroup_created_at']) * 1000,
</span><span class="cx">         'buildRequests' =&gt; array(),
</span><ins>+        'rootSets' =&gt; array(),
</ins><span class="cx">     );
</span><span class="cx"> }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludebuildrequestsfetcherphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -6,6 +6,8 @@
</span><span class="cx">     function __construct($db) {
</span><span class="cx">         $this-&gt;db = $db;
</span><span class="cx">         $this-&gt;rows = null;
</span><ins>+        $this-&gt;root_sets = array();
+        $this-&gt;roots = array();
</ins><span class="cx">         $this-&gt;root_sets_by_id = array();
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -50,7 +52,7 @@
</span><span class="cx">             $root_set_id = $row['request_root_set'];
</span><span class="cx"> 
</span><span class="cx">             if (!array_key_exists($root_set_id, $this-&gt;root_sets_by_id))
</span><del>-                $this-&gt;root_sets_by_id[$root_set_id] = $this-&gt;fetch_roots_for_set($root_set_id);
</del><ins>+                $this-&gt;root_sets_by_id[$root_set_id] = $this-&gt;fetch_roots_for_set($root_set_id, $resolve_ids);
</ins><span class="cx"> 
</span><span class="cx">             array_push($requests, array(
</span><span class="cx">                 'id' =&gt; $row['request_id'],
</span><span class="lines">@@ -69,18 +71,34 @@
</span><span class="cx">         return $requests;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    function root_sets() {
</del><ins>+    function root_sets_by_id() {
</ins><span class="cx">         return $this-&gt;root_sets_by_id;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    private function fetch_roots_for_set($root_set_id) {
</del><ins>+    function root_sets() {
+        return $this-&gt;root_sets;
+    }
+
+    function roots() {
+        return $this-&gt;roots;
+    }
+
+    private function fetch_roots_for_set($root_set_id, $resolve_ids) {
</ins><span class="cx">         $root_rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT *
</span><span class="cx">             FROM roots, commits LEFT OUTER JOIN repositories ON commit_repository = repository_id
</span><span class="cx">             WHERE root_commit = commit_id AND root_set = $1', array($root_set_id));
</span><span class="cx"> 
</span><span class="cx">         $roots = array();
</span><del>-        foreach ($root_rows as $row)
-            $roots[$row['repository_name']] = $row['commit_revision'];
</del><ins>+        $root_ids = array();
+        foreach ($root_rows as $row) {
+            $repository = $row['repository_id'];
+            $revision = $row['commit_revision'];
+            $root_id = $root_set_id . '-' . $repository;
+            array_push($root_ids, $root_id);
+            array_push($this-&gt;roots, array('id' =&gt; $root_id, 'repository' =&gt; $repository, 'revision' =&gt; $revision));
+            $roots[$resolve_ids ? $row['repository_name'] : $row['repository_id']] = $revision;
+        }
+        array_push($this-&gt;root_sets, array('id' =&gt; $root_set_id, 'roots' =&gt; $root_ids));
</ins><span class="cx"> 
</span><span class="cx">         return $roots;
</span><span class="cx">     }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2analysisjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/analysis.js (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/analysis.js        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/analysis.js        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -65,21 +65,28 @@
</span><span class="cx">     }
</span><span class="cx"> });
</span><span class="cx"> 
</span><ins>+App.Root = App.Model.extend({
+    repository: DS.belongsTo('repository'),
+    revision: DS.attr('string'),
+});
+
+App.RootSet = App.Model.extend({
+    roots: DS.hasMany('roots'),
+    revisionForRepository: function (repository)
+    {
+        var root = this.get('roots').findBy('repository', repository);
+        if (!root)
+            return null;
+        return root.get('revision');
+    }
+});
+
</ins><span class="cx"> App.TestGroup = App.NameLabelModel.extend({
</span><span class="cx">     task: DS.belongsTo('analysisTask'),
</span><span class="cx">     author: DS.attr('string'),
</span><span class="cx">     createdAt: DS.attr('date'),
</span><span class="cx">     buildRequests: DS.hasMany('buildRequests'),
</span><del>-    rootSets: function ()
-    {
-        var rootSetIds = [];
-        this.get('buildRequests').forEach(function (request) {
-            var rootSet = request.get('rootSet');
-            if (!rootSetIds.contains(rootSet))
-                rootSetIds.push(rootSet);
-        });
-        return rootSetIds;
-    }.property('buildRequests'),
</del><ins>+    rootSets: DS.hasMany('rootSets'),
</ins><span class="cx">     _fetchChartData: function ()
</span><span class="cx">     {
</span><span class="cx">         var task = this.get('task');
</span><span class="lines">@@ -144,13 +151,7 @@
</span><span class="cx">     {
</span><span class="cx">         return this.get('order') + 1;
</span><span class="cx">     }.property('order'),
</span><del>-    rootSet: DS.attr('number'),
-    configLetter: function ()
-    {
-        var rootSets = this.get('testGroup').get('rootSets');
-        var index = rootSets.indexOf(this.get('rootSet'));
-        return String.fromCharCode('A'.charCodeAt(0) + index);
-    }.property('testGroup', 'testGroup.rootSets'),
</del><ins>+    rootSet: DS.belongsTo('rootSet'),
</ins><span class="cx">     status: DS.attr('string'),
</span><span class="cx">     statusLabel: function ()
</span><span class="cx">     {
</span><span class="lines">@@ -164,24 +165,33 @@
</span><span class="cx">         case 'failed':
</span><span class="cx">             return 'Failed';
</span><span class="cx">         case 'completed':
</span><del>-            return 'Finished';
</del><ins>+            return 'Completed';
</ins><span class="cx">         }
</span><span class="cx">     }.property('status'),
</span><span class="cx">     url: DS.attr('string'),
</span><span class="cx">     build: DS.attr('number'),
</span><del>-    _fetchMean: function ()
-    {
-        var testGroup = this.get('testGroup');
-        if (!testGroup)
-            return;
-        var chartData = testGroup.get('chartData');
-        if (!chartData)
-            return;
</del><ins>+});
</ins><span class="cx"> 
</span><del>-        var point = chartData.current.findPointByBuild(this.get('build'));
-        if (!point)
-            return;
-        this.set('mean', chartData.formatter(point.value) + (chartData.unit ? ' ' + chartData.unit : ''));
-        this.set('buildNumber', point.measurement.buildNumber());
-    }.observes('build', 'testGroup', 'testGroup.chartData').on('init'),
-});
</del><ins>+App.BuildRequest.aggregateStatuses = function (requests)
+{
+    var completeCount = 0;
+    var failureCount = 0;
+    requests.forEach(function (request) {
+        switch (request.get('status')) {
+        case 'failed':
+            failureCount++;
+            break;
+        case 'completed':
+            completeCount++;
+            break;
+        }
+    });
+    if (completeCount == requests.length)
+        return 'Done';
+    if (failureCount == requests.length)
+        return 'All failed';
+    var status = completeCount + ' out of ' + requests.length + ' completed';
+    if (failureCount)
+        status += ', ' + failureCount + ' failed';
+    return status;
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appcss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.css (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.css        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/app.css        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -451,6 +451,49 @@
</span><span class="cx">     border-top: solid 1px #ddd;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.analysis-group .results .summary td {
+    vertical-align: top;
+}
+
+.analysis-group .results thead td {
+    text-align: center;
+}
+
+.analysis-group .results .config-letter,
+.analysis-group .results .summary {
+    cursor: pointer;
+}
+
+.analysis-group .results .request .config-letter {
+    border-color: transparent;
+}
+
+.analysis-group .results .hideRequests .request {
+    display: none;
+}
+
+.box-plot {
+    display: inline-block;
+    width: 100px;
+    height: 0.6rem;
+    border: solid 1px #ddd;
+    padding: 1px;
+    vertical-align: middle;
+}
+
+.box-plot .percentage {
+    fill: #ccc;
+}
+
+.box-plot .delta {
+    fill: #333;
+    opacity: 0.5;
+}
+
+.box-plot svg {
+    display: block;
+}
+
</ins><span class="cx"> #analysis-task-title {
</span><span class="cx">     font-weight: normal;
</span><span class="cx">     font-size: 1.2rem;
</span><span class="lines">@@ -470,17 +513,29 @@
</span><span class="cx">     border: 1px solid #bbb;
</span><span class="cx">     border-radius: 0.5rem;
</span><span class="cx">     box-shadow: rgba(0, 0, 0, 0.03) 1px 1px 0px 0px;
</span><del>-
-    padding: 0.5rem 1rem;
</del><span class="cx">     margin-bottom: 1.5rem;
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-.analysis-group caption {
</del><ins>+.analysis-group &gt; * {
+    margin: 0.5rem;
+}
+
+.analysis-group &gt; h1 {
</ins><span class="cx">     font-size: 1.1rem;
</span><ins>+    font-weight: normal;
</ins><span class="cx">     text-align: left;
</span><span class="cx">     margin-bottom: 0.5rem;
</span><ins>+    border-bottom: 1px solid #bbb;
+    margin: 0;
+    padding: 0.2rem 0.5rem;
</ins><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.analysis-group h1 &gt; input {
+    font-size: 1rem;
+    min-width: 20rem;
+    margin: 0.2rem 0;
+}
+
</ins><span class="cx"> .analysis-bugs th {
</span><span class="cx">     font-weight: normal;
</span><span class="cx">     text-align: right;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.js        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -297,6 +297,8 @@
</span><span class="cx">     metricId: null,
</span><span class="cx">     metric: null,
</span><span class="cx">     selectedItem: null,
</span><ins>+    selectedPoints: null,
+    hoveredOrSelectedItem: null,
</ins><span class="cx">     showFullYAxis: false,
</span><span class="cx">     searchCommit: function (repository, keyword) {
</span><span class="cx">         var self = this;
</span><span class="lines">@@ -538,6 +540,56 @@
</span><span class="cx">         if (JSON.stringify(config) != JSON.stringify(this.get(configName)))
</span><span class="cx">             this.set(configName, config);
</span><span class="cx">     },
</span><ins>+    _updateDetails: function ()
+    {
+        var selectedPoints = this.get('selectedPoints');
+        var currentPoint = this.get('hoveredOrSelectedItem');
+        if (!selectedPoints &amp;&amp; !currentPoint) {
+            this.set('details', null);
+            return;
+        }
+
+        var currentMeasurement;
+        var previousPoint;
+        if (!selectedPoints)
+            previousPoint = currentPoint.series.previousPoint(currentPoint);
+        else {
+            currentPoint = selectedPoints[selectedPoints.length - 1];
+            previousPoint = selectedPoints[0];
+        }
+        var currentMeasurement = currentPoint.measurement;
+        var oldMeasurement = previousPoint ? previousPoint.measurement : null;
+
+        var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
+        var revisions = App.Manifest.get('repositories')
+            .filter(function (repository) { return formattedRevisions[repository.get('id')]; })
+            .map(function (repository) {
+            var revision = Ember.Object.create(formattedRevisions[repository.get('id')]);
+            revision['url'] = revision.previousRevision
+                ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
+                : repository.urlForRevision(revision.currentRevision);
+            revision['name'] = repository.get('name');
+            revision['repository'] = repository;
+            return revision; 
+        });
+
+        var buildNumber = null;
+        var buildURL = null;
+        if (!selectedPoints) {
+            buildNumber = currentMeasurement.buildNumber();
+            var builder = App.Manifest.builder(currentMeasurement.builderId());
+            if (builder)
+                buildURL = builder.urlFromBuildNumber(buildNumber);
+        }
+
+        this.set('details', Ember.Object.create({
+            status: this.computeStatus(currentPoint, previousPoint),
+            buildNumber: buildNumber,
+            buildURL: buildURL,
+            buildTime: currentMeasurement.formattedBuildTime(),
+            revisions: revisions,
+        }));
+    }.observes('hoveredOrSelectedItem', 'selectedPoints'),
</ins><span class="cx"> });
</span><span class="cx"> 
</span><span class="cx"> App.encodePrettifiedJSON = function (plain)
</span><span class="lines">@@ -900,65 +952,13 @@
</span><span class="cx">         this.set('mainPlotDomain', newSelection || this.get('overviewDomain'));
</span><span class="cx">         this.set('overviewSelection', newSelection);
</span><span class="cx">     }.observes('parentController.sharedZoom').on('init'),
</span><del>-    _updateDetails: function ()
-    {
-        var selectedPoints = this.get('selectedPoints');
-        var currentPoint = this.get('currentItem');
-        if (!selectedPoints &amp;&amp; !currentPoint) {
-            this.set('details', null);
-            return;
-        }
-
-        var currentMeasurement;
-        var previousPoint;
-        if (!selectedPoints)
-            previousPoint = currentPoint.series.previousPoint(currentPoint);
-        else {
-            currentPoint = selectedPoints[selectedPoints.length - 1];
-            previousPoint = selectedPoints[0];
-        }
-        var currentMeasurement = currentPoint.measurement;
-        var oldMeasurement = previousPoint ? previousPoint.measurement : null;
-
-        var formattedRevisions = currentMeasurement.formattedRevisions(oldMeasurement);
-        var revisions = App.Manifest.get('repositories')
-            .filter(function (repository) { return formattedRevisions[repository.get('id')]; })
-            .map(function (repository) {
-            var revision = Ember.Object.create(formattedRevisions[repository.get('id')]);
-            revision['url'] = revision.previousRevision
-                ? repository.urlForRevisionRange(revision.previousRevision, revision.currentRevision)
-                : repository.urlForRevision(revision.currentRevision);
-            revision['name'] = repository.get('name');
-            revision['repository'] = repository;
-            return revision; 
-        });
-
-        var buildNumber = null;
-        var buildURL = null;
-        if (!selectedPoints) {
-            buildNumber = currentMeasurement.buildNumber();
-            var builder = App.Manifest.builder(currentMeasurement.builderId());
-            if (builder)
-                buildURL = builder.urlFromBuildNumber(buildNumber);
-        }
-
-        this.set('details', Ember.Object.create({
-            status: this.get('model').computeStatus(currentPoint, previousPoint),
-            buildNumber: buildNumber,
-            buildURL: buildURL,
-            buildTime: currentMeasurement.formattedBuildTime(),
-            revisions: revisions,
-        }));
-        this._updateCanAnalyze();
-    }.observes('currentItem', 'selectedPoints'),
</del><span class="cx">     _updateCanAnalyze: function ()
</span><span class="cx">     {
</span><del>-        var points = this.get('selectedPoints');
</del><ins>+        var points = this.get('model').get('selectedPoints');
</ins><span class="cx">         this.set('cannotAnalyze', !this.get('newAnalysisTaskName') || !points || points.length &lt; 2);
</span><del>-    }.observes('newAnalysisTaskName'),
</del><ins>+    }.observes('newAnalysisTaskName', 'model.selectedPoints'),
</ins><span class="cx"> });
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> App.AnalysisRoute = Ember.Route.extend({
</span><span class="cx">     model: function () {
</span><span class="cx">         return this.store.findAll('analysisTask').then(function (tasks) {
</span><span class="lines">@@ -978,7 +978,7 @@
</span><span class="cx">     label: Ember.computed.alias('model.name'),
</span><span class="cx">     platform: Ember.computed.alias('model.platform'),
</span><span class="cx">     metric: Ember.computed.alias('model.metric'),
</span><del>-    testGroups: Ember.computed.alias('model.testGroups'),
</del><ins>+    details: Ember.computed.alias('pane.details'),
</ins><span class="cx">     testSets: [],
</span><span class="cx">     roots: [],
</span><span class="cx">     bugTrackers: [],
</span><span class="lines">@@ -989,10 +989,12 @@
</span><span class="cx">         if (!model)
</span><span class="cx">             return;
</span><span class="cx"> 
</span><del>-        var platformId = model.get('platform').get('id');
-        var metricId = model.get('metric').get('id');
</del><span class="cx">         App.Manifest.fetch(this.store).then(this._fetchedManifest.bind(this));
</span><del>-        App.Manifest.fetchRunsWithPlatformAndMetric(this.store, platformId, metricId).then(this._fetchedRuns.bind(this));
</del><ins>+        this.set('pane', App.Pane.create({
+            store: this.store,
+            platformId: model.get('platform').get('id'),
+            metricId: model.get('metric').get('id'),
+        }));
</ins><span class="cx">     }.observes('model').on('init'),
</span><span class="cx">     _fetchedManifest: function ()
</span><span class="cx">     {
</span><span class="lines">@@ -1010,17 +1012,24 @@
</span><span class="cx">             });
</span><span class="cx">         }));
</span><span class="cx">     },
</span><del>-    _fetchedRuns: function (result)
</del><ins>+    paneDomain: function ()
</ins><span class="cx">     {
</span><del>-        var chartData = result.data;
</del><ins>+        var pane = this.get('pane');
+        if (!pane)
+            return;
+
+        var chartData = pane.get('chartData');
+        if (!chartData)
+            return null;
+
</ins><span class="cx">         var currentTimeSeries = chartData.current;
</span><span class="cx">         if (!currentTimeSeries)
</span><del>-            return; // FIXME: Report an error.
</del><ins>+            return null; // FIXME: Report an error.
</ins><span class="cx"> 
</span><span class="cx">         var start = currentTimeSeries.findPointByMeasurementId(this.get('model').get('startRun'));
</span><span class="cx">         var end = currentTimeSeries.findPointByMeasurementId(this.get('model').get('endRun'));
</span><span class="cx">         if (!start || !end)
</span><del>-            return; // FIXME: Report an error.
</del><ins>+            return null; // FIXME: Report an error.
</ins><span class="cx"> 
</span><span class="cx">         var highlightedItems = {};
</span><span class="cx">         highlightedItems[start.measurement.id()] = true;
</span><span class="lines">@@ -1031,16 +1040,16 @@
</span><span class="cx">                 id: point.measurement.id(),
</span><span class="cx">                 measurement: point.measurement,
</span><span class="cx">                 label: 'Point ' + (index + 1),
</span><del>-                value: chartData.formatter(point.value) + (chartData.unit ? ' ' + chartData.unit : ''),
</del><ins>+                value: chartData.formatWithUnit(point.value),
</ins><span class="cx">             };
</span><span class="cx">         });
</span><span class="cx"> 
</span><span class="cx">         var margin = (end.time - start.time) * 0.1;
</span><del>-        this.set('chartData', chartData);
-        this.set('chartDomain', [start.time - margin, +end.time + margin]);
</del><span class="cx">         this.set('highlightedItems', highlightedItems);
</span><span class="cx">         this.set('analysisPoints', formatedPoints);
</span><del>-    },
</del><ins>+
+        return [start.time - margin, +end.time + margin];
+    }.property('pane.chartData', 'model', 'model'),
</ins><span class="cx">     testSets: function ()
</span><span class="cx">     {
</span><span class="cx">         var analysisPoints = this.get('analysisPoints');
</span><span class="lines">@@ -1115,6 +1124,16 @@
</span><span class="cx">             }));
</span><span class="cx">         });
</span><span class="cx">     }.observes('analysisPoints'),
</span><ins>+    updateTestGroupPanes: function ()
+    {
+        var model = this.get('model');
+        if (!model)
+            return;
+        var self = this;
+        model.get('testGroups').then(function (groups) {
+            self.set('testGroupPanes', groups.map(function (group) { return App.TestGroupPane.create({content: group}); }));
+        });
+    }.observes('model'),
</ins><span class="cx">     actions: {
</span><span class="cx">         associateBug: function (bugTracker, bugNumber)
</span><span class="cx">         {
</span><span class="lines">@@ -1136,5 +1155,190 @@
</span><span class="cx">                 
</span><span class="cx">             });
</span><span class="cx">         },
</span><ins>+        toggleShowRequestList: function (configuration)
+        {
+            configuration.toggleProperty('showRequestList');
+        }
</ins><span class="cx">     },
</span><span class="cx"> });
</span><ins>+
+App.TestGroupPane = Ember.ObjectProxy.extend({
+    _populate: function ()
+    {
+        var buildRequests = this.get('buildRequests');
+        var chartData = this.get('chartData');
+        if (!buildRequests || !chartData)
+            return [];
+
+        var repositories = this._computeRepositoryList();
+        this.set('repositories', repositories);
+
+        var requestsByRooSet = this._groupRequestsByConfigurations(buildRequests);
+
+        var configurations = [];
+        var index = 0;
+        var range = {min: Infinity, max: -Infinity};
+        for (var rootSetId in requestsByRooSet) {
+            var configLetter = String.fromCharCode('A'.charCodeAt(0) + index++);
+            configurations.push(this._createConfigurationSummary(requestsByRooSet[rootSetId], configLetter, range));
+        }
+
+        var margin = 0.1 * (range.max - range.min);
+        range.max += margin;
+        range.min -= margin;
+
+        this.set('configurations', configurations);
+    }.observes('chartData', 'buildRequests'),
+    _computeRepositoryList: function ()
+    {
+        var specifiedRepositories = new Ember.Set();
+        (this.get('rootSets') || []).forEach(function (rootSet) {
+            (rootSet.get('roots') || []).forEach(function (root) {
+                specifiedRepositories.add(root.get('repository'));
+            });
+        });
+        var reportedRepositories = new Ember.Set();
+        var chartData = this.get('chartData');
+        (this.get('buildRequests') || []).forEach(function (request) {
+            var point = chartData.current.findPointByBuild(request.get('build'));
+            if (!point)
+                return;
+
+            var revisionByRepositoryId = point.measurement.formattedRevisions();
+            for (var repositoryId in revisionByRepositoryId) {
+                var repository = App.Manifest.repository(repositoryId);
+                if (!specifiedRepositories.contains(repository))
+                    reportedRepositories.add(repository);
+            }
+        });
+        return specifiedRepositories.sortBy('name').concat(reportedRepositories.sortBy('name'));
+    },
+    _groupRequestsByConfigurations: function (requests, repositoryList)
+    {
+        var rootSetIdToRequests = {};
+        var testGroup = this;
+        requests.forEach(function (request) {
+            var rootSetId = request.get('rootSet').get('id');
+            if (!rootSetIdToRequests[rootSetId])
+                rootSetIdToRequests[rootSetId] = [];
+            rootSetIdToRequests[rootSetId].push(request);
+        });
+        return rootSetIdToRequests;
+    },
+    _createConfigurationSummary: function (buildRequests, configLetter, range)
+    {
+        var repositories = this.get('repositories');
+        var chartData = this.get('chartData');
+        var requests = buildRequests.map(function (originalRequest) {
+            var point = chartData.current.findPointByBuild(originalRequest.get('build'));
+            var revisionByRepositoryId = point ? point.measurement.formattedRevisions() : {};
+            return Ember.ObjectProxy.create({
+                content: originalRequest,
+                revisions: repositories.map(function (repository, index) {
+                    return (revisionByRepositoryId[repository.get('id')] || {label:null}).label;
+                }),
+                value: point ? point.value : null,
+                valueRange: range,
+                formattedValue: point ? chartData.formatWithUnit(point.value) : null,
+                buildNumber: point ? point.measurement.buildNumber() : null,
+            });
+        });
+
+        var rootSet = requests ? requests[0].get('rootSet') : null;
+        var summaryRevisions = repositories.map(function (repository, index) {
+            var revision = rootSet ? rootSet.revisionForRepository(repository) : null;
+            if (!revision)
+                return requests[0].get('revisions')[index];
+            return Measurement.formatRevisionRange(revision).label;
+        });
+
+        requests.forEach(function (request) {
+            var revisions = request.get('revisions');
+            repositories.forEach(function (repository, index) {
+                if (revisions[index] == summaryRevisions[index])
+                    revisions[index] = null;
+            });
+        });
+
+        var valuesInConfig = requests.mapBy('value').filter(function (value) { return typeof(value) === 'number' &amp;&amp; !isNaN(value); });
+        var sum = Statistics.sum(valuesInConfig);
+        var ciDelta = Statistics.confidenceIntervalDelta(0.95, valuesInConfig.length, sum, Statistics.squareSum(valuesInConfig));
+        var mean = sum / valuesInConfig.length;
+
+        range.min = Math.min(range.min, Statistics.min(valuesInConfig));
+        range.max = Math.max(range.max, Statistics.max(valuesInConfig));
+        if (ciDelta &amp;&amp; !isNaN(ciDelta)) {
+            range.min = Math.min(range.min, mean - ciDelta);
+            range.max = Math.max(range.max, mean + ciDelta);
+        }
+
+        var summary = Ember.Object.create({
+            isAverage: true,
+            configLetter: configLetter,
+            revisions: summaryRevisions,
+            formattedValue: isNaN(mean) ? null : chartData.formatWithDeltaAndUnit(mean, ciDelta),
+            value: mean,
+            confidenceIntervalDelta: ciDelta,
+            valueRange: range,
+            statusLabel: App.BuildRequest.aggregateStatuses(requests),
+        });
+
+        return Ember.Object.create({summary: summary, items: requests});
+    },
+});
+
+App.BoxPlotComponent = Ember.Component.extend({
+    classNames: ['box-plot'],
+    range: null,
+    value: null,
+    delta: null,
+    didInsertElement: function ()
+    {
+        var element = this.get('element');
+        var svg = d3.select(element).append('svg')
+            .attr('viewBox', '0 0 100 20')
+            .attr('preserveAspectRatio', 'none')
+            .style({width: '100%', height: '100%'});
+
+        this._percentageRect = svg
+            .append('rect')
+            .attr('x', 0)
+            .attr('y', 0)
+            .attr('width', 0)
+            .attr('height', 20)
+            .attr('class', 'percentage');
+
+        this._deltaRect = svg
+            .append('rect')
+            .attr('x', 0)
+            .attr('y', 5)
+            .attr('width', 0)
+            .attr('height', 10)
+            .attr('class', 'delta')
+            .attr('opacity', 0.5)
+        this._updateBars();
+    },
+    _updateBars: function ()
+    {
+        if (!this._percentageRect || typeof(this._percentage) !== 'number' || isNaN(this._percentage))
+            return;
+
+        this._percentageRect.attr('width', this._percentage);
+        if (typeof(this._delta) === 'number' &amp;&amp; !isNaN(this._delta)) {
+            this._deltaRect.attr('x', this._percentage - this._delta);
+            this._deltaRect.attr('width', this._delta * 2);
+        }
+    },
+    valueChanged: function ()
+    {
+        var range = this.get('range');
+        var value = this.get('value');
+        if (!range || !value)
+            return;
+        var scalingFactor = 100 / (range.max - range.min);
+        var percentage = (value - range.min) * scalingFactor;
+        this._percentage = percentage;
+        this._delta = this.get('delta') * scalingFactor;
+        this._updateBars();
+    }.observes('value', 'range').on('init'),
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2chartpanecss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/chart-pane.css (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -217,6 +217,10 @@
</span><span class="cx">     border-left: solid 1px #bbb;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.analysis-chart-pane {
+    height: 15rem;
+}
+
</ins><span class="cx"> .analysis-chart-pane .details {
</span><span class="cx">     overflow: scroll;
</span><span class="cx"> }
</span><span class="lines">@@ -237,6 +241,10 @@
</span><span class="cx">     height: 13rem;
</span><span class="cx">     overflow: scroll;
</span><span class="cx"> }
</span><ins>+.analysis-chart-pane .details-table-container {
+    position: static;
+    height: 15rem;
+}
</ins><span class="cx"> 
</span><span class="cx"> .chart-pane .details-table,
</span><span class="cx"> .chart-pane .commits-viewer {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2datajs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/data.js (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/data.js        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/data.js        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -169,14 +169,14 @@
</span><span class="cx">     for (var repositoryId in revisions) {
</span><span class="cx">         var currentRevision = revisions[repositoryId][0];
</span><span class="cx">         var previousRevision = previousRevisions ? previousRevisions[repositoryId][0] : null;
</span><del>-        var formatttedRevision = this._formatRevisionRange(previousRevision, currentRevision);
</del><ins>+        var formatttedRevision = Measurement.formatRevisionRange(currentRevision, previousRevision);
</ins><span class="cx">         formattedRevisions[repositoryId] = formatttedRevision;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     return formattedRevisions;
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-Measurement.prototype._formatRevisionRange = function (previousRevision, currentRevision)
</del><ins>+Measurement.formatRevisionRange = function (currentRevision, previousRevision)
</ins><span class="cx"> {
</span><span class="cx">     var revisionChanged = false;
</span><span class="cx">     if (previousRevision == currentRevision)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/index.html (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/index.html        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/index.html        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -169,15 +169,15 @@
</span><span class="cx">                             domain=mainPlotDomain
</span><span class="cx">                             interactive=true
</span><span class="cx">                             chartPointRadius=2
</span><del>-                            currentItem=currentItem
</del><ins>+                            currentItem=hoveredOrSelectedItem
</ins><span class="cx">                             currentTime=sharedTime
</span><span class="cx">                             selectedItem=selectedItem
</span><span class="cx">                             highlightedItems=highlightedItems
</span><span class="cx">                             rangeRoute=&quot;analysisTask&quot;
</span><span class="cx">                             selection=timeRange
</span><span class="cx">                             selectedPoints=selectedPoints
</span><del>-                            markedPoints=markedPoints
</del><span class="cx">                             showFullYAxis=showFullYAxis
</span><ins>+                            zoomable=true
</ins><span class="cx">                             zoom=&quot;zoomed&quot;}}
</span><span class="cx">                     {{else}}
</span><span class="cx">                         {{#if failure}}
</span><span class="lines">@@ -197,9 +197,7 @@
</span><span class="cx">                                 selection=overviewSelection}}
</span><span class="cx">                         {{/if}}
</span><span class="cx">                         &lt;/div&gt;
</span><del>-                        {{#if details}}
-                            {{partial &quot;chart-details&quot;}}
-                        {{/if}}
</del><ins>+                        {{partial &quot;chart-details&quot;}}
</ins><span class="cx">                     &lt;/div&gt;
</span><span class="cx">                 &lt;/div&gt;
</span><span class="cx"> 
</span><span class="lines">@@ -248,6 +246,7 @@
</span><span class="cx">     &lt;/script&gt;
</span><span class="cx"> 
</span><span class="cx">     &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;chart-details&quot;&gt;
</span><ins>+    {{#if details}}
</ins><span class="cx">     &lt;div class=&quot;details-table-container&quot;&gt;
</span><span class="cx">         &lt;table class=&quot;details-table&quot;&gt;
</span><span class="cx">             &lt;tbody class=&quot;bugs&quot;&gt;
</span><span class="lines">@@ -313,6 +312,7 @@
</span><span class="cx">             {{/each}}
</span><span class="cx">         &lt;/div&gt;
</span><span class="cx">     &lt;/div&gt;
</span><ins>+    {{/if}}
</ins><span class="cx">     &lt;/script&gt;
</span><span class="cx"> 
</span><span class="cx">     &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;components/commits-viewer&quot;&gt;
</span><span class="lines">@@ -529,72 +529,96 @@
</span><span class="cx">             &lt;h3 id=&quot;analysis-task-testname&quot;&gt;{{metric.fullName}} - {{platform.label}}&lt;/h3&gt;
</span><span class="cx">         {{/if}}
</span><span class="cx"> 
</span><del>-        {{#if chartData}}
-            &lt;section class=&quot;analysis-chart-pane chart-pane&quot;&gt;
</del><ins>+        {{#if pane}}
+            &lt;section class=&quot;analysis-chart-pane chart-pane&quot; tabindex=&quot;0&quot;&gt;
</ins><span class="cx">                 &lt;div class=&quot;svg-container&quot;&gt;
</span><span class="cx">                     {{interactive-chart
</span><del>-                        chartData=chartData
-                        enableSelection=false
</del><ins>+                        chartData=pane.chartData
+                        ranges=pane.analyticRanges
+                        domain=paneDomain
+                        interactive=true
</ins><span class="cx">                         chartPointRadius=2
</span><del>-                        domain=chartDomain
-                        highlightedItems=highlightedItems}}
</del><ins>+                        currentItem=pane.hoveredOrSelectedItem
+                        selectedPoints=pane.selectedPoints
+                        selection=timeRange
+                        highlightedItems=highlightedItems
+                        rangeRoute=&quot;analysisTask&quot;}}
</ins><span class="cx">                 &lt;/div&gt;
</span><span class="cx">                 &lt;div class=&quot;details&quot;&gt;
</span><del>-                    &lt;table class=&quot;analysis-bugs&quot;&gt;
-                        &lt;tbody&gt;
-                            {{#each bugTrackers}}
-                                &lt;tr&gt;
-                                    &lt;th&gt;{{label}}&lt;/th&gt;
-                                    &lt;td&gt;
-                                        &lt;form {{action &quot;associateBug&quot; this editedBugNumber on=&quot;submit&quot;}}&gt;
-                                            {{input type=text value=editedBugNumber}}
-                                        &lt;/form&gt;
-                                    &lt;/td&gt;
-                                &lt;/tr&gt;
-                            {{/each}}
-                        &lt;/tbody&gt;
-                    &lt;/table&gt;
-                    &lt;table&gt;
-                        &lt;tbody&gt;
-                            {{#each analysisPoints}}
-                                &lt;tr&gt;&lt;td&gt;{{label}}&lt;/td&gt;&lt;td&gt;{{value}}&lt;/td&gt;&lt;/tr&gt;
-                            {{/each}}
-                        &lt;/tbody&gt;
-                    &lt;/table&gt;
</del><ins>+                    {{partial &quot;chart-details&quot;}}
</ins><span class="cx">                 &lt;/div&gt;
</span><span class="cx">             &lt;/section&gt;
</span><del>-            {{#each testGroups}}
-                &lt;section class=&quot;analysis-group&quot;&gt;
-                    &lt;table&gt;
-                        &lt;caption&gt;{{name}}&lt;/caption&gt;
-                        &lt;thead&gt;
-                            &lt;tr&gt;
-                                &lt;td&gt;Order&lt;/td&gt;
-                                &lt;td&gt;Configuration&lt;/td&gt;
-                                &lt;td&gt;Status&lt;/td&gt;
-                                &lt;td&gt;Build&lt;/td&gt;
-                                &lt;td&gt;{{../metric.fullName}}&lt;/td&gt;
</del><ins>+        {{/if}}
+
+        {{partial &quot;testGroupForm&quot;}}
+
+        {{#each testGroupPanes}}
+            {{partial &quot;testGroup&quot;}}
+        {{/each}}
+    &lt;/script&gt;
+
+    &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;testGroup&quot;&gt;
+        &lt;section class=&quot;analysis-group&quot;&gt;
+            &lt;h1&gt;{{name}}&lt;/h1&gt;
+            &lt;table class=&quot;results&quot;&gt;
+                &lt;thead&gt;
+                    &lt;tr&gt;
+                        &lt;td colspan=&quot;2&quot;&gt;Configuration&lt;/td&gt;
+                        {{#each repositories}}
+                            &lt;td&gt;{{name}}&lt;/td&gt;
+                        {{/each}}
+                        &lt;td&gt;Results&lt;/td&gt;
+                        &lt;td&gt;Status&lt;/td&gt;
+                    &lt;/tr&gt;
+                &lt;/thead&gt;
+                {{#each configurations}}
+                    &lt;tbody {{bind-attr class=&quot;showRequestList::hideRequests&quot;}}&gt;
+                        &lt;tr class=&quot;summary&quot; {{action toggleShowRequestList this}}&gt;
+                            &lt;td class=&quot;config-letter&quot; colspan=&quot;2&quot;&gt;{{summary.configLetter}}&lt;/td&gt;
+                            {{#with summary}}
+                                {{partial &quot;testGroupRow&quot;}}
+                            {{/with}}
+                        &lt;/tr&gt;
+                        {{#each items}}
+                            &lt;tr class=&quot;request&quot;&gt;
+                                {{#with ../this}}
+                                    &lt;td class=&quot;config-letter&quot; {{action toggleShowRequestList this}}&gt;&lt;/td&gt;
+                                {{/with}}
+                                &lt;td&gt;Run {{orderLabel}}&lt;/td&gt;
+                                {{partial &quot;testGroupRow&quot;}}
</ins><span class="cx">                             &lt;/tr&gt;
</span><del>-                        &lt;/thead&gt;
-                        &lt;tbody&gt;
-                            {{#each buildRequests}}
-                                &lt;tr&gt;
-                                    &lt;td&gt;{{orderLabel}}&lt;/td&gt;
-                                    &lt;td&gt;{{configLetter}}&lt;/td&gt;
-                                    &lt;td&gt;&lt;a {{bind-attr href=url}}&gt;{{statusLabel}}&lt;/a&gt;&lt;/td&gt;
-                                    &lt;td&gt;{{buildNumber}}&lt;/td&gt;
-                                    &lt;td&gt;{{mean}}&lt;/td&gt;
-                                &lt;/tr&gt;
-                            {{/each}}
-                        &lt;/tbody&gt;
-                    &lt;/table&gt;
-                &lt;/section&gt;
-            {{/each}}
</del><ins>+                        {{/each}}
+                    &lt;/tbody&gt;
+                {{/each}}
+            &lt;/table&gt;
+        &lt;/section&gt;
+    &lt;/script&gt;
</ins><span class="cx"> 
</span><del>-            {{#if roots}}
-            &lt;form method=&quot;POST&quot; {{action &quot;createTestGroup&quot; newTestGroupName repetitionCount on=&quot;submit&quot;}} class=&quot;analysis-group&quot;&gt;
</del><ins>+    &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;testGroupRow&quot;&gt;
+        {{#each revisions}}
+            &lt;td&gt;{{this}}&lt;/td&gt;
+        {{/each}}
+        &lt;td&gt;
+            {{#if value}}
+                {{box-plot range=valueRange value=value delta=confidenceIntervalDelta}}
+            {{/if}}
+            {{formattedValue}}
+        &lt;/td&gt;
+        &lt;td&gt;
+            {{#if buildNumber}}
+                 {{statusLabel}} / &lt;a {{bind-attr href=url}}&gt;Build {{buildNumber}}&lt;/a&gt;
+            {{else}}
+                &lt;a {{bind-attr href=url}}&gt;{{statusLabel}}&lt;/a&gt;
+            {{/if}}
+        &lt;/td&gt;
+    &lt;/script&gt;
+
+    &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;testGroupForm&quot;&gt;
+    {{#if roots}}
+        &lt;form method=&quot;POST&quot; {{action &quot;createTestGroup&quot; newTestGroupName repetitionCount on=&quot;submit&quot;}}&gt;
+            &lt;section class=&quot;analysis-group&quot;&gt;
+                &lt;h1&gt;{{input name=&quot;name&quot; value=newTestGroupName placeholder=&quot;Test group name&quot; required=true type=&quot;text&quot;}}&lt;/h1&gt;
</ins><span class="cx">                 &lt;table&gt;
</span><del>-                    &lt;caption&gt;{{input name=&quot;name&quot; value=newTestGroupName placeholder=&quot;Test group name&quot; required=true type=&quot;text&quot;}}&lt;/caption&gt;
</del><span class="cx">                     &lt;thead&gt;
</span><span class="cx">                         &lt;tr&gt;
</span><span class="cx">                             &lt;th&gt;Root&lt;/th&gt;
</span><span class="lines">@@ -633,9 +657,9 @@
</span><span class="cx">                 &lt;/table&gt;
</span><span class="cx"> 
</span><span class="cx">                 &lt;button type=&quot;submit&quot;&gt;Start A/B testing&lt;/button&gt;
</span><del>-            &lt;/form&gt;
-            {{/if}}
-        {{/if}}
</del><ins>+            &lt;/section&gt;
+        &lt;/form&gt;
+    {{/if}}
</ins><span class="cx">     &lt;/script&gt;
</span><span class="cx"> 
</span><span class="cx"> &lt;/head&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2interactivechartjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/interactive-chart.js        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -197,6 +197,8 @@
</span><span class="cx">     _updateDomain: function ()
</span><span class="cx">     {
</span><span class="cx">         var xDomain = this.get('domain');
</span><ins>+        if (!xDomain || !this._currentTimeSeriesData)
+            return null;
</ins><span class="cx">         var intrinsicXDomain = this._computeXAxisDomain(this._currentTimeSeriesData);
</span><span class="cx">         if (!xDomain)
</span><span class="cx">             xDomain = intrinsicXDomain;
</span><span class="lines">@@ -373,6 +375,8 @@
</span><span class="cx">     {
</span><span class="cx">         var selection = this._currentSelection() || this.get('sharedSelection');
</span><span class="cx">         var newXDomain = this._updateDomain();
</span><ins>+        if (!newXDomain)
+            return;
</ins><span class="cx"> 
</span><span class="cx">         if (selection &amp;&amp; newXDomain &amp;&amp; selection[0] &lt;= newXDomain[0] &amp;&amp; newXDomain[1] &lt;= selection[1])
</span><span class="cx">             selection = null; // Otherwise the user has no way of clearing the selection.
</span><span class="lines">@@ -753,7 +757,7 @@
</span><span class="cx">     },
</span><span class="cx">     _updateSelectionToolbar: function ()
</span><span class="cx">     {
</span><del>-        if (!this.get('interactive'))
</del><ins>+        if (!this.get('zoomable'))
</ins><span class="cx">             return;
</span><span class="cx"> 
</span><span class="cx">         var selection = this._currentSelection();
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2jsstatisticsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/js/statistics.js (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/js/statistics.js        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/js/statistics.js        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -1,5 +1,13 @@
</span><span class="cx"> var Statistics = new (function () {
</span><span class="cx"> 
</span><ins>+    this.min = function (values) {
+        return Math.min.apply(Math, values);
+    }
+
+    this.max = function (values) {
+        return Math.max.apply(Math, values);
+    }
+
</ins><span class="cx">     this.sum = function (values) {
</span><span class="cx">         return values.length ? values.reduce(function (a, b) { return a + b; }) : 0;
</span><span class="cx">     }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2manifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/manifest.js (180332 => 180333)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/manifest.js        2015-02-19 01:12:58 UTC (rev 180332)
+++ trunk/Websites/perf.webkit.org/public/v2/manifest.js        2015-02-19 02:41:45 UTC (rev 180333)
</span><span class="lines">@@ -302,6 +302,8 @@
</span><span class="cx">             var smallerIsBetter = unit != 'fps' &amp;&amp; unit != '/s'; // Assume smaller is better for unit-less metrics.
</span><span class="cx"> 
</span><span class="cx">             var useSI = unit == 'bytes';
</span><ins>+            var unitSuffix = unit ? ' ' + unit : '';
+            var deltaFormatterWithoutSign = useSI ? d3.format('.2s') : d3.format('.2g');
</ins><span class="cx">             return {
</span><span class="cx">                 platform: platform,
</span><span class="cx">                 metric: metric,
</span><span class="lines">@@ -310,6 +312,11 @@
</span><span class="cx">                     baseline: runs.baseline ? runs.baseline.timeSeriesByCommitTime() : null,
</span><span class="cx">                     target: runs.target ? runs.target.timeSeriesByCommitTime() : null,
</span><span class="cx">                     unit: unit,
</span><ins>+                    formatWithUnit: function (value) { return this.formatter(value) + unitSuffix; },
+                    formatWithDeltaAndUnit: function (value, delta)
+                    {
+                        return this.formatter(value) + (delta &amp;&amp; !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
+                    },
</ins><span class="cx">                     formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
</span><span class="cx">                     deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
</span><span class="cx">                     smallerIsBetter: smallerIsBetter,
</span></span></pre>
</div>
</div>

</body>
</html>