<!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>[203709] 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/203709">203709</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2016-07-25 19:45:13 -0700 (Mon, 25 Jul 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Perf dashboard should show the list of a pending A/B testing jobs
https://bugs.webkit.org/show_bug.cgi?id=160138

Rubber-stamped by Chris Dumez.

Add a page to show the list of A/B testing build requests per triggerable. Ideally, we would like to
see a queue per builder but that would require changes to database tables and syncing scripts.

Because this page is most useful when the analysis task with which each build request is associated,
JSON API at /api/build-requests/ has been modified to return the analysis task ID for each request.

Also streamlined the page that shows the list of analysis tasks per Chris' feedback by consolidating
&quot;Bisecting&quot; and &quot;Identified&quot; into &quot;Investigated&quot; and moving the toolbar from the upper left corner
inside the heading to right beneath the heading above the table. Also made the category page serialize
the filter an user had typed in so that reloading the page doesn't clear it.

* public/api/analysis-tasks.php:
(fetch_associated_data_for_tasks): Removed 'category' from the list of columns returned as the notion
of 'category' is only relevant in UI, and it's better computed in the front-end.
(format_task): Ditto.
(determine_category): Deleted.

* public/api/test-groups.php:
(main):

* public/include/build-requests-fetcher.php:
(BuildRequestsFetcher::fetch_for_task): Include the analysis task ID in the rows.
(BuildRequestsFetcher::fetch_for_group): Ditto. Ditto.
(BuildRequestsFetcher::fetch_incomplete_requests_for_triggerable): Ditto.
(BuildRequestsFetcher::results_internal): Ditto.

* public/v3/index.html:

* public/v3/main.js:
(main): Create a newly introduced BuildRequestQueuePage as a subpage of AnalysisCategoryPage.

* public/v3/components/ratio-bar-graph.js:
(RatioBarGraph.prototype.update): Fixed a bogus assertion here. ratio can be any number. The coercion
into [-1, 1] is done inside RatioBarGraph's render() function.

* public/v3/models/analysis-task.js:
(AnalysisTask.prototype.category): Moved the code to compute the analysis task's category from
determine_category in analysis-tasks.php. Also merged &quot;bisecting&quot; and &quot;identified&quot; into &quot;investigated&quot;.
(AnalysisTask.categories): Merged &quot;bisecting&quot; and &quot;identified&quot; into &quot;investigated&quot;.

* public/v3/models/build-request.js:
(BuildRequest): Remember the triggerable and the analysis task associated with this request as well as
the time at when this request was created.        
(BuildRequest.prototype.analysisTaskId): Added.
(BuildRequest.prototype.statusLabel): Use a shorter label: &quot;Waiting&quot; for &quot;pending&quot; status.
(BuildRequest.prototype.createdAt): Added.
(BuildRequest.prototype.waitingTime): Added. Returns a human readable time duration since the creation
of this build request such as &quot;2 hours 21 minutes&quot; against a reference time.
(BuildRequest.fetchTriggerables): Added.
(BuildRequest.cachedRequestsForTriggerableID): Added. Used when navigating back to 

* public/v3/pages/analysis-category-page.js:
(AnalysisCategoryPage): Construct AnalysisCategoryToolbar and store it in this._categoryToolbar since it
no longer inherits from Toolbar class, which PageWithHeading recognizes and stores.
(AnalysisCategoryPage.prototype.title):
(AnalysisCategoryPage.prototype.serializeState): Added.
(AnalysisCategoryPage.prototype.stateForCategory): Added. Include the filter in the serialization.
(AnalysisCategoryPage.prototype.updateFromSerializedState): Restore the filter from the URL state.
(AnalysisCategoryPage.prototype.filterDidChange): Added. Called by AnalysisCategoryToolbar to update
the URL state in addition to calling render() as done previously via setFilterCallback.
(AnalysisCategoryPage.prototype.render): Always call _categoryToolbar.render() since the hyperlinks for
the category pages now include the filter, which can be updated in each call.
(AnalysisCategoryPage.cssTemplate):

* public/v3/pages/analysis-category-toolbar.js:
(AnalysisCategoryToolbar): Inherits from ComponentBase instead of Toolbar since AnalysisCategoryToolbar
no longer works with Heading class unlike other subclasses of Toolbar class.
(AnalysisCategoryToolbar.prototype.setCategoryPage): Added.
(AnalysisCategoryToolbar.prototype.setFilterCallback): Deleted.
(AnalysisCategoryToolbar.prototype.setFilter): Added. Used to restore from a serialized URL state.
(AnalysisCategoryToolbar.prototype.render): Don't recreate the input element as it clears the value as
well as the selection of the element. Also use AnalysisCategoryPage's stateForCategory to serialize the
category name and the current filter for each hyperlink.
(AnalysisCategoryToolbar.prototype._filterMayHaveChanged): Now takes an boolean argument specifying
whether the URL state should be updated or not. We update the URL only when a change event is fired to
avoid constantly updating it while an user is still typing.
(AnalysisCategoryToolbar.cssTemplate): Added.
(AnalysisCategoryToolbar.htmlTemplate): Added a button to open the newly added queue page.

* public/v3/pages/build-request-queue-page.js:
(BuildRequestQueuePage): Added.
(BuildRequestQueuePage.prototype.routeName): Added.
(BuildRequestQueuePage.prototype.pageTitle): Added.
(BuildRequestQueuePage.prototype.open): Added. Fetch open build requests for every triggerables using
the same API as the syncing scripts.
(BuildRequestQueuePage.prototype.render): Added.
(BuildRequestQueuePage.prototype._constructBuildRequestTable): Added. Construct a table for the list of
pending, scheduled or running build requests in the order syncing scripts would see. Note that the list
of build requests returned by /api/build-requests/* can contain completed, canceled, or failed requests
since the JSON returns all build requests associated with each test group if one of the requests of the
group have not finished. This helps syncing scripts picking the right builder for A/B testing when it
had previously been unloaded or crashed in the middle of processing a test group. This characteristics
of the API actually helps us here because we can reliably compute the total number of build requests in
the group. The first half of this function does this counting as well as collapses all but the first
unfinished build requests into a &quot;contraction&quot; row, which just shows the number of build requests that
are remaining in the group.
(BuildRequestQueuePage.cssTemplate): Added.
(BuildRequestQueuePage.htmlTemplate): Added.

* public/v3/pages/summary-page.js:
(SummaryPage.prototype.open): Use one-day median instead of seven-day median to compute the status.
(SummaryPageConfigurationGroup): Initialize _ratio to NaN. This was causing assertion failures in
RatioBarGraph's update() while measurement sets are being fetched.

* server-tests/api-build-requests-tests.js: Updated the tests per change in BuildRequest's statusLabel.
* unit-tests/analysis-task-tests.js: Ditto.
* unit-tests/test-groups-tests.js: Ditto.
* unit-tests/build-request-tests.js: Added tests for BuildRequest's waitingTime.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<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="#trunkWebsitesperfwebkitorgpublicincludebuildrequestsfetcherphp">trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentsratiobargraphjs">trunk/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3mainjs">trunk/Websites/perf.webkit.org/public/v3/main.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsanalysistaskjs">trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs">trunk/Websites/perf.webkit.org/public/v3/models/build-request.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysiscategorypagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysiscategorytoolbarjs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-toolbar.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagessummarypagejs">trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsapibuildrequeststestsjs">trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsanalysistasktestsjs">trunk/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunitteststestgroupstestsjs">trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesbuildrequestqueuepagejs">trunk/Websites/perf.webkit.org/public/v3/pages/build-request-queue-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgunittestsbuildrequesttestsjs">trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.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 (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -1,3 +1,119 @@
</span><ins>+2016-07-23  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Perf dashboard should show the list of a pending A/B testing jobs
+        https://bugs.webkit.org/show_bug.cgi?id=160138
+
+        Rubber-stamped by Chris Dumez.
+
+        Add a page to show the list of A/B testing build requests per triggerable. Ideally, we would like to
+        see a queue per builder but that would require changes to database tables and syncing scripts.
+
+        Because this page is most useful when the analysis task with which each build request is associated,
+        JSON API at /api/build-requests/ has been modified to return the analysis task ID for each request.
+
+        Also streamlined the page that shows the list of analysis tasks per Chris' feedback by consolidating
+        &quot;Bisecting&quot; and &quot;Identified&quot; into &quot;Investigated&quot; and moving the toolbar from the upper left corner
+        inside the heading to right beneath the heading above the table. Also made the category page serialize
+        the filter an user had typed in so that reloading the page doesn't clear it.
+
+        * public/api/analysis-tasks.php:
+        (fetch_associated_data_for_tasks): Removed 'category' from the list of columns returned as the notion
+        of 'category' is only relevant in UI, and it's better computed in the front-end.
+        (format_task): Ditto.
+        (determine_category): Deleted.
+
+        * public/api/test-groups.php:
+        (main):
+
+        * public/include/build-requests-fetcher.php:
+        (BuildRequestsFetcher::fetch_for_task): Include the analysis task ID in the rows.
+        (BuildRequestsFetcher::fetch_for_group): Ditto. Ditto.
+        (BuildRequestsFetcher::fetch_incomplete_requests_for_triggerable): Ditto.
+        (BuildRequestsFetcher::results_internal): Ditto.
+
+        * public/v3/index.html:
+
+        * public/v3/main.js:
+        (main): Create a newly introduced BuildRequestQueuePage as a subpage of AnalysisCategoryPage.
+
+        * public/v3/components/ratio-bar-graph.js:
+        (RatioBarGraph.prototype.update): Fixed a bogus assertion here. ratio can be any number. The coercion
+        into [-1, 1] is done inside RatioBarGraph's render() function.
+
+        * public/v3/models/analysis-task.js:
+        (AnalysisTask.prototype.category): Moved the code to compute the analysis task's category from
+        determine_category in analysis-tasks.php. Also merged &quot;bisecting&quot; and &quot;identified&quot; into &quot;investigated&quot;.
+        (AnalysisTask.categories): Merged &quot;bisecting&quot; and &quot;identified&quot; into &quot;investigated&quot;.
+
+        * public/v3/models/build-request.js:
+        (BuildRequest): Remember the triggerable and the analysis task associated with this request as well as
+        the time at when this request was created.        
+        (BuildRequest.prototype.analysisTaskId): Added.
+        (BuildRequest.prototype.statusLabel): Use a shorter label: &quot;Waiting&quot; for &quot;pending&quot; status.
+        (BuildRequest.prototype.createdAt): Added.
+        (BuildRequest.prototype.waitingTime): Added. Returns a human readable time duration since the creation
+        of this build request such as &quot;2 hours 21 minutes&quot; against a reference time.
+        (BuildRequest.fetchTriggerables): Added.
+        (BuildRequest.cachedRequestsForTriggerableID): Added. Used when navigating back to 
+
+        * public/v3/pages/analysis-category-page.js:
+        (AnalysisCategoryPage): Construct AnalysisCategoryToolbar and store it in this._categoryToolbar since it
+        no longer inherits from Toolbar class, which PageWithHeading recognizes and stores.
+        (AnalysisCategoryPage.prototype.title):
+        (AnalysisCategoryPage.prototype.serializeState): Added.
+        (AnalysisCategoryPage.prototype.stateForCategory): Added. Include the filter in the serialization.
+        (AnalysisCategoryPage.prototype.updateFromSerializedState): Restore the filter from the URL state.
+        (AnalysisCategoryPage.prototype.filterDidChange): Added. Called by AnalysisCategoryToolbar to update
+        the URL state in addition to calling render() as done previously via setFilterCallback.
+        (AnalysisCategoryPage.prototype.render): Always call _categoryToolbar.render() since the hyperlinks for
+        the category pages now include the filter, which can be updated in each call.
+        (AnalysisCategoryPage.cssTemplate):
+
+        * public/v3/pages/analysis-category-toolbar.js:
+        (AnalysisCategoryToolbar): Inherits from ComponentBase instead of Toolbar since AnalysisCategoryToolbar
+        no longer works with Heading class unlike other subclasses of Toolbar class.
+        (AnalysisCategoryToolbar.prototype.setCategoryPage): Added.
+        (AnalysisCategoryToolbar.prototype.setFilterCallback): Deleted.
+        (AnalysisCategoryToolbar.prototype.setFilter): Added. Used to restore from a serialized URL state.
+        (AnalysisCategoryToolbar.prototype.render): Don't recreate the input element as it clears the value as
+        well as the selection of the element. Also use AnalysisCategoryPage's stateForCategory to serialize the
+        category name and the current filter for each hyperlink.
+        (AnalysisCategoryToolbar.prototype._filterMayHaveChanged): Now takes an boolean argument specifying
+        whether the URL state should be updated or not. We update the URL only when a change event is fired to
+        avoid constantly updating it while an user is still typing.
+        (AnalysisCategoryToolbar.cssTemplate): Added.
+        (AnalysisCategoryToolbar.htmlTemplate): Added a button to open the newly added queue page.
+
+        * public/v3/pages/build-request-queue-page.js:
+        (BuildRequestQueuePage): Added.
+        (BuildRequestQueuePage.prototype.routeName): Added.
+        (BuildRequestQueuePage.prototype.pageTitle): Added.
+        (BuildRequestQueuePage.prototype.open): Added. Fetch open build requests for every triggerables using
+        the same API as the syncing scripts.
+        (BuildRequestQueuePage.prototype.render): Added.
+        (BuildRequestQueuePage.prototype._constructBuildRequestTable): Added. Construct a table for the list of
+        pending, scheduled or running build requests in the order syncing scripts would see. Note that the list
+        of build requests returned by /api/build-requests/* can contain completed, canceled, or failed requests
+        since the JSON returns all build requests associated with each test group if one of the requests of the
+        group have not finished. This helps syncing scripts picking the right builder for A/B testing when it
+        had previously been unloaded or crashed in the middle of processing a test group. This characteristics
+        of the API actually helps us here because we can reliably compute the total number of build requests in
+        the group. The first half of this function does this counting as well as collapses all but the first
+        unfinished build requests into a &quot;contraction&quot; row, which just shows the number of build requests that
+        are remaining in the group.
+        (BuildRequestQueuePage.cssTemplate): Added.
+        (BuildRequestQueuePage.htmlTemplate): Added.
+
+        * public/v3/pages/summary-page.js:
+        (SummaryPage.prototype.open): Use one-day median instead of seven-day median to compute the status.
+        (SummaryPageConfigurationGroup): Initialize _ratio to NaN. This was causing assertion failures in
+        RatioBarGraph's update() while measurement sets are being fetched.
+
+        * server-tests/api-build-requests-tests.js: Updated the tests per change in BuildRequest's statusLabel.
+        * unit-tests/analysis-task-tests.js: Ditto.
+        * unit-tests/test-groups-tests.js: Ditto.
+        * unit-tests/build-request-tests.js: Added tests for BuildRequest's waitingTime.
+
</ins><span class="cx"> 2016-07-22  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         REGRESSION(r203035): Marking points as an outlier no longer updates charts
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapianalysistasksphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -83,7 +83,6 @@
</span><span class="cx">         $task = &amp;$task_by_id[$build_count['task']];
</span><span class="cx">         $task['buildRequestCount'] = $build_count['total'];
</span><span class="cx">         $task['finishedBuildRequestCount'] = $build_count['finished'];
</span><del>-        $task['category'] = determine_category($task);
</del><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     return array('analysisTasks' =&gt; $tasks, 'bugs' =&gt; $bugs, 'commits' =&gt; $commits);
</span><span class="lines">@@ -103,7 +102,6 @@
</span><span class="cx">         'startRunTime' =&gt; Database::to_js_time($task_row['task_start_run_time']),
</span><span class="cx">         'endRun' =&gt; $task_row['task_end_run'],
</span><span class="cx">         'endRunTime' =&gt; Database::to_js_time($task_row['task_end_run_time']),
</span><del>-        'category' =&gt; null,
</del><span class="cx">         'result' =&gt; $task_row['task_result'],
</span><span class="cx">         'needed' =&gt; $task_row['task_needed'] ? Database::is_true($task_row['task_needed']) : null,
</span><span class="cx">         'bugs' =&gt; array(),
</span><span class="lines">@@ -112,20 +110,6 @@
</span><span class="cx">     );
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-function determine_category($task) {
-    $category = 'unconfirmed';
-
-    $result = $task['result'];
-    if ($result == 'unchanged' || $result == 'inconclusive' || $task['fixes'] || ($result == 'progression' &amp;&amp; $task['causes']))
-        $category = 'closed';
-    else if ($task['causes'])
-        $category = 'identified';
-    else if ($result)
-        $category = 'bisecting';
-
-    return $category;
-}
-
</del><span class="cx"> main(array_key_exists('PATH_INFO', $_SERVER) ? explode('/', trim($_SERVER['PATH_INFO'], '/')) : array());
</span><span class="cx"> 
</span><span class="cx"> ?&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapitestgroupsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/test-groups.php (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/test-groups.php        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/api/test-groups.php        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -19,7 +19,7 @@
</span><span class="cx">         if (!$group)
</span><span class="cx">             exit_with_error('GroupNotFound', array('id' =&gt; $group_id));
</span><span class="cx">         $test_groups = array($group);
</span><del>-        $build_requests_fetcher-&gt;fetch_for_group($group_id);
</del><ins>+        $build_requests_fetcher-&gt;fetch_for_group($group['testgroup_task'], $group_id);
</ins><span class="cx">     } else {
</span><span class="cx">         $task_id = array_get($_GET, 'task');
</span><span class="cx">         if (!$task_id)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludebuildrequestsfetcherphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -13,21 +13,23 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     function fetch_for_task($task_id) {
</span><del>-        $this-&gt;rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT *
</del><ins>+        $this-&gt;rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT *, testgroup_task as task_id
</ins><span class="cx">             FROM build_requests LEFT OUTER JOIN builds ON request_build = build_id, analysis_test_groups
</span><span class="cx">             WHERE request_group = testgroup_id AND testgroup_task = $1
</span><span class="cx">             ORDER BY request_group, request_order', array($task_id));
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    function fetch_for_group($test_group_id) {
</del><ins>+    function fetch_for_group($task_id, $test_group_id) {
</ins><span class="cx">         $this-&gt;rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT *
</span><span class="cx">             FROM build_requests LEFT OUTER JOIN builds ON request_build = build_id
</span><span class="cx">             WHERE request_group = $1 ORDER BY request_order', array($test_group_id));
</span><ins>+        foreach ($this-&gt;rows as &amp;$row)
+            $row['task_id'] = $task_id;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     function fetch_incomplete_requests_for_triggerable($triggerable_id) {
</span><del>-        $this-&gt;rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT * FROM build_requests,
-            (SELECT testgroup_id, (case when testgroup_author is not null then 0 else 1 end) as author_order, testgroup_created_at
</del><ins>+        $this-&gt;rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT *, test_groups.testgroup_task as task_id FROM build_requests,
+            (SELECT testgroup_id, testgroup_task, (case when testgroup_author is not null then 0 else 1 end) as author_order, testgroup_created_at
</ins><span class="cx">                 FROM analysis_test_groups WHERE EXISTS
</span><span class="cx">                     (SELECT 1 FROM build_requests WHERE testgroup_id = request_group AND request_status
</span><span class="cx">                         IN (\'pending\', \'scheduled\', \'running\'))) AS test_groups
</span><span class="lines">@@ -64,6 +66,7 @@
</span><span class="cx"> 
</span><span class="cx">             array_push($requests, array(
</span><span class="cx">                 'id' =&gt; $row['request_id'],
</span><ins>+                'task' =&gt; $row['task_id'],
</ins><span class="cx">                 'triggerable' =&gt; $row['request_triggerable'],
</span><span class="cx">                 'test' =&gt; $resolve_ids ? $test_path_resolver-&gt;path_for_test($test_id) : $test_id,
</span><span class="cx">                 'platform' =&gt; $resolve_ids ? $id_to_platform_name[$platform_id] : $platform_id,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentsratiobargraphjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -11,7 +11,7 @@
</span><span class="cx"> 
</span><span class="cx">     update(ratio, label, showWarningIcon)
</span><span class="cx">     {
</span><del>-        console.assert(isNaN(ratio) || (ratio &gt;= -1 &amp;&amp; ratio &lt;= 1));
</del><ins>+        console.assert(typeof(ratio) == 'number');
</ins><span class="cx">         this._ratio = ratio;
</span><span class="cx">         this._label = label;
</span><span class="cx">         this._showWarningIcon = showWarningIcon;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -101,6 +101,7 @@
</span><span class="cx">         &lt;script src=&quot;pages/analysis-category-page.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;pages/analysis-task-page.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;pages/create-analysis-task-page.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;pages/build-request-queue-page.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;pages/summary-page.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3mainjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/main.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/main.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/main.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -30,6 +30,9 @@
</span><span class="cx">         var analysisTaskPage = new AnalysisTaskPage();
</span><span class="cx">         analysisTaskPage.setParentPage(analysisCategoryPage);
</span><span class="cx"> 
</span><ins>+        var buildRequestQueuePage = new BuildRequestQueuePage();
+        buildRequestQueuePage.setParentPage(analysisCategoryPage);
+
</ins><span class="cx">         var heading = new Heading(manifest.siteTitle);
</span><span class="cx">         heading.addPageGroup([summaryPage, chartsPage, analysisCategoryPage].filter(function (page) { return page; }));
</span><span class="cx"> 
</span><span class="lines">@@ -42,6 +45,7 @@
</span><span class="cx">         router.addPage(chartsPage);
</span><span class="cx">         router.addPage(createAnalysisTaskPage);
</span><span class="cx">         router.addPage(analysisTaskPage);
</span><ins>+        router.addPage(buildRequestQueuePage);
</ins><span class="cx">         router.addPage(analysisCategoryPage);
</span><span class="cx">         for (var page of dashboardPages)
</span><span class="cx">             router.addPage(page);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsanalysistaskjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -71,7 +71,7 @@
</span><span class="cx">     fixes() { return this._fixes; }
</span><span class="cx">     platform() { return this._platform; }
</span><span class="cx">     metric() { return this._metric; }
</span><del>-    category() { return this._category; }
</del><ins>+
</ins><span class="cx">     changeType() { return this._changeType; }
</span><span class="cx"> 
</span><span class="cx">     updateName(newName) { return this._updateRemoteState({name: newName}); }
</span><span class="lines">@@ -146,12 +146,25 @@
</span><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    category()
+    {
+        var category = 'unconfirmed';
+
+        if (this._changeType == 'unchanged' || this._changeType == 'inconclusive'
+            || (this._changeType == 'regression' &amp;&amp; this._fixes.length)
+            || (this._changeType == 'progression' &amp;&amp; (this._causes.length || this._fixes.length)))
+            category = 'closed';
+        else if (this._causes.length || this._fixes.length || this._changeType == 'regression' || this._changeType == 'progression')
+            category = 'investigated';
+
+        return category;
+    }
+
</ins><span class="cx">     static categories()
</span><span class="cx">     {
</span><span class="cx">         return [
</span><span class="cx">             'unconfirmed',
</span><del>-            'bisecting',
-            'identified',
</del><ins>+            'investigated',
</ins><span class="cx">             'closed'
</span><span class="cx">         ];
</span><span class="cx">     }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsbuildrequestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/models/build-request.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -5,6 +5,8 @@
</span><span class="cx">     constructor(id, object)
</span><span class="cx">     {
</span><span class="cx">         super(id, object);
</span><ins>+        this._triggerable = object.triggerable;
+        this._analysisTaskId = object.task;
</ins><span class="cx">         this._testGroupId = object.testGroupId;
</span><span class="cx">         console.assert(!object.testGroup || object.testGroup instanceof TestGroup);
</span><span class="cx">         this._testGroup = object.testGroup;
</span><span class="lines">@@ -20,6 +22,7 @@
</span><span class="cx">         this._status = object.status;
</span><span class="cx">         this._statusUrl = object.url;
</span><span class="cx">         this._buildId = object.build;
</span><ins>+        this._createdAt = new Date(object.createdAt);
</ins><span class="cx">         this._result = null;
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -33,6 +36,7 @@
</span><span class="cx">         this._buildId = object.build;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    analysisTaskId() { return this._analysisTaskId; }
</ins><span class="cx">     testGroupId() { return this._testGroupId; }
</span><span class="cx">     testGroup() { return this._testGroup; }
</span><span class="cx">     platform() { return this._platform; }
</span><span class="lines">@@ -49,7 +53,7 @@
</span><span class="cx">     {
</span><span class="cx">         switch (this._status) {
</span><span class="cx">         case 'pending':
</span><del>-            return 'Waiting to be scheduled';
</del><ins>+            return 'Waiting';
</ins><span class="cx">         case 'scheduled':
</span><span class="cx">             return 'Scheduled';
</span><span class="cx">         case 'running':
</span><span class="lines">@@ -65,7 +69,45 @@
</span><span class="cx">     statusUrl() { return this._statusUrl; }
</span><span class="cx"> 
</span><span class="cx">     buildId() { return this._buildId; }
</span><ins>+    createdAt() { return this._createdAt; }
</ins><span class="cx"> 
</span><ins>+    waitingTime(referenceTime)
+    {
+        const units = [
+            {unit: 'week', length: 7 * 24 * 3600},
+            {unit: 'day', length: 24 * 3600},
+            {unit: 'hour', length: 3600},
+            {unit: 'minute', length: 60},
+        ];
+
+        var diff = (referenceTime - this.createdAt()) / 1000;
+
+        var indexOfFirstSmallEnoughUnit = units.length - 1;
+        for (var i = 0; i &lt; units.length; i++) {
+            if (diff &gt; 1.5 * units[i].length) {
+                indexOfFirstSmallEnoughUnit = i;
+                break;
+            }
+        }
+
+        var label = '';
+        var lastUnit = false;
+        for (var i = indexOfFirstSmallEnoughUnit; !lastUnit; i++) {
+            lastUnit = i == indexOfFirstSmallEnoughUnit + 1 || i == units.length - 1;
+            var length = units[i].length;
+            var valueForUnit = lastUnit ? Math.round(diff / length) : Math.floor(diff / length);
+
+            var unit = units[i].unit + (valueForUnit == 1 ? '' : 's');
+            if (label)
+                label += ' ';
+            label += `${valueForUnit} ${unit}`;
+
+            diff = diff - valueForUnit * length;
+        }
+
+        return label;
+    }
+
</ins><span class="cx">     result() { return this._result; }
</span><span class="cx">     setResult(result)
</span><span class="cx">     {
</span><span class="lines">@@ -73,6 +115,21 @@
</span><span class="cx">         this._testGroup.didSetResult(this);
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static fetchTriggerables()
+    {
+        return this.cachedFetch('/api/triggerables/').then(function (response) {
+            return response.triggerables.map(function (entry) { return {id: entry.id, name: entry.name}; });
+        });
+    }
+
+    // FIXME: Create a real model object for triggerables.
+    static cachedRequestsForTriggerableID(id)
+    {
+        return this.all().filter(function (request) {
+            return request._triggerable == id;
+        });
+    }
+
</ins><span class="cx">     static fetchForTriggerable(triggerable)
</span><span class="cx">     {
</span><span class="cx">         return RemoteAPI.getJSONWithStatus('/api/build-requests/' + triggerable).then(function (data) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysiscategorypagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-page.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-page.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-page.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -2,8 +2,9 @@
</span><span class="cx"> class AnalysisCategoryPage extends PageWithHeading {
</span><span class="cx">     constructor()
</span><span class="cx">     {
</span><del>-        super('Analysis', new AnalysisCategoryToolbar);
-        this.toolbar().setFilterCallback(this.render.bind(this));
</del><ins>+        super('Analysis');
+        this._categoryToolbar = this.content().querySelector('analysis-category-toolbar').component();
+        this._categoryToolbar.setCategoryPage(this);
</ins><span class="cx">         this._renderedList = false;
</span><span class="cx">         this._renderedFilter = false;
</span><span class="cx">         this._fetched = false;
</span><span class="lines">@@ -12,7 +13,7 @@
</span><span class="cx"> 
</span><span class="cx">     title()
</span><span class="cx">     {
</span><del>-        var category = this.toolbar().currentCategory();
</del><ins>+        var category = this._categoryToolbar.currentCategory();
</ins><span class="cx">         return (category ? category.charAt(0).toUpperCase() + category.slice(1) + ' ' : '') + 'Analysis Tasks';
</span><span class="cx">     }
</span><span class="cx">     routeName() { return 'analysis'; }
</span><span class="lines">@@ -30,26 +31,50 @@
</span><span class="cx">         super.open(state);
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    serializeState()
+    {
+        return this.stateForCategory(this._categoryToolbar.currentCategory());
+    }
+
+    stateForCategory(category)
+    {
+        var state = {category: category};
+        var filter = this._categoryToolbar.filter();
+        if (filter)
+            state.filter = filter;
+        return state;
+    }
+
</ins><span class="cx">     updateFromSerializedState(state, isOpen)
</span><span class="cx">     {
</span><span class="cx">         if (state.category instanceof Set)
</span><span class="cx">             state.category = Array.from(state.category.values())[0];
</span><ins>+        if (state.filter instanceof Set)
+            state.filter = Array.from(state.filter.values())[0];
</ins><span class="cx"> 
</span><del>-        if (this.toolbar().setCategoryIfValid(state.category))
</del><ins>+        if (this._categoryToolbar.setCategoryIfValid(state.category))
</ins><span class="cx">             this._renderedList = false;
</span><span class="cx"> 
</span><ins>+        if (state.filter)
+            this._categoryToolbar.setFilter(state.filter);
+
</ins><span class="cx">         if (!isOpen)
</span><span class="cx">             this.render();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    filterDidChange(shouldUpdateState)
+    {
+        this.render();
+        if (shouldUpdateState)
+            this.scheduleUrlStateUpdate();
+    }
+
</ins><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'render');
</span><span class="cx"> 
</span><del>-        if (!this._renderedList) {
-            super.render();
-            this.toolbar().render();
-        }
</del><ins>+        super.render();
+        this._categoryToolbar.render();
</ins><span class="cx"> 
</span><span class="cx">         if (this._errorMessage) {
</span><span class="cx">             console.assert(!this._fetched);
</span><span class="lines">@@ -69,7 +94,7 @@
</span><span class="cx">             this._renderedList = true;
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-        var filter = this.toolbar().filter();
</del><ins>+        var filter = this._categoryToolbar.filter();
</ins><span class="cx">         if (filter || this._renderedFilter) {
</span><span class="cx">             Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'filterByKeywords');
</span><span class="cx">             var keywordList = filter ? filter.toLowerCase().split(/\s+/) : [];
</span><span class="lines">@@ -98,7 +123,7 @@
</span><span class="cx">         Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'reconstructTaskList');
</span><span class="cx"> 
</span><span class="cx">         console.assert(this.router());
</span><del>-        var currentCategory = this.toolbar().currentCategory();
</del><ins>+        var currentCategory = this._categoryToolbar.currentCategory();
</ins><span class="cx"> 
</span><span class="cx">         var tasks = AnalysisTask.all().filter(function (task) {
</span><span class="cx">             return task.category() == currentCategory;
</span><span class="lines">@@ -164,6 +189,7 @@
</span><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><ins>+            &lt;div class=&quot;toolbar-container&quot;&gt;&lt;analysis-category-toolbar&gt;&lt;/analysis-category-toolbar&gt;&lt;/div&gt;
</ins><span class="cx">             &lt;div class=&quot;analysis-task-category&quot;&gt;
</span><span class="cx">                 &lt;table&gt;
</span><span class="cx">                     &lt;thead&gt;
</span><span class="lines">@@ -184,6 +210,10 @@
</span><span class="cx">     static cssTemplate()
</span><span class="cx">     {
</span><span class="cx">         return `
</span><ins>+            .toolbar-container {
+                text-align: center;
+            }
+
</ins><span class="cx">             .analysis-task-category {
</span><span class="cx">                 width: calc(100% - 2rem);
</span><span class="cx">                 margin: 1rem;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysiscategorytoolbarjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-toolbar.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-toolbar.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-toolbar.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -1,63 +1,57 @@
</span><span class="cx"> 
</span><del>-class AnalysisCategoryToolbar extends Toolbar {
-    constructor()
</del><ins>+class AnalysisCategoryToolbar extends ComponentBase {
+    constructor(categoryPage)
</ins><span class="cx">     {
</span><span class="cx">         super('analysis-category-toolbar');
</span><ins>+        this._categoryPage = null;
</ins><span class="cx">         this._categories = AnalysisTask.categories();
</span><span class="cx">         this._currentCategory = null;
</span><span class="cx">         this._filter = null;
</span><del>-        this._filterCallback = null;
</del><span class="cx">         this.setCategoryIfValid(null);
</span><ins>+
+        this._filterInput = this.content().querySelector('input');
+        this._filterInput.oninput = this._filterMayHaveChanged.bind(this, false);
+        this._filterInput.onchange = this._filterMayHaveChanged.bind(this, true);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    setCategoryPage(categoryPage) { this._categoryPage = categoryPage; }
</ins><span class="cx">     currentCategory() { return this._currentCategory; }
</span><del>-
</del><span class="cx">     filter() { return this._filter; }
</span><del>-    setFilterCallback(callback)
-    {
-        console.assert(!callback || callback instanceof Function);
-        this._filterCallback = callback;
-    }
</del><ins>+    setFilter(filter) { this._filter = filter; }
</ins><span class="cx"> 
</span><span class="cx">     render()
</span><span class="cx">     {
</span><del>-        var router = this.router();
</del><ins>+        if (!this._categoryPage)
+            return;
+
+        var router = this._categoryPage.router();
</ins><span class="cx">         console.assert(router);
</span><span class="cx"> 
</span><del>-        var currentPage = router.currentPage();
-        console.assert(currentPage instanceof AnalysisCategoryPage);
-
</del><span class="cx">         super.render();
</span><span class="cx"> 
</span><span class="cx">         var element = ComponentBase.createElement;
</span><span class="cx">         var link = ComponentBase.createLink;
</span><span class="cx"> 
</span><del>-        var input = element('input',
-            {
-                oninput: this._filterMayHaveChanged.bind(this),
-                onchange: this._filterMayHaveChanged.bind(this),
-            });
-        if (this._filter != null)
-            input.value = this._filter;
</del><ins>+        if (this._filterInput.value != this._filter)
+            this._filterInput.value = this._filter;
</ins><span class="cx"> 
</span><span class="cx">         var currentCategory = this._currentCategory;
</span><del>-        this.renderReplace(this.content().querySelector('.analysis-task-category-toolbar'), [
-            element('ul', {class: 'buttoned-toolbar'},
-                this._categories.map(function (category) {
-                    return element('li',
-                        {class: category == currentCategory ? 'selected' : null},
-                        link(category, router.url(currentPage.routeName(), {category: category})));
-                })),
-            input]);
</del><ins>+        var categoryPage = this._categoryPage;
+        this.renderReplace(this.content().querySelector('.analysis-task-category-toolbar'),
+            this._categories.map(function (category) {
+                return element('li',
+                    {class: category == currentCategory ? 'selected' : null},
+                    link(category, router.url(categoryPage.routeName(), categoryPage.stateForCategory(category))));
+            }));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    _filterMayHaveChanged(event)
</del><ins>+    _filterMayHaveChanged(shouldUpdateState, event)
</ins><span class="cx">     {
</span><span class="cx">         var input = event.target;
</span><span class="cx">         var oldFilter = this._filter;
</span><span class="cx">         this._filter = input.value;
</span><del>-        if (this._filter != oldFilter &amp;&amp; this._filterCallback)
-            this._filterCallback(this._filter);
</del><ins>+        if (this._filter != oldFilter &amp;&amp; this._categoryPage || shouldUpdateState)
+            this._categoryPage.filterDidChange(shouldUpdateState);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     setCategoryIfValid(category)
</span><span class="lines">@@ -67,17 +61,28 @@
</span><span class="cx">         if (this._categories.indexOf(category) &lt; 0)
</span><span class="cx">             return false;
</span><span class="cx">         this._currentCategory = category;
</span><del>-
-        var filterDidChange = !!this._filter;
-        this._filter = null;
-        if (filterDidChange &amp;&amp; this._filterCallback)
-            this._filterCallback(this._filter);
-
</del><span class="cx">         return true;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static cssTemplate()
+    {
+        return Toolbar.cssTemplate() + `
+            .queue-toolbar {
+                position: absolute;
+                right: 1rem;
+            }
+        `
+    }
+
</ins><span class="cx">     static htmlTemplate()
</span><span class="cx">     {
</span><del>-        return `&lt;div class=&quot;buttoned-toolbar analysis-task-category-toolbar&quot;&gt;&lt;/div&gt;`;
</del><ins>+        return `
+            &lt;ul class=&quot;analysis-task-category-toolbar buttoned-toolbar&quot;&gt;&lt;/ul&gt;
+            &lt;input type=&quot;text&quot;&gt;
+            &lt;ul class=&quot;buttoned-toolbar queue-toolbar&quot;&gt;
+                &lt;li&gt;&lt;a href=&quot;#/analysis/queue&quot;&gt;Queue&lt;/a&gt;&lt;/li&gt;
+            &lt;/ul&gt;`;
</ins><span class="cx">     }
</span><span class="cx"> }
</span><ins>+
+ComponentBase.defineElement('analysis-category-toolbar', AnalysisCategoryToolbar);
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesbuildrequestqueuepagejs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/pages/build-request-queue-page.js (0 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/build-request-queue-page.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/build-request-queue-page.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -0,0 +1,166 @@
</span><ins>+
+class BuildRequestQueuePage extends PageWithHeading {
+    constructor()
+    {
+        super('build-request-queue-page');
+        this._triggerables = [];
+    }
+
+    routeName() { return 'analysis/queue'; }
+    pageTitle() { return 'Build Request Queue'; }
+
+    open(state)
+    {
+        var self = this;
+        BuildRequest.fetchTriggerables().then(function (list) {
+            self._triggerables = list.map(function (entry) {
+                var triggerable = {name: entry.name, buildRequests: BuildRequest.cachedRequestsForTriggerableID(entry.id)};
+
+                BuildRequest.fetchForTriggerable(entry.name).then(function (requests) {
+                    triggerable.buildRequests = requests;
+                    self.render();
+                });
+
+                return triggerable;
+            });
+            self.render();
+        });
+
+        AnalysisTask.fetchAll().then(function () {
+            self.render();
+        });
+
+        super.open(state);
+    }
+
+    render()
+    {
+        super.render();
+
+        var referenceTime = Date.now();
+        this.renderReplace(this.content().querySelector('.triggerable-list'),
+            this._triggerables.map(this._constructBuildRequestTable.bind(this, referenceTime)));
+    }
+
+    _constructBuildRequestTable(referenceTime, triggerable)
+    {
+        if (!triggerable.buildRequests.length)
+            return [];
+
+        var rowList = [];
+        var previousRow = null;
+        var requestCount = 0;
+        var requestCountForGroup = {};
+        for (var request of triggerable.buildRequests) {
+            var groupId = request.testGroupId();
+            if (groupId in requestCountForGroup)
+                requestCountForGroup[groupId]++;
+            else
+                requestCountForGroup[groupId] = 1
+
+            if (request.hasFinished())
+                continue;
+
+            requestCount++;
+            if (previousRow &amp;&amp; previousRow.request.testGroupId() == groupId) {
+                if (previousRow.contraction)
+                    previousRow.count++;
+                else
+                    rowList.push({contraction: true, request: previousRow.request, count: 1});
+            } else
+                rowList.push({contraction: false, request: request, count: null});
+            previousRow = rowList[rowList.length - 1];
+        }
+
+        var element = ComponentBase.createElement;
+        var link = ComponentBase.createLink;
+        var router = this.router();
+        return element('table', {class: 'build-request-table'}, [
+            element('caption', `${triggerable.name}: ${requestCount} pending requests`),
+            element('thead', [
+                element('td', 'Request ID'),
+                element('td', 'Platform'),
+                element('td', 'Test'),
+                element('td', 'Analysis Task'),
+                element('td', 'Group'),
+                element('td', 'Order'),
+                element('td', 'Status'),
+                element('td', 'Waiting Time'),
+            ]),
+            element('tbody', rowList.map(function (entry) {
+                if (entry.contraction) {
+                    return element('tr', {class: 'contraction'}, [
+                        element('td', {colspan: 8}, `${entry.count} additional requests`)
+                    ]);
+                }
+
+                var request = entry.request;
+                var taskId = request.analysisTaskId();
+                var task = AnalysisTask.findById(taskId);
+                return element('tr', [
+                    element('td', {class: 'request-id'}, request.id()),
+                    element('td', {class: 'platform'}, request.platform().name()),
+                    element('td', {class: 'test'}, request.test().fullName()),
+                    element('td', {class: 'task'}, !task ? taskId : link(task.name(), router.url(`analysis/task/${task.id()}`))),
+                    element('td', {class: 'test-group'}, request.testGroupId()),
+                    element('td', {class: 'order'}, `${request.order() + 1} of ${requestCountForGroup[request.testGroupId()]}`),
+                    element('td', {class: 'status'}, request.statusLabel()),
+                    element('td', {class: 'wait'}, request.waitingTime(referenceTime))]);
+            }))]);
+    }
+
+    static htmlTemplate()
+    {
+        return `&lt;div class=&quot;triggerable-list&quot;&gt;&lt;/div&gt;`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            .triggerable-list {
+                margin: 1rem;
+            }
+
+            .build-request-table {
+                border-collapse: collapse;
+                border: solid 0px #ccc;
+                font-size: 0.9rem;
+                margin-bottom: 2rem;
+            }
+
+            .build-request-table caption {
+                text-align: left;
+                font-size: 1.2rem;
+                margin: 1rem 0 0.5rem 0;
+                color: #c93;
+            }
+
+            .build-request-table td {
+                padding: 0.2rem;
+            }
+
+            .build-request-table td:not(.test):not(.task) {
+                white-space: nowrap;
+            }
+
+            .build-request-table .contraction {
+                text-align: center;
+                color: #999;
+            }
+
+            .build-request-table tr:not(.contraction) td {
+                border-top: solid 1px #eee;
+            }
+
+            .build-request-table tr:last-child td {
+                border-bottom: solid 1px #eee;
+            }
+
+            .build-request-table thead {
+                font-weight: inherit;
+                color: #c93;
+            }
+        `;
+    }
+
+}
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagessummarypagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -33,7 +33,7 @@
</span><span class="cx">         super.open(state);
</span><span class="cx"> 
</span><span class="cx">         var current = Date.now();
</span><del>-        var timeRange = [current - 7 * 24 * 3600 * 1000, current];
</del><ins>+        var timeRange = [current - 24 * 3600 * 1000, current];
</ins><span class="cx">         for (var group of this._configGroups)
</span><span class="cx">             group.fetchAndComputeSummary(timeRange).then(this.render.bind(this));
</span><span class="cx">     }
</span><span class="lines">@@ -250,7 +250,7 @@
</span><span class="cx">         this._measurementSets = [];
</span><span class="cx">         this._configurationList = [];
</span><span class="cx">         this._setToRatio = new Map;
</span><del>-        this._ratio = null;
</del><ins>+        this._ratio = NaN;
</ins><span class="cx">         this._label = null;
</span><span class="cx">         this._missingPlatforms = new Set;
</span><span class="cx">         this._platformsWithoutBaseline = new Set;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsapibuildrequeststestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -177,7 +177,7 @@
</span><span class="cx">             assert.ok(!buildRequests[0].hasFinished());
</span><span class="cx">             assert.ok(!buildRequests[0].hasStarted());
</span><span class="cx">             assert.ok(buildRequests[0].isPending());
</span><del>-            assert.equal(buildRequests[0].statusLabel(), 'Waiting to be scheduled');
</del><ins>+            assert.equal(buildRequests[0].statusLabel(), 'Waiting');
</ins><span class="cx"> 
</span><span class="cx">             assert.equal(buildRequests[1].id(), 701);
</span><span class="cx">             assert.equal(buildRequests[1].testGroupId(), 600);
</span><span class="lines">@@ -188,7 +188,7 @@
</span><span class="cx">             assert.ok(!buildRequests[1].hasFinished());
</span><span class="cx">             assert.ok(!buildRequests[1].hasStarted());
</span><span class="cx">             assert.ok(buildRequests[1].isPending());
</span><del>-            assert.equal(buildRequests[1].statusLabel(), 'Waiting to be scheduled');
</del><ins>+            assert.equal(buildRequests[1].statusLabel(), 'Waiting');
</ins><span class="cx"> 
</span><span class="cx">             assert.equal(buildRequests[2].id(), 702);
</span><span class="cx">             assert.equal(buildRequests[2].testGroupId(), 600);
</span><span class="lines">@@ -199,7 +199,7 @@
</span><span class="cx">             assert.ok(!buildRequests[2].hasFinished());
</span><span class="cx">             assert.ok(!buildRequests[2].hasStarted());
</span><span class="cx">             assert.ok(buildRequests[2].isPending());
</span><del>-            assert.equal(buildRequests[2].statusLabel(), 'Waiting to be scheduled');
</del><ins>+            assert.equal(buildRequests[2].statusLabel(), 'Waiting');
</ins><span class="cx"> 
</span><span class="cx">             assert.equal(buildRequests[3].id(), 703);
</span><span class="cx">             assert.equal(buildRequests[3].testGroupId(), 600);
</span><span class="lines">@@ -210,7 +210,7 @@
</span><span class="cx">             assert.ok(!buildRequests[3].hasFinished());
</span><span class="cx">             assert.ok(!buildRequests[3].hasStarted());
</span><span class="cx">             assert.ok(buildRequests[3].isPending());
</span><del>-            assert.equal(buildRequests[3].statusLabel(), 'Waiting to be scheduled');
</del><ins>+            assert.equal(buildRequests[3].statusLabel(), 'Waiting');
</ins><span class="cx"> 
</span><span class="cx">             let osx = Repository.findById(9);
</span><span class="cx">             assert.equal(osx.name(), 'OS X');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsanalysistasktestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -175,7 +175,7 @@
</span><span class="cx">                 assert.ok(task.hasResults());
</span><span class="cx">                 assert.ok(task.hasPendingRequests());
</span><span class="cx">                 assert.equal(task.requestLabel(), '6 of 14');
</span><del>-                assert.equal(task.category(), 'identified');
</del><ins>+                assert.equal(task.category(), 'investigated');
</ins><span class="cx">                 assert.equal(task.changeType(), 'regression');
</span><span class="cx">                 assert.equal(task.startMeasurementId(), 37117949);
</span><span class="cx">                 assert.equal(task.startTime(), 1454444458791);
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunittestsbuildrequesttestsjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js (0 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -0,0 +1,158 @@
</span><ins>+'use strict';
+
+const assert = require('assert');
+
+require('../tools/js/v3-models.js');
+let MockModels = require('./resources/mock-v3-models.js').MockModels;
+
+function sampleBuildRequestData()
+{
+    return {
+        &quot;buildRequests&quot;: [{
+            &quot;id&quot;: &quot;16985&quot;,
+            &quot;triggerable&quot;: &quot;3&quot;,
+            &quot;test&quot;: &quot;844&quot;,
+            &quot;platform&quot;: &quot;31&quot;,
+            &quot;testGroup&quot;: &quot;2128&quot;,
+            &quot;order&quot;: &quot;0&quot;,
+            &quot;rootSet&quot;: &quot;4255&quot;,
+            &quot;status&quot;: &quot;pending&quot;,
+            &quot;url&quot;: null,
+            &quot;build&quot;: null,
+            &quot;createdAt&quot;: 1458688514000
+        }],
+        &quot;rootSets&quot;: [{
+            &quot;id&quot;: &quot;4255&quot;,
+            &quot;roots&quot;: [&quot;87832&quot;, &quot;93116&quot;]
+        }, {
+            &quot;id&quot;: &quot;4256&quot;,
+            &quot;roots&quot;: [&quot;87832&quot;, &quot;96336&quot;]
+        }],
+        &quot;roots&quot;: [{
+            &quot;id&quot;: &quot;87832&quot;,
+            &quot;repository&quot;: &quot;9&quot;,
+            &quot;revision&quot;: &quot;10.11 15A284&quot;,
+            &quot;time&quot;: 0
+        }, {
+            &quot;id&quot;: &quot;93116&quot;,
+            &quot;repository&quot;: &quot;11&quot;,
+            &quot;revision&quot;: &quot;191622&quot;,
+            &quot;time&quot;: 1445945816878
+        }, {
+            &quot;id&quot;: &quot;87832&quot;,
+            &quot;repository&quot;: &quot;9&quot;,
+            &quot;revision&quot;: &quot;10.11 15A284&quot;,
+            &quot;time&quot;: 0
+        }, {
+            &quot;id&quot;: &quot;96336&quot;,
+            &quot;repository&quot;: &quot;11&quot;,
+            &quot;revision&quot;: &quot;192736&quot;,
+            &quot;time&quot;: 1448225325650
+        }],
+        &quot;status&quot;: &quot;OK&quot;
+    };
+}
+
+describe('TestGroup', function () {
+    MockModels.inject();
+
+    describe('waitingTime', function () {
+        it('should return &quot;0 minutes&quot; when the reference time is identically equal to createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '0 minutes');
+        });
+
+        it('should return &quot;1 minute&quot; when the reference time is exactly one minute head of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - 60 * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '1 minute');
+        });
+
+        it('should return &quot;1 minute&quot; when the reference time is 75 seconds head of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - 75 * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '1 minute');
+        });
+
+        it('should return &quot;2 minutes&quot; when the reference time is 118 seconds head of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - 118 * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '2 minutes');
+        });
+
+        it('should return &quot;75 minutes&quot; when the reference time is 75 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - 75 * 60 * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '75 minutes');
+        });
+
+        it('should return &quot;1 hour 58 minutes&quot; when the reference time is 118 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - 118 * 60 * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '1 hour 58 minutes');
+        });
+
+        it('should return &quot;3 hours 2 minutes&quot; when the reference time is 182 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - 182 * 60 * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '3 hours 2 minutes');
+        });
+
+        it('should return &quot;27 hours 14 minutes&quot; when the reference time is 27 hours 14 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - (27 * 3600 + 14 * 60) * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '27 hours 14 minutes');
+        });
+
+        it('should return &quot;2 days 3 hours&quot; when the reference time is 51 hours 14 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - (51 * 3600 + 14 * 60) * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '2 days 3 hours');
+        });
+
+        it('should return &quot;2 days 0 hours&quot; when the reference time is 48 hours 1 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - (48 * 3600 + 1 * 60) * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '2 days 0 hours');
+        });
+
+        it('should return &quot;2 days 2 hours&quot; when the reference time is 49 hours 59 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - (49 * 3600 + 59 * 60) * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '2 days 2 hours');
+        });
+
+        it('should return &quot;2 weeks 6 days&quot; when the reference time is 20 days 5 hours 21 minutes ahead of createdAt', function () {
+            const data = sampleBuildRequestData();
+            const now = Date.now();
+            data.buildRequests[0].createdAt = now - ((20 * 24 + 5) * 3600 + 21 * 60) * 1000;
+            const request = BuildRequest.constructBuildRequestsFromData(data)[0];
+            assert.equal(request.waitingTime(now), '2 weeks 6 days');
+        });
+
+    });
+
+});
</ins><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgunitteststestgroupstestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js (203708 => 203709)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js        2016-07-26 02:40:04 UTC (rev 203708)
+++ trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js        2016-07-26 02:45:13 UTC (rev 203709)
</span><span class="lines">@@ -140,7 +140,7 @@
</span><span class="cx">             assert.ok(!buildRequests[0].hasFinished());
</span><span class="cx">             assert.ok(!buildRequests[0].hasStarted());
</span><span class="cx">             assert.ok(buildRequests[0].isPending());
</span><del>-            assert.equal(buildRequests[0].statusLabel(), 'Waiting to be scheduled');
</del><ins>+            assert.equal(buildRequests[0].statusLabel(), 'Waiting');
</ins><span class="cx">             assert.equal(buildRequests[0].buildId(), null);
</span><span class="cx">             assert.equal(buildRequests[0].result(), null);
</span><span class="cx"> 
</span><span class="lines">@@ -149,7 +149,7 @@
</span><span class="cx">             assert.ok(!buildRequests[1].hasFinished());
</span><span class="cx">             assert.ok(!buildRequests[1].hasStarted());
</span><span class="cx">             assert.ok(buildRequests[1].isPending());
</span><del>-            assert.equal(buildRequests[1].statusLabel(), 'Waiting to be scheduled');
</del><ins>+            assert.equal(buildRequests[1].statusLabel(), 'Waiting');
</ins><span class="cx">             assert.equal(buildRequests[1].buildId(), null);
</span><span class="cx">             assert.equal(buildRequests[1].result(), null);
</span><span class="cx">         });
</span></span></pre>
</div>
</div>

</body>
</html>