<!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>[175768] 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/175768">175768</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2014-11-07 15:47:09 -0800 (Fri, 07 Nov 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Introduce the concept of analysis task to perf dashboard
https://bugs.webkit.org/show_bug.cgi?id=138517

Reviewed by Andreas Kling.

Introduced the concept of an analysis task, which is created for a range of measurements for a given metric on
a single platform and used to bisect regressions in the range.
        
Added a new page to see the list of active analysis tasks and a page to view the contents of an analysis task.

* init-database.sql: Added a bunch of tables to store information about analysis tasks.
    analysis_tasks - Represents each analysis task. Associated with a platform and a metric and possibly with two
    test runs. Analysis tasks not associated with test runs are used for try new patches.
    analysis_test_groups - A test group in an analysis task represents a bunch of related A/B testing results.
    root_sets - A root set represents a set of roots (or packages) installed in each A/B testing.
    build_requests - A build request represents a single pending build for A/B testing.

* public/api/analysis-tasks.php: Added. Returns the specified analysis task or all analysis tasks in an array.
(main):
(format_task):

* public/api/test-groups.php: Added. Returns analysis task groups for the specified analysis task or returns
the specified analysis task group as well as build requests associated with them.
(main):
(fetch_test_groups_for_task):
(fetch_build_requests_for_task):
(fetch_build_requests_for_group):
(format_test_group):
(format_build_request):

* public/include/json-header.php:
(remote_user_name): Extracted from compute_token so that we can use it in create-analysis-task.php.
(compute_token):

* public/privileged-api/associate-bug.php:
(main): Fixed a typo.

* public/privileged-api/create-analysis-task.php: Added. Creates a new analysis task for a given test run range.
(main):
(ensure_row_by_id):
(ensure_config_from_runs):

* public/privileged-api/generate-csrf-token.php: Use remote_user_name.

* public/v2/analysis.js: Added. Various Ember data store models to represent analysis tasks and related objects.
(App.AnalysisTask):
(App.AnalysisTask.create):
(App.TestGroup):
(App.TestGroupAdapter):
(App.AnalysisTaskSerializer):
(App.TestGroupSerializer):
(App.BuildRequest):

* public/v2/app.css: Added style rules for the analysis page.

* public/v2/app.js:
(App.Pane._fetch): Use fetchRunsWithPlatformAndMetric, which has been refactored into App.Manifest.

(App.PaneController.actions.toggleBugsPane): Show bugs pane even when there are no bug trackers or there is not
exactly one selected point as we can still create an analysis task.
(App.PaneController.actions.associateBug): Renamed singlySelectedPoint to selectedSinglePoint to be more
grammatical and also alert'ed the error message when there is one.
(App.PaneController.actions.createAnalysisTask): Added. Creates a new analysis task and opens it in a new tab.
Since window.open only works during the click, we open the new &quot;window&quot; preemptively and navigates or closes it
once XHR request has completed.
(App.PaneController._detailsChanged): Changes due to singlySelectedPoint to selectedSinglePoint rename.
(App.PaneController._updateBugs): Fixed a bug that we were showing bugs in the previous point when a single point
is selected in the details pane.

(App.AnalysisRoute): Added.
(App.AnalysisTaskRoute): Added.
(App.AnalysisTaskViewModel): Added.
(App.AnalysisTaskViewModel._taskUpdated): Fetch runs for the associated platform and metric.
(App.AnalysisTaskViewModel._fetchedRuns): Setup the chart data to show.
(App.AnalysisTaskViewModel.testSets): The computed property used to update roots for all repositories/projects.
(App.AnalysisTaskViewModel._rootChangedForTestSet): Updates root selections for all repositories/projects when
the user selects an option for all roots in A or B configuration.
(App.AnalysisTaskViewModel.roots): The computed property used to show root choices for each repository/project.

* public/v2/chart-pane.css: Added style rules for details view in the analysis task page.

* public/v2/data.js:
(Measurement.prototype._formatRevisionRange): Don't prefix a revision number with &quot;At &quot; when there is no previous
point so that we can use it in App.AnalysisTaskViewModel.roots.
(TimeSeries.prototype.findPointByMeasurementId): Added.
(TimeSeries.prototype.seriesBetweenPoints): Added.

* public/v2/index.html: Use Metric.fullName since the same value is needed in the analysis task page. Also added
a button to create an analysis task, and made bugs pane button always enabled since we can an analysis task even
when multiple points are selected. Finally, added a new template for the analysis task page.

* public/v2/manifest.js:
(App.Metric.fullName): Added to share code between the charts page and the analysis task page.
(App.Manifest.fetchRunsWithPlatformAndMetric): Extracted from App.Pane._fetch to be reused in
App.AnalysisTaskViewModel._taskUpdated.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorginitdatabasesql">trunk/Websites/perf.webkit.org/init-database.sql</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludejsonheaderphp">trunk/Websites/perf.webkit.org/public/include/json-header.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapiassociatebugphp">trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapigeneratecsrftokenphp">trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php</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="#trunkWebsitesperfwebkitorgpublicv2manifestjs">trunk/Websites/perf.webkit.org/public/v2/manifest.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicapianalysistasksphp">trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapitestgroupsphp">trunk/Websites/perf.webkit.org/public/api/test-groups.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicprivilegedapicreateanalysistaskphp">trunk/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2analysisjs">trunk/Websites/perf.webkit.org/public/v2/analysis.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 (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -1,3 +1,101 @@
</span><ins>+2014-11-07  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Introduce the concept of analysis task to perf dashboard
+        https://bugs.webkit.org/show_bug.cgi?id=138517
+
+        Reviewed by Andreas Kling.
+
+        Introduced the concept of an analysis task, which is created for a range of measurements for a given metric on
+        a single platform and used to bisect regressions in the range.
+        
+        Added a new page to see the list of active analysis tasks and a page to view the contents of an analysis task.
+
+        * init-database.sql: Added a bunch of tables to store information about analysis tasks.
+            analysis_tasks - Represents each analysis task. Associated with a platform and a metric and possibly with two
+            test runs. Analysis tasks not associated with test runs are used for try new patches.
+            analysis_test_groups - A test group in an analysis task represents a bunch of related A/B testing results.
+            root_sets - A root set represents a set of roots (or packages) installed in each A/B testing.
+            build_requests - A build request represents a single pending build for A/B testing.
+
+        * public/api/analysis-tasks.php: Added. Returns the specified analysis task or all analysis tasks in an array.
+        (main):
+        (format_task):
+
+        * public/api/test-groups.php: Added. Returns analysis task groups for the specified analysis task or returns
+        the specified analysis task group as well as build requests associated with them.
+        (main):
+        (fetch_test_groups_for_task):
+        (fetch_build_requests_for_task):
+        (fetch_build_requests_for_group):
+        (format_test_group):
+        (format_build_request):
+
+        * public/include/json-header.php:
+        (remote_user_name): Extracted from compute_token so that we can use it in create-analysis-task.php.
+        (compute_token):
+
+        * public/privileged-api/associate-bug.php:
+        (main): Fixed a typo.
+
+        * public/privileged-api/create-analysis-task.php: Added. Creates a new analysis task for a given test run range.
+        (main):
+        (ensure_row_by_id):
+        (ensure_config_from_runs):
+
+        * public/privileged-api/generate-csrf-token.php: Use remote_user_name.
+
+        * public/v2/analysis.js: Added. Various Ember data store models to represent analysis tasks and related objects.
+        (App.AnalysisTask):
+        (App.AnalysisTask.create):
+        (App.TestGroup):
+        (App.TestGroupAdapter):
+        (App.AnalysisTaskSerializer):
+        (App.TestGroupSerializer):
+        (App.BuildRequest):
+
+        * public/v2/app.css: Added style rules for the analysis page.
+
+        * public/v2/app.js:
+        (App.Pane._fetch): Use fetchRunsWithPlatformAndMetric, which has been refactored into App.Manifest.
+
+        (App.PaneController.actions.toggleBugsPane): Show bugs pane even when there are no bug trackers or there is not
+        exactly one selected point as we can still create an analysis task.
+        (App.PaneController.actions.associateBug): Renamed singlySelectedPoint to selectedSinglePoint to be more
+        grammatical and also alert'ed the error message when there is one.
+        (App.PaneController.actions.createAnalysisTask): Added. Creates a new analysis task and opens it in a new tab.
+        Since window.open only works during the click, we open the new &quot;window&quot; preemptively and navigates or closes it
+        once XHR request has completed.
+        (App.PaneController._detailsChanged): Changes due to singlySelectedPoint to selectedSinglePoint rename.
+        (App.PaneController._updateBugs): Fixed a bug that we were showing bugs in the previous point when a single point
+        is selected in the details pane.
+
+        (App.AnalysisRoute): Added.
+        (App.AnalysisTaskRoute): Added.
+        (App.AnalysisTaskViewModel): Added.
+        (App.AnalysisTaskViewModel._taskUpdated): Fetch runs for the associated platform and metric.
+        (App.AnalysisTaskViewModel._fetchedRuns): Setup the chart data to show.
+        (App.AnalysisTaskViewModel.testSets): The computed property used to update roots for all repositories/projects.
+        (App.AnalysisTaskViewModel._rootChangedForTestSet): Updates root selections for all repositories/projects when
+        the user selects an option for all roots in A or B configuration.
+        (App.AnalysisTaskViewModel.roots): The computed property used to show root choices for each repository/project.
+
+        * public/v2/chart-pane.css: Added style rules for details view in the analysis task page.
+
+        * public/v2/data.js:
+        (Measurement.prototype._formatRevisionRange): Don't prefix a revision number with &quot;At &quot; when there is no previous
+        point so that we can use it in App.AnalysisTaskViewModel.roots.
+        (TimeSeries.prototype.findPointByMeasurementId): Added.
+        (TimeSeries.prototype.seriesBetweenPoints): Added.
+
+        * public/v2/index.html: Use Metric.fullName since the same value is needed in the analysis task page. Also added
+        a button to create an analysis task, and made bugs pane button always enabled since we can an analysis task even
+        when multiple points are selected. Finally, added a new template for the analysis task page.
+
+        * public/v2/manifest.js:
+        (App.Metric.fullName): Added to share code between the charts page and the analysis task page.
+        (App.Manifest.fetchRunsWithPlatformAndMetric): Extracted from App.Pane._fetch to be reused in
+        App.AnalysisTaskViewModel._taskUpdated.
+
</ins><span class="cx"> 2014-10-28  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Remove App.PaneController.bugsChangeCount in the new perf dashboard
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorginitdatabasesql"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/init-database.sql (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/init-database.sql        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/init-database.sql        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -138,3 +138,33 @@
</span><span class="cx">     CONSTRAINT bug_tracker_and_run_must_be_unique UNIQUE(bug_tracker, bug_run));
</span><span class="cx"> CREATE INDEX bugs_tracker_number_index ON bugs(bug_tracker, bug_number);
</span><span class="cx"> CREATE INDEX bugs_run_index ON bugs(bug_run);
</span><ins>+
+CREATE TABLE analysis_tasks (
+    task_id serial PRIMARY KEY,
+    task_name varchar(256) NOT NULL,
+    task_author varchar(256),
+    task_created_at timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
+    task_platform integer REFERENCES platforms NOT NULL,
+    task_metric integer REFERENCES test_metrics NOT NULL,
+    task_start_run integer REFERENCES test_runs,
+    task_end_run integer REFERENCES test_runs,
+    CONSTRAINT analysis_task_should_be_unique_for_range UNIQUE(task_start_run, task_end_run));
+
+CREATE TABLE analysis_test_groups (
+    testgroup_id serial PRIMARY KEY,
+    testgroup_task integer REFERENCES analysis_tasks NOT NULL,
+    testgroup_name varchar(256),
+    testgroup_author varchar(256) NOT NULL,
+    testgroup_created_at timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'));
+CREATE INDEX testgroup_task_index ON analysis_test_groups(testgroup_task);
+
+CREATE TABLE root_sets (
+    rootset_id serial PRIMARY KEY);
+
+CREATE TABLE build_requests (
+    request_id serial PRIMARY KEY,
+    request_group integer REFERENCES analysis_test_groups NOT NULL,
+    request_order integer NOT NULL,
+    request_root_set integer REFERENCES root_sets NOT NULL,
+    request_build integer REFERENCES builds,
+    CONSTRAINT build_request_order_must_be_unique_in_group UNIQUE(request_group, request_order));
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapianalysistasksphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php (0 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -0,0 +1,45 @@
</span><ins>+&lt;?php
+
+require('../include/json-header.php');
+
+function main($path) {
+    $db = new Database;
+    if (!$db-&gt;connect())
+        exit_with_error('DatabaseConnectionFailure');
+
+    if (count($path) &gt; 1)
+        exit_with_error('InvalidRequest');
+
+    if (count($path) &gt; 0 &amp;&amp; $path[0]) {
+        $task_id = intval($path[0]);
+        $task = $db-&gt;select_first_row('analysis_tasks', 'task', array('id' =&gt; $task_id));
+        if (!$task)
+            exit_with_error('TaskNotFound', array('id' =&gt; $task_id));
+        $tasks = array($task);
+    } else {
+        // FIXME: Limit the number of tasks we fetch.
+        $tasks = array_reverse($db-&gt;fetch_table('analysis_tasks', 'task_created_at'));
+        if (!is_array($tasks))
+            exit_with_error('FailedToFetchTasks');
+    }
+
+    exit_with_success(array('analysisTasks' =&gt; array_map(&quot;format_task&quot;, $tasks)));
+}
+
+date_default_timezone_set('UTC');
+function format_task($task_row) {
+    return array(
+        'id' =&gt; $task_row['task_id'],
+        'name' =&gt; $task_row['task_name'],
+        'author' =&gt; $task_row['task_author'],
+        'createdAt' =&gt; strtotime($task_row['task_created_at']) * 1000,
+        'platform' =&gt; $task_row['task_platform'],
+        'metric' =&gt; $task_row['task_metric'],
+        'startRun' =&gt; $task_row['task_start_run'],
+        'endRun' =&gt; $task_row['task_end_run'],
+    );
+}
+
+main(array_key_exists('PATH_INFO', $_SERVER) ? explode('/', trim($_SERVER['PATH_INFO'], '/')) : array());
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapitestgroupsphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/api/test-groups.php (0 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/test-groups.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/api/test-groups.php        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -0,0 +1,89 @@
</span><ins>+&lt;?php
+
+require('../include/json-header.php');
+
+function main($path) {
+    $db = new Database;
+    if (!$db-&gt;connect())
+        exit_with_error('DatabaseConnectionFailure');
+
+    if (count($path) &gt; 1)
+        exit_with_error('InvalidRequest');
+
+    if (count($path) &gt; 0 &amp;&amp; $path[0]) {
+        $group_id = intval($path[0]);
+        $group = $db-&gt;select_first_row('analysis_test_groups', 'testgroup', array('id' =&gt; $group_id));
+        if (!$group)
+            exit_with_error('GroupNotFound', array('id' =&gt; $group_id));
+        $test_groups = array($group);
+        $build_requests = fetch_build_requests_for_group($db, $group_id);
+    } else {
+        $task_id = array_get($_GET, 'task');
+        if (!$task_id)
+            exit_with_error('TaskIdNotSpecified');
+
+        $test_groups = fetch_test_groups_for_task($db, $task_id);
+        if (!is_array($test_groups))
+            exit_with_error('FailedToFetchTestGroups');
+        $build_requests = fetch_build_requests_for_task($db, $task_id);
+    }
+    if (!is_array($build_requests))
+        exit_with_error('FailedToFetchBuildRequests');
+
+    $test_groups = array_map(&quot;format_test_group&quot;, $test_groups);
+    $group_by_id = array();
+    foreach ($test_groups as &amp;$group)
+        $group_by_id[$group['id']] = &amp;$group;
+
+    $build_requests = array_map(&quot;format_build_request&quot;, $build_requests);
+    foreach ($build_requests as $request)
+        array_push($group_by_id[$request['testGroup']]['buildRequests'], $request['id']);
+
+    exit_with_success(array('testGroups' =&gt; $test_groups, 'buildRequests' =&gt; $build_requests));
+}
+
+function fetch_test_groups_for_task($db, $task_id) {
+    return $db-&gt;query_and_fetch_all('SELECT * FROM analysis_test_groups WHERE testgroup_task = $1
+        ORDER BY testgroup_created_at', array($task_id));
+}
+
+function fetch_build_requests_for_task($db, $task_id) {
+    return $db-&gt;query_and_fetch_all('SELECT * FROM build_requests, builds
+        WHERE request_build = build_id
+            AND request_group IN (SELECT testgroup_id FROM analysis_test_groups WHERE testgroup_task = $1)
+        ORDER BY request_group, request_order', array($task_id));
+}
+
+function fetch_build_requests_for_group($db, $test_group_id) {
+    return $db-&gt;query_and_fetch_all('SELECT * FROM build_requests, builds
+        WHERE request_build = build_id AND request_group = $1 ORDER BY request_order', array($test_group_id));
+}
+
+date_default_timezone_set('UTC');
+function format_test_group($group_row) {
+    return array(
+        'id' =&gt; $group_row['testgroup_id'],
+        'task' =&gt; $group_row['testgroup_task'],
+        'name' =&gt; $group_row['testgroup_name'],
+        'author' =&gt; $group_row['testgroup_author'],
+        'createdAt' =&gt; strtotime($group_row['testgroup_created_at']) * 1000,
+        'buildRequests' =&gt; array(),
+    );
+}
+
+function format_build_request($request_row) {
+    return array(
+        'id' =&gt; $request_row['request_id'],
+        'testGroup' =&gt; $request_row['request_group'],
+        'order' =&gt; $request_row['request_order'],
+        'rootSet' =&gt; $request_row['request_root_set'],
+        'build' =&gt; $request_row['request_build'],
+        'builder' =&gt; $request_row['build_builder'],
+        'buildNumber' =&gt; $request_row['build_number'],
+        'buildTime' =&gt; $request_row['build_time'] ? strtotime($request_row['build_time']) * 1000 : NULL,
+    );
+}
+
+main(array_key_exists('PATH_INFO', $_SERVER) ? explode('/', trim($_SERVER['PATH_INFO'], '/')) : array());
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludejsonheaderphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/json-header.php (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/json-header.php        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/include/json-header.php        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -93,10 +93,14 @@
</span><span class="cx">     return $data;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function remote_user_name() {
+    return array_get($_SERVER, 'REMOTE_USER');
+}
+
</ins><span class="cx"> function compute_token() {
</span><span class="cx">     if (!array_key_exists('CSRFSalt', $_COOKIE) || !array_key_exists('CSRFExpiration', $_COOKIE))
</span><span class="cx">         return NULL;
</span><del>-    $user = array_get($_SERVER, 'REMOTE_USER');
</del><ins>+    $user = remote_user_name();
</ins><span class="cx">     $salt = $_COOKIE['CSRFSalt'];
</span><span class="cx">     $expiration = $_COOKIE['CSRFExpiration'];
</span><span class="cx">     return hash('sha256', &quot;$salt|$user|$expiration&quot;);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapiassociatebugphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/associate-bug.php        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -31,7 +31,7 @@
</span><span class="cx">     }
</span><span class="cx">     $db-&gt;commit_transaction();
</span><span class="cx"> 
</span><del>-    exit_with_success(array('bug_id' =&gt; $bug_id));
</del><ins>+    exit_with_success(array('bugId' =&gt; $bug_id));
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> main();
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapicreateanalysistaskphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php (0 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/create-analysis-task.php        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -0,0 +1,62 @@
</span><ins>+&lt;?php
+
+require_once('../include/json-header.php');
+
+function main() {
+    $data = ensure_privileged_api_data_and_token();
+
+    $author = remote_user_name();
+    $name = array_get($data, 'name');
+    $start_run_id = array_get($data, 'startRun');
+    $end_run_id = array_get($data, 'endRun');
+
+    if (!$name)
+        exit_with_error('MissingName', array('name' =&gt; $name));
+    $range = array('startRunId' =&gt; $start_run_id, 'endRunId' =&gt; $end_run_id);
+    if (!$start_run_id || !$end_run_id)
+        exit_with_error('MissingRange', $range);
+
+    $db = connect();
+    $start_run = ensure_row_by_id($db, 'test_runs', 'run', $start_run_id, 'InvalidStartRun', $range);
+    $end_run = ensure_row_by_id($db, 'test_runs', 'run', $end_run_id, 'InvalidEndRun', $range);
+
+    $config = ensure_config_from_runs($db, $start_run, $end_run);
+
+    $db-&gt;begin_transaction();
+    $duplicate = $db-&gt;select_first_row('analysis_tasks', 'task', array('start_run' =&gt; $start_run_id, 'end_run' =&gt; $end_run_id));
+    if ($duplicate) {
+        $db-&gt;rollback_transaction();
+        exit_with_error('DuplicateAnalysisTask', array('duplicate' =&gt; $duplicate));
+    }
+
+    $task_id = $db-&gt;insert_row('analysis_tasks', 'task', array(
+        'name' =&gt; $name,
+        'author' =&gt; $author,
+        'platform' =&gt; $config['config_platform'],
+        'metric' =&gt; $config['config_metric'],
+        'start_run' =&gt; $start_run_id,
+        'end_run' =&gt; $end_run_id));
+    $db-&gt;commit_transaction();
+
+    exit_with_success(array('taskId' =&gt; $task_id));
+}
+
+function ensure_row_by_id($db, $table, $prefix, $id, $error_name, $error_params) {
+    $row = $db-&gt;select_first_row($table, $prefix, array('id' =&gt; $id));
+    if (!$row)
+        exit_with_error($error_name, array($error_params));
+    return $row;
+}
+
+function ensure_config_from_runs($db, $start_run, $end_run) {
+    $range = array('startRun' =&gt; $start_run, 'endRun' =&gt; $end_run);
+
+    if ($start_run['run_config'] != $end_run['run_config'])
+        exit_with_error('RunConfigMismatch', $range);
+
+    return ensure_row_by_id($db, 'test_configurations', 'config', $start_run['run_config'], 'ConfigNotFound', $range);
+}
+
+main();
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicprivilegedapigeneratecsrftokenphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/privileged-api/generate-csrf-token.php        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -4,8 +4,6 @@
</span><span class="cx"> 
</span><span class="cx"> ensure_privileged_api_data();
</span><span class="cx"> 
</span><del>-$user = array_get($_SERVER, 'REMOTE_USER');
-
</del><span class="cx"> $expiritaion = time() + 3600; // Valid for one hour.
</span><span class="cx"> $_COOKIE['CSRFSalt'] = rand();
</span><span class="cx"> $_COOKIE['CSRFExpiration'] = $expiritaion;
</span><span class="lines">@@ -13,6 +11,6 @@
</span><span class="cx"> setcookie('CSRFSalt', $_COOKIE['CSRFSalt']);
</span><span class="cx"> setcookie('CSRFExpiration', $expiritaion);
</span><span class="cx"> 
</span><del>-exit_with_success(array('user' =&gt; $user, 'token' =&gt; compute_token(), 'expiration' =&gt; $expiritaion * 1000));
</del><ins>+exit_with_success(array('user' =&gt; remote_user_name(), 'token' =&gt; compute_token(), 'expiration' =&gt; $expiritaion * 1000));
</ins><span class="cx"> 
</span><span class="cx"> ?&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2analysisjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v2/analysis.js (0 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/analysis.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v2/analysis.js        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -0,0 +1,60 @@
</span><ins>+App.AnalysisTask = App.NameLabelModel.extend({
+    author: DS.attr('string'),
+    createdAt: DS.attr('date'),
+    platform: DS.belongsTo('platform'),
+    metric: DS.belongsTo('metric'),
+    startRun: DS.attr('number'),
+    endRun: DS.attr('number'),
+    testGroups: function () {
+        return this.store.find('testGroup', {task: this.get('id')});
+    }.property(),
+});
+
+// FIXME: Use DS.RESTAdapter instead.
+App.AnalysisTask.create = function (name, startMeasurement, endMeasurement)
+{
+    return PrivilegedAPI.sendRequest('create-analysis-task', {
+        name: name,
+        startRun: startMeasurement.id(),
+        endRun: endMeasurement.id(),
+    });
+}
+
+App.AnalysisTaskAdapter = DS.RESTAdapter.extend({
+    buildURL: function (type, id)
+    {
+        return '../api/analysis-tasks/' + (id ? id : '');
+    },
+});
+
+App.TestGroup = App.NameLabelModel.extend({
+    analysisTask: DS.belongsTo('analysisTask'),
+    author: DS.attr('string'),
+    createdAt: DS.attr('date'),
+    buildRequests: DS.hasMany('buildRequests'),
+});
+
+App.TestGroupAdapter = DS.RESTAdapter.extend({
+    buildURL: function (type, id)
+    {
+        return '../api/test-groups/' + (id ? id : '');
+    },
+});
+
+App.AnalysisTaskSerializer = App.TestGroupSerializer = DS.RESTSerializer.extend({
+    normalizePayload: function (payload)
+    {
+        delete payload['status'];
+        return payload;
+    }
+});
+
+App.BuildRequest = DS.Model.extend({
+    group: DS.belongsTo('testGroup'),
+    order: DS.attr('number'),
+    rootSet: DS.attr('number'),
+    build: DS.attr('number'),
+    buildNumber: DS.attr('number'),
+    buildBuilder: DS.belongsTo('builder'),
+    buildTime: DS.attr('date'),
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appcss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.css (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.css        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/v2/app.css        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -411,3 +411,63 @@
</span><span class="cx">     line-height: 12rem;
</span><span class="cx">     vertical-align: center;
</span><span class="cx"> }
</span><ins>+
+#analysis-tasks,
+.test-groups &gt; table {
+    border: solid 0px #999;
+    border-collapse: collapse;
+}
+
+#analysis-tasks thead,
+.test-groups &gt; table thead {
+    color: #c93;
+}
+
+#analysis-tasks th,
+.test-groups &gt; table th {
+    font-weight: normal;
+}
+
+#analysis-tasks td,
+#analysis-tasks th,
+.test-groups &gt; table td,
+.test-groups &gt; table th {
+    padding: 0.2rem 0.5rem;
+}
+
+#analysis-tasks tbody td,
+#analysis-tasks tbody th,
+.test-groups &gt; table tbody td,
+.test-groups &gt; table tbody th {
+    border-top: solid 1px #ddd;
+}
+
+#analysis-task-title {
+    font-weight: normal;
+    font-size: 1.2rem;
+    margin: 0 0 0 0.5rem;
+    padding: 0;
+}
+
+#analysis-task-testname {
+    font-weight: normal;
+    font-size: 1rem;
+    margin: 0 0 1rem 0.5rem;
+    padding: 0;
+    color: #333;
+}
+
+.test-groups {
+    border: 1px solid #bbb;
+    border-radius: 0.5rem;
+    box-shadow: rgba(0, 0, 0, 0.03) 1px 1px 0px 0px;
+
+    padding: 0.5rem 1rem;
+    margin-bottom: 1.5rem;
+}
+
+.test-groups caption {
+    font-size: 1.1rem;
+    text-align: left;
+    margin-bottom: 0.5rem;
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.js        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -2,6 +2,8 @@
</span><span class="cx"> 
</span><span class="cx"> App.Router.map(function () {
</span><span class="cx">     this.resource('charts', {path: 'charts'});
</span><ins>+    this.resource('analysis', {path: 'analysis'});
+    this.resource('analysisTask', {path: 'analysis/task/:taskId'});
</ins><span class="cx"> });
</span><span class="cx"> 
</span><span class="cx"> App.DashboardRow = Ember.Object.extend({
</span><span class="lines">@@ -338,46 +340,20 @@
</span><span class="cx">         else {
</span><span class="cx">             var self = this;
</span><span class="cx"> 
</span><del>-            var metric;
-            var manifestPromise = App.Manifest.fetch(this.store).then(function () {
-                return new Ember.RSVP.Promise(function (resolve, reject) {
-                    var platform = App.Manifest.platform(platformId);
-                    metric = App.Manifest.metric(metricId);
-                    if (!platform)
-                        reject('Could not find the platform &quot;' + platformId + '&quot;');
-                    else if (!metric)
-                        reject('Could not find the metric &quot;' + metricId + '&quot;');
-                    else {
-                        self.set('platform', platform);
-                        self.set('metric', metric);
-                        resolve(null);
-                    }
-                });
</del><ins>+            App.Manifest.fetchRunsWithPlatformAndMetric(this.store, platformId, metricId).then(function (result) {
+                self.set('platform', result.platform);
+                self.set('metric', result.metric);
+                self.set('chartData', result.runs);
+            }, function (result) {
+                if (!result || typeof(result) === &quot;string&quot;)
+                    self.set('failure', 'Failed to fetch the JSON with an error: ' + result);
+                else if (!result.platform)
+                    self.set('failure', 'Could not find the platform &quot;' + platformId + '&quot;');
+                else if (!result.metric)
+                    self.set('failure', 'Could not find the metric &quot;' + metricId + '&quot;');
+                else
+                    self.set('failure', 'An internal error');
</ins><span class="cx">             });
</span><del>-
-            Ember.RSVP.all([
-                RunsData.fetchRuns(platformId, metricId),
-                manifestPromise,
-            ]).then(function (values) {
-                var runs = values[0];
-
-                // FIXME: Include this information in JSON and process it in RunsData.fetchRuns
-                var unit = {'Combined': '', // Assume smaller is better for now.
-                    'FrameRate': 'fps',
-                    'Runs': 'runs/s',
-                    'Time': 'ms',
-                    'Malloc': 'bytes',
-                    'JSHeap': 'bytes',
-                    'Allocations': 'bytes',
-                    'EndAllocations': 'bytes',
-                    'MaxAllocations': 'bytes',
-                    'MeanAllocations': 'bytes'}[metric.get('name')];
-                runs.unit = unit;
-
-                self.set('chartData', runs);
-            }, function (status) {
-                self.set('failure', 'Failed to fetch the JSON with an error: ' + status);
-            });
</del><span class="cx">         }
</span><span class="cx">     }.observes('platformId', 'metricId').on('init'),
</span><span class="cx">     _isValidId: function (id)
</span><span class="lines">@@ -675,22 +651,42 @@
</span><span class="cx">         },
</span><span class="cx">         toggleBugsPane: function ()
</span><span class="cx">         {
</span><del>-            if (!App.Manifest.bugTrackers || !this.get('singlySelectedPoint'))
-                return;
</del><span class="cx">             if (this.toggleProperty('showingBugsPane'))
</span><span class="cx">                 this.set('showingSearchPane', false);
</span><span class="cx">         },
</span><span class="cx">         associateBug: function (bugTracker, bugNumber)
</span><span class="cx">         {
</span><del>-            var point = this.get('singlySelectedPoint');
</del><ins>+            var point = this.get('selectedSinglePoint');
</ins><span class="cx">             if (!point)
</span><span class="cx">                 return;
</span><span class="cx">             var self = this;
</span><span class="cx">             point.measurement.associateBug(bugTracker.get('id'), bugNumber).then(function () {
</span><span class="cx">                 self._updateBugs();
</span><span class="cx">                 self._updateMarkedPoints();
</span><ins>+            }, function (error) {
+                alert(error);
</ins><span class="cx">             });
</span><span class="cx">         },
</span><ins>+        createAnalysisTask: function ()
+        {
+            var name = this.get('newAnalysisTaskName');
+            var points = this._selectedPoints;
+            if (!name || !points || points.length &lt; 2)
+                return;
+
+            var newWindow = window.open();
+            App.AnalysisTask.create(name, points[0].measurement, points[points.length - 1].measurement).then(function (data) {
+                // FIXME: Update the UI to show the new analysis task.
+                var url = App.Router.router.generate('analysisTask', data['taskId']);
+                newWindow.location.href = '#' + url;
+            }, function (error) {
+                newWindow.close();
+                if (error === 'DuplicateAnalysisTask') {
+                    // FIXME: Duplicate this error more gracefully.
+                }
+                alert(error);
+            });
+        },
</ins><span class="cx">         toggleSearchPane: function ()
</span><span class="cx">         {
</span><span class="cx">             if (!App.Manifest.repositoriesWithReportedCommits)
</span><span class="lines">@@ -732,7 +728,7 @@
</span><span class="cx">     _detailsChanged: function ()
</span><span class="cx">     {
</span><span class="cx">         this.set('showingBugsPane', false);
</span><del>-        this.set('singlySelectedPoint', !this._hasRange &amp;&amp; this._selectedPoints ? this._selectedPoints[0] : null);
</del><ins>+        this.set('selectedSinglePoint', !this._hasRange &amp;&amp; this._selectedPoints ? this._selectedPoints[0] : null);
</ins><span class="cx">     }.observes('details'),
</span><span class="cx">     _overviewSelectionChanged: function ()
</span><span class="cx">     {
</span><span class="lines">@@ -820,7 +816,9 @@
</span><span class="cx">         var bugTrackers = App.Manifest.get('bugTrackers');
</span><span class="cx">         var trackerToBugNumbers = {};
</span><span class="cx">         bugTrackers.forEach(function (tracker) { trackerToBugNumbers[tracker.get('id')] = new Array(); });
</span><del>-        this._selectedPoints.map(function (point) {
</del><ins>+
+        var points = this._hasRange ? this._selectedPoints : [this._selectedPoints[1]];
+        points.map(function (point) {
</ins><span class="cx">             var bugs = point.measurement.bugs();
</span><span class="cx">             bugTrackers.forEach(function (tracker) {
</span><span class="cx">                 var bugNumber = bugs[tracker.get('id')];
</span><span class="lines">@@ -1582,3 +1580,131 @@
</span><span class="cx">         })
</span><span class="cx">     }.observes('repository').observes('revisionInfo').on('init'),
</span><span class="cx"> });
</span><ins>+
+
+App.AnalysisRoute = Ember.Route.extend({
+    model: function () {
+        return this.store.findAll('analysisTask').then(function (tasks) {
+            return Ember.Object.create({'tasks': tasks});
+        });
+    },
+});
+
+App.AnalysisTaskRoute = Ember.Route.extend({
+    model: function (param) {
+        var store = this.store;
+        return this.store.find('analysisTask', param.taskId).then(function (task) {
+            return App.AnalysisTaskViewModel.create({content: task});
+        });
+    },
+});
+
+App.AnalysisTaskViewModel = Ember.ObjectProxy.extend({
+    testSets: [],
+    roots: [],
+    _taskUpdated: function ()
+    {
+        var platformId = this.get('platform').get('id');
+        var metricId = this.get('metric').get('id');
+        App.Manifest.fetchRunsWithPlatformAndMetric(this.store, platformId, metricId).then(this._fetchedRuns.bind(this));
+    }.observes('platform', 'metric').on('init'),
+    _fetchedRuns: function (data) {
+        var runs = data.runs;
+
+        var currentTimeSeries = runs.current.timeSeriesByCommitTime();
+        if (!currentTimeSeries)
+            return; // FIXME: Report an error.
+
+        var start = currentTimeSeries.findPointByMeasurementId(this.get('startRun'));
+        var end = currentTimeSeries.findPointByMeasurementId(this.get('endRun'));
+        if (!start || !end)
+            return; // FIXME: Report an error.
+
+        var markedPoints = {};
+        markedPoints[start.measurement.id()] = true;
+        markedPoints[end.measurement.id()] = true;
+
+        var formatedPoints = currentTimeSeries.seriesBetweenPoints(start, end).map(function (point, index) {
+            return {
+                id: point.measurement.id(),
+                measurement: point.measurement,
+                label: 'Point ' + (index + 1),
+                value: point.value + (runs.unit ? ' ' + runs.unit : ''),
+            };
+        });
+
+        var margin = (end.time - start.time) * 0.1;
+        this.set('chartData', runs);
+        this.set('chartDomain', [start.time - margin, +end.time + margin]);
+        this.set('markedPoints', markedPoints);
+        this.set('analysisPoints', formatedPoints);
+    },
+    testSets: function ()
+    {
+        var analysisPoints = this.get('analysisPoints');
+        if (!analysisPoints)
+            return;
+        var pointOptions = [{value: ' ', label: 'None'}]
+            .concat(analysisPoints.map(function (point) { return {value: point.id, label: point.label}; }));
+        return [
+            Ember.Object.create({name: &quot;A&quot;, options: pointOptions, selection: pointOptions[1]}),
+            Ember.Object.create({name: &quot;B&quot;, options: pointOptions, selection: pointOptions[pointOptions.length - 1]}),
+        ];
+    }.property('analysisPoints'),
+    _rootChangedForTestSet: function () {
+        var sets = this.get('testSets');
+        var roots = this.get('roots');
+        if (!sets || !roots)
+            return;
+
+        sets.forEach(function (testSet, setIndex) {
+            var currentSelection = testSet.get('selection');
+            if (currentSelection == testSet.get('previousSelection'))
+                return;
+            testSet.set('previousSelection', currentSelection);
+            var pointIndex = testSet.get('options').indexOf(currentSelection);
+
+            roots.forEach(function (root) {
+                var set = root.sets[setIndex];
+                set.set('selection', set.revisions[pointIndex]);
+            });
+        });
+
+    }.observes('testSets.@each.selection'),
+    _updateRoots: function ()
+    {
+        var analysisPoints = this.get('analysisPoints');
+        if (!analysisPoints)
+            return [];
+        var repositoryToRevisions = {};
+        analysisPoints.forEach(function (point, pointIndex) {
+            var revisions = point.measurement.formattedRevisions();
+            for (var repositoryName in revisions) {
+                if (!repositoryToRevisions[repositoryName])
+                    repositoryToRevisions[repositoryName] = new Array(analysisPoints.length);
+                var revision = revisions[repositoryName];
+                repositoryToRevisions[repositoryName][pointIndex] = {
+                    label: point.label + ': ' + revision.label,
+                    value: revision.currentRevision,
+                };
+            }
+        });
+
+        var roots = [];
+        for (var repositoryName in repositoryToRevisions) {
+            var revisions = [{value: ' ', label: 'None'}].concat(repositoryToRevisions[repositoryName]);
+            roots.push(Ember.Object.create({
+                name: repositoryName,
+                sets: [
+                    Ember.Object.create({name: 'A[' + repositoryName + ']',
+                        revisions: revisions,
+                        selection: revisions[1]}),
+                    Ember.Object.create({name: 'B[' + repositoryName + ']',
+                        revisions: revisions,
+                        selection: revisions[revisions.length - 1]}),
+                ],
+            }));
+        }
+        return rooots;
+    }.property('analysisPoints'),
+});
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2chartpanecss"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/chart-pane.css (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/v2/chart-pane.css        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -171,6 +171,14 @@
</span><span class="cx">     border-left: solid 1px #bbb;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+.analysis-chart-pane .details {
+    overflow: scroll;
+}
+
+.analysis-chart-pane .details table {
+    margin: 0.5rem;
+}
+
</ins><span class="cx"> .chart-pane .overview {
</span><span class="cx">     height: 5rem;
</span><span class="cx">     border-bottom: solid 0px #eee;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2datajs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/data.js (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/data.js        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/v2/data.js        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -198,18 +198,18 @@
</span><span class="cx">     } else if (currentRevision.indexOf(' ') &gt;= 0) // e.g. 10.9 13C64.
</span><span class="cx">         revisionDelimiter = ' - ';
</span><span class="cx">     else if (currentRevision.length == 40) { // e.g. git hash
</span><del>-        formattedCurrentHash = currentRevision.substring(0, 8);
</del><ins>+        var formattedCurrentHash = currentRevision.substring(0, 8);
</ins><span class="cx">         if (previousRevision)
</span><span class="cx">             label = previousRevision.substring(0, 8) + '..' + formattedCurrentHash;
</span><span class="cx">         else
</span><del>-            label = 'At ' + formattedCurrentHash;
</del><ins>+            label = formattedCurrentHash;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     if (!label) {
</span><span class="cx">         if (previousRevision)
</span><span class="cx">             label = revisionPrefix + previousRevision + revisionDelimiter + revisionPrefix + currentRevision;
</span><span class="cx">         else
</span><del>-            label = 'At ' + revisionPrefix + currentRevision;
</del><ins>+            label = revisionPrefix + currentRevision;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     return {
</span><span class="lines">@@ -375,6 +375,18 @@
</span><span class="cx">     this._max = max;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+TimeSeries.prototype.findPointByMeasurementId = function (measurementId)
+{
+    return this._series.find(function (point) { return point.measurement.id() == measurementId; });
+}
+
+TimeSeries.prototype.seriesBetweenPoints = function (startPoint, endPoint)
+{
+    if (!startPoint.seriesIndex || !endPoint.seriesIndex)
+        return null;
+    return this._series.slice(startPoint.seriesIndex, endPoint.seriesIndex + 1);
+}
+
</ins><span class="cx"> TimeSeries.prototype.minMaxForTimeRange = function (startTime, endTime)
</span><span class="cx"> {
</span><span class="cx">     var data = this._series;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/index.html (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/index.html        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/v2/index.html        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -12,6 +12,7 @@
</span><span class="cx">     &lt;script src=&quot;data.js&quot; defer&gt;&lt;/script&gt;
</span><span class="cx">     &lt;script src=&quot;app.js&quot; defer&gt;&lt;/script&gt;
</span><span class="cx">     &lt;script src=&quot;manifest.js&quot; defer&gt;&lt;/script&gt;
</span><ins>+    &lt;script src=&quot;analysis.js&quot; defer&gt;&lt;/script&gt;
</ins><span class="cx">     &lt;script src=&quot;popup.js&quot; defer&gt;&lt;/script&gt;
</span><span class="cx">     &lt;link rel=&quot;stylesheet&quot; href=&quot;app.css&quot;&gt;
</span><span class="cx">     &lt;link rel=&quot;stylesheet&quot; href=&quot;chart-pane.css&quot;&gt;
</span><span class="lines">@@ -128,17 +129,10 @@
</span><span class="cx">         {{#each panes itemController=&quot;pane&quot;}}
</span><span class="cx">             &lt;section class=&quot;chart-pane&quot; tabindex=&quot;0&quot;&gt;
</span><span class="cx">                 &lt;header&gt;
</span><del>-                    &lt;h1 {{action &quot;toggleDetails&quot;}}&gt;
-                    {{#each metric.path}}
-                        {{this}} &amp;ni;
-                    {{/each}}
-                    {{metric.label}}
-                    - {{ platform.name}}&lt;/h2&gt;
</del><ins>+                    &lt;h1 {{action &quot;toggleDetails&quot;}}&gt;{{metric.fullName}} - {{ platform.name}}&lt;/h1&gt;
</ins><span class="cx">                     &lt;a href=&quot;#&quot; title=&quot;Close&quot; class=&quot;close-button&quot; {{action &quot;close&quot;}}&gt;{{partial &quot;close-button&quot;}}&lt;/a&gt;
</span><span class="cx">                     {{#if App.Manifest.bugTrackers}}
</span><del>-                        &lt;a href=&quot;#&quot; title=&quot;Bugs&quot;
-                            {{bind-attr class=&quot;:bugs-button singlySelectedPoint::disabled&quot;}}
-                            {{action &quot;toggleBugsPane&quot;}}&gt;
</del><ins>+                        &lt;a href=&quot;#&quot; title=&quot;Bugs and Analysis&quot; class=&quot;bugs-button&quot; {{action &quot;toggleBugsPane&quot;}}&gt;
</ins><span class="cx">                             {{partial &quot;bugs-button&quot;}}
</span><span class="cx">                         &lt;/a&gt;
</span><span class="cx">                     {{/if}}
</span><span class="lines">@@ -202,16 +196,24 @@
</span><span class="cx"> 
</span><span class="cx">                 &lt;div {{bind-attr class=&quot;:bugs-pane showingBugsPane::hidden&quot;}}&gt;
</span><span class="cx">                     &lt;table&gt;
</span><del>-                        {{#each details.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}}
</del><ins>+                        {{#if selectedSinglePoint}}
+                            {{#each details.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}}
+                        {{/if}}
+                        &lt;tr&gt;
+                            &lt;th&gt;
+                                &lt;label&gt;Name: {{input type=text value=newAnalysisTaskName}}&lt;/label&gt;
+                                &lt;button {{action &quot;createAnalysisTask&quot;}}&gt;Analyze&lt;/button&gt;
+                            &lt;/th&gt;
+                        &lt;/tr&gt;
</ins><span class="cx">                     &lt;/table&gt;
</span><span class="cx">                 &lt;/div&gt;
</span><span class="cx"> 
</span><span class="lines">@@ -361,6 +363,9 @@
</span><span class="cx">                 {{#link-to 'charts' tagName='li'}}
</span><span class="cx">                     {{#link-to 'charts'}}Charts{{/link-to}}
</span><span class="cx">                 {{/link-to}}
</span><ins>+                {{#link-to 'analysis' tagName='li'}}
+                    {{#link-to 'analysis'}}Analysis{{/link-to}}
+                {{/link-to}}
</ins><span class="cx">             &lt;/ul&gt;
</span><span class="cx">         &lt;/nav&gt;
</span><span class="cx">     &lt;/script&gt;
</span><span class="lines">@@ -413,6 +418,138 @@
</span><span class="cx">         {{/each}}
</span><span class="cx">     &lt;/script&gt;
</span><span class="cx"> 
</span><ins>+    &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;analysis&quot;&gt;
+        &lt;header id=&quot;header&quot;&gt;
+            {{partial &quot;navbar&quot;}}
+        &lt;/header&gt;
+
+        &lt;table id=&quot;analysis-tasks&quot;&gt;
+            &lt;thead&gt;
+                &lt;tr&gt;
+                    &lt;td&gt;ID&lt;/td&gt;
+                    &lt;td&gt;Name&lt;/td&gt;
+                    &lt;td&gt;Created at&lt;/td&gt;
+                &lt;/tr&gt;
+            &lt;/thead&gt;
+            &lt;tbody&gt;
+                {{#each model.tasks}}
+                    &lt;tr&gt;
+                        &lt;td&gt;{{#link-to 'analysisTask' id}}{{id}}{{/link-to}}&lt;/td&gt;
+                        &lt;td&gt;{{name}}&lt;/td&gt;
+                        &lt;td&gt;{{createdAt}}&lt;/td&gt;
+                    &lt;/tr&gt;
+                {{/each}}
+            &lt;/tbody&gt;
+        &lt;/table&gt;
+    &lt;/script&gt;
+
+    &lt;script type=&quot;text/x-handlebars&quot; data-template-name=&quot;analysisTask&quot;&gt;
+        &lt;header id=&quot;header&quot;&gt;
+            {{partial &quot;navbar&quot;}}
+        &lt;/header&gt;
+
+        &lt;h2 id=&quot;analysis-task-title&quot;&gt;{{name}}&lt;/h2&gt;
+        {{#if platform.label}}
+            &lt;h3 id=&quot;analysis-task-testname&quot;&gt;{{metric.fullName}} - {{platform.label}}&lt;/h3&gt;
+
+            &lt;section class=&quot;analysis-chart-pane chart-pane&quot;&gt;
+                &lt;div class=&quot;svg-container&quot;&gt;
+                    {{interactive-chart
+                        chartData=chartData
+                        enableSelection=false
+                        chartPointRadius=2
+                        domain=chartDomain
+                        markedPoints=markedPoints}}
+                &lt;/div&gt;
+                &lt;div class=&quot;details&quot;&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;
+                &lt;/div&gt;
+            &lt;/section&gt;
+
+            {{#each testGroups}}
+                &lt;section class=&quot;test-groups&quot;&gt;
+                    &lt;table&gt;
+                        &lt;caption&gt;{{name}}&lt;/caption&gt;
+                        &lt;thead&gt;
+                            &lt;tr&gt;
+                                &lt;td&gt;Configuration&lt;/td&gt;
+                                &lt;td&gt;Build&lt;/td&gt;
+                                &lt;td&gt;Build Time&lt;/td&gt;
+                                &lt;td&gt;{{../metric.fullName}}&lt;/td&gt;
+                            &lt;/tr&gt;
+                        &lt;/thead&gt;
+                        &lt;tbody&gt;
+                            {{#each buildRequests}}
+                                &lt;tr&gt;
+                                    &lt;td&gt;{{id}}&lt;/td&gt;
+                                    &lt;td&gt;{{buildNumber}}&lt;/td&gt;
+                                    &lt;td&gt;{{buildTime}}&lt;/td&gt;
+                                    &lt;td&gt;{{mean}}&lt;/td&gt;
+                                &lt;/tr&gt;
+                            {{/each}}
+                        &lt;/tbody&gt;
+                    &lt;/table&gt;
+                &lt;/section&gt;
+            {{/each}}
+
+            &lt;form class=&quot;test-groups&quot;&gt;
+                &lt;table&gt;
+                    &lt;caption&gt;&lt;input name=&quot;name&quot; placeholder=&quot;Test group name&quot; required&gt;&lt;/caption&gt;
+                    &lt;thead&gt;
+                        &lt;tr&gt;
+                            &lt;th&gt;Root&lt;/th&gt;
+                            {{#each testSets}}
+                                &lt;th&gt;
+                                    {{name}}
+                                    {{view Ember.Select
+                                        content=options
+                                        optionValuePath=&quot;content.value&quot;
+                                        optionLabelPath=&quot;content.label&quot;
+                                        selection=selection}}
+                                &lt;/th&gt;
+                            {{/each}}
+                        &lt;/tr&gt;
+                    &lt;/thead&gt;
+                    &lt;tbody&gt;
+                        {{#each roots}}
+                            &lt;tr&gt;
+                                &lt;th&gt;{{name}}&lt;/th&gt;
+                                {{#each sets}}
+                                    &lt;td&gt;{{view Ember.Select name=name content=revisions
+                                        optionValuePath=&quot;content.value&quot; optionLabelPath=&quot;content.label&quot;
+                                        selection=selection}}&lt;/td&gt;
+                                {{/each}}
+                            &lt;/tr&gt;
+                        {{/each}}
+                    &lt;/tbody&gt;
+                    &lt;tbody&gt;
+                        &lt;tr&gt;
+                            &lt;th&gt;Number of runs&lt;/th&gt;
+                            &lt;td colspan=2&gt;
+                                &lt;select&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;/select&gt;
+                            &lt;/td&gt;
+                        &lt;/tr&gt;
+                    &lt;/tbody&gt;
+                &lt;/table&gt;
+
+                &lt;button type=&quot;submit&quot;&gt;Start A/B testing&lt;/button&gt;
+            &lt;/form&gt;
+        {{/if}}
+    &lt;/script&gt;
+
</ins><span class="cx"> &lt;/head&gt;
</span><span class="cx"> &lt;body&gt;
</span><span class="cx"> &lt;/body&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2manifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/manifest.js (175767 => 175768)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/manifest.js        2014-11-07 23:37:38 UTC (rev 175767)
+++ trunk/Websites/perf.webkit.org/public/v2/manifest.js        2014-11-07 23:47:09 UTC (rev 175768)
</span><span class="lines">@@ -30,6 +30,11 @@
</span><span class="cx">         }
</span><span class="cx">         return path.reverse();
</span><span class="cx">     }.property('name', 'test'),
</span><ins>+    fullName: function ()
+    {
+        return this.get('path').join(' \u2208 ') /* &amp;in; */
+            + ' : ' + this.get('label');
+    }.property('path', 'label'),
</ins><span class="cx"> });
</span><span class="cx"> 
</span><span class="cx"> App.Builder = App.NameLabelModel.extend({
</span><span class="lines">@@ -205,5 +210,32 @@
</span><span class="cx">             repositories.filter(function (repository) { return repository.get('hasReportedCommits'); }));
</span><span class="cx"> 
</span><span class="cx">         this.set('bugTrackers', store.all('bugTracker').sortBy('name'));
</span><del>-    }
</del><ins>+    },
+    fetchRunsWithPlatformAndMetric: function (store, platformId, metricId)
+    {
+        return Ember.RSVP.all([
+            RunsData.fetchRuns(platformId, metricId),
+            this.fetch(store),
+        ]).then(function (values) {
+            var runs = values[0];
+
+            var platform = App.Manifest.platform(platformId);
+            var metric = App.Manifest.metric(metricId);
+
+            // FIXME: Include this information in JSON and process it in RunsData.fetchRuns
+            var unit = {'Combined': '', // Assume smaller is better for now.
+                'FrameRate': 'fps',
+                'Runs': 'runs/s',
+                'Time': 'ms',
+                'Malloc': 'bytes',
+                'JSHeap': 'bytes',
+                'Allocations': 'bytes',
+                'EndAllocations': 'bytes',
+                'MaxAllocations': 'bytes',
+                'MeanAllocations': 'bytes'}[metric.get('name')];
+            runs.unit = unit;
+
+            return {platform: platform, metric: metric, runs: runs};
+        });
+    },
</ins><span class="cx"> }).create();
</span></span></pre>
</div>
</div>

</body>
</html>