Title: [203709] trunk/Websites/perf.webkit.org
Revision
203709
Author
rn...@webkit.org
Date
2016-07-25 19:45:13 -0700 (Mon, 25 Jul 2016)

Log Message

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
"Bisecting" and "Identified" into "Investigated" 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 "bisecting" and "identified" into "investigated".
(AnalysisTask.categories): Merged "bisecting" and "identified" into "investigated".

* 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: "Waiting" for "pending" status.
(BuildRequest.prototype.createdAt): Added.
(BuildRequest.prototype.waitingTime): Added. Returns a human readable time duration since the creation
of this build request such as "2 hours 21 minutes" 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 "contraction" 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.

Modified Paths

Added Paths

Diff

Modified: trunk/Websites/perf.webkit.org/ChangeLog (203708 => 203709)


--- 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)
@@ -1,3 +1,119 @@
+2016-07-23  Ryosuke Niwa  <rn...@webkit.org>
+
+        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
+        "Bisecting" and "Identified" into "Investigated" 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 "bisecting" and "identified" into "investigated".
+        (AnalysisTask.categories): Merged "bisecting" and "identified" into "investigated".
+
+        * 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: "Waiting" for "pending" status.
+        (BuildRequest.prototype.createdAt): Added.
+        (BuildRequest.prototype.waitingTime): Added. Returns a human readable time duration since the creation
+        of this build request such as "2 hours 21 minutes" 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 "contraction" 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.
+
 2016-07-22  Ryosuke Niwa  <rn...@webkit.org>
 
         REGRESSION(r203035): Marking points as an outlier no longer updates charts

Modified: trunk/Websites/perf.webkit.org/public/api/analysis-tasks.php (203708 => 203709)


--- 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)
@@ -83,7 +83,6 @@
         $task = &$task_by_id[$build_count['task']];
         $task['buildRequestCount'] = $build_count['total'];
         $task['finishedBuildRequestCount'] = $build_count['finished'];
-        $task['category'] = determine_category($task);
     }
 
     return array('analysisTasks' => $tasks, 'bugs' => $bugs, 'commits' => $commits);
@@ -103,7 +102,6 @@
         'startRunTime' => Database::to_js_time($task_row['task_start_run_time']),
         'endRun' => $task_row['task_end_run'],
         'endRunTime' => Database::to_js_time($task_row['task_end_run_time']),
-        'category' => null,
         'result' => $task_row['task_result'],
         'needed' => $task_row['task_needed'] ? Database::is_true($task_row['task_needed']) : null,
         'bugs' => array(),
@@ -112,20 +110,6 @@
     );
 }
 
-function determine_category($task) {
-    $category = 'unconfirmed';
-
-    $result = $task['result'];
-    if ($result == 'unchanged' || $result == 'inconclusive' || $task['fixes'] || ($result == 'progression' && $task['causes']))
-        $category = 'closed';
-    else if ($task['causes'])
-        $category = 'identified';
-    else if ($result)
-        $category = 'bisecting';
-
-    return $category;
-}
-
 main(array_key_exists('PATH_INFO', $_SERVER) ? explode('/', trim($_SERVER['PATH_INFO'], '/')) : array());
 
 ?>

Modified: trunk/Websites/perf.webkit.org/public/api/test-groups.php (203708 => 203709)


--- 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)
@@ -19,7 +19,7 @@
         if (!$group)
             exit_with_error('GroupNotFound', array('id' => $group_id));
         $test_groups = array($group);
-        $build_requests_fetcher->fetch_for_group($group_id);
+        $build_requests_fetcher->fetch_for_group($group['testgroup_task'], $group_id);
     } else {
         $task_id = array_get($_GET, 'task');
         if (!$task_id)

Modified: trunk/Websites/perf.webkit.org/public/include/build-requests-fetcher.php (203708 => 203709)


--- 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)
@@ -13,21 +13,23 @@
     }
 
     function fetch_for_task($task_id) {
-        $this->rows = $this->db->query_and_fetch_all('SELECT *
+        $this->rows = $this->db->query_and_fetch_all('SELECT *, testgroup_task as task_id
             FROM build_requests LEFT OUTER JOIN builds ON request_build = build_id, analysis_test_groups
             WHERE request_group = testgroup_id AND testgroup_task = $1
             ORDER BY request_group, request_order', array($task_id));
     }
 
-    function fetch_for_group($test_group_id) {
+    function fetch_for_group($task_id, $test_group_id) {
         $this->rows = $this->db->query_and_fetch_all('SELECT *
             FROM build_requests LEFT OUTER JOIN builds ON request_build = build_id
             WHERE request_group = $1 ORDER BY request_order', array($test_group_id));
+        foreach ($this->rows as &$row)
+            $row['task_id'] = $task_id;
     }
 
     function fetch_incomplete_requests_for_triggerable($triggerable_id) {
-        $this->rows = $this->db->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
+        $this->rows = $this->db->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
                 FROM analysis_test_groups WHERE EXISTS
                     (SELECT 1 FROM build_requests WHERE testgroup_id = request_group AND request_status
                         IN (\'pending\', \'scheduled\', \'running\'))) AS test_groups
@@ -64,6 +66,7 @@
 
             array_push($requests, array(
                 'id' => $row['request_id'],
+                'task' => $row['task_id'],
                 'triggerable' => $row['request_triggerable'],
                 'test' => $resolve_ids ? $test_path_resolver->path_for_test($test_id) : $test_id,
                 'platform' => $resolve_ids ? $id_to_platform_name[$platform_id] : $platform_id,

Modified: trunk/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js (203708 => 203709)


--- 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)
@@ -11,7 +11,7 @@
 
     update(ratio, label, showWarningIcon)
     {
-        console.assert(isNaN(ratio) || (ratio >= -1 && ratio <= 1));
+        console.assert(typeof(ratio) == 'number');
         this._ratio = ratio;
         this._label = label;
         this._showWarningIcon = showWarningIcon;

Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (203708 => 203709)


--- 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)
@@ -101,6 +101,7 @@
         <script src=""
         <script src=""
         <script src=""
+        <script src=""
         <script src=""
 
         <script src=""

Modified: trunk/Websites/perf.webkit.org/public/v3/main.js (203708 => 203709)


--- 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)
@@ -30,6 +30,9 @@
         var analysisTaskPage = new AnalysisTaskPage();
         analysisTaskPage.setParentPage(analysisCategoryPage);
 
+        var buildRequestQueuePage = new BuildRequestQueuePage();
+        buildRequestQueuePage.setParentPage(analysisCategoryPage);
+
         var heading = new Heading(manifest.siteTitle);
         heading.addPageGroup([summaryPage, chartsPage, analysisCategoryPage].filter(function (page) { return page; }));
 
@@ -42,6 +45,7 @@
         router.addPage(chartsPage);
         router.addPage(createAnalysisTaskPage);
         router.addPage(analysisTaskPage);
+        router.addPage(buildRequestQueuePage);
         router.addPage(analysisCategoryPage);
         for (var page of dashboardPages)
             router.addPage(page);

Modified: trunk/Websites/perf.webkit.org/public/v3/models/analysis-task.js (203708 => 203709)


--- 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)
@@ -71,7 +71,7 @@
     fixes() { return this._fixes; }
     platform() { return this._platform; }
     metric() { return this._metric; }
-    category() { return this._category; }
+
     changeType() { return this._changeType; }
 
     updateName(newName) { return this._updateRemoteState({name: newName}); }
@@ -146,12 +146,25 @@
         });
     }
 
+    category()
+    {
+        var category = 'unconfirmed';
+
+        if (this._changeType == 'unchanged' || this._changeType == 'inconclusive'
+            || (this._changeType == 'regression' && this._fixes.length)
+            || (this._changeType == 'progression' && (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;
+    }
+
     static categories()
     {
         return [
             'unconfirmed',
-            'bisecting',
-            'identified',
+            'investigated',
             'closed'
         ];
     }

Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (203708 => 203709)


--- 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)
@@ -5,6 +5,8 @@
     constructor(id, object)
     {
         super(id, object);
+        this._triggerable = object.triggerable;
+        this._analysisTaskId = object.task;
         this._testGroupId = object.testGroupId;
         console.assert(!object.testGroup || object.testGroup instanceof TestGroup);
         this._testGroup = object.testGroup;
@@ -20,6 +22,7 @@
         this._status = object.status;
         this._statusUrl = object.url;
         this._buildId = object.build;
+        this._createdAt = new Date(object.createdAt);
         this._result = null;
     }
 
@@ -33,6 +36,7 @@
         this._buildId = object.build;
     }
 
+    analysisTaskId() { return this._analysisTaskId; }
     testGroupId() { return this._testGroupId; }
     testGroup() { return this._testGroup; }
     platform() { return this._platform; }
@@ -49,7 +53,7 @@
     {
         switch (this._status) {
         case 'pending':
-            return 'Waiting to be scheduled';
+            return 'Waiting';
         case 'scheduled':
             return 'Scheduled';
         case 'running':
@@ -65,7 +69,45 @@
     statusUrl() { return this._statusUrl; }
 
     buildId() { return this._buildId; }
+    createdAt() { return this._createdAt; }
 
+    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 < units.length; i++) {
+            if (diff > 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;
+    }
+
     result() { return this._result; }
     setResult(result)
     {
@@ -73,6 +115,21 @@
         this._testGroup.didSetResult(this);
     }
 
+    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;
+        });
+    }
+
     static fetchForTriggerable(triggerable)
     {
         return RemoteAPI.getJSONWithStatus('/api/build-requests/' + triggerable).then(function (data) {

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-page.js (203708 => 203709)


--- 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)
@@ -2,8 +2,9 @@
 class AnalysisCategoryPage extends PageWithHeading {
     constructor()
     {
-        super('Analysis', new AnalysisCategoryToolbar);
-        this.toolbar().setFilterCallback(this.render.bind(this));
+        super('Analysis');
+        this._categoryToolbar = this.content().querySelector('analysis-category-toolbar').component();
+        this._categoryToolbar.setCategoryPage(this);
         this._renderedList = false;
         this._renderedFilter = false;
         this._fetched = false;
@@ -12,7 +13,7 @@
 
     title()
     {
-        var category = this.toolbar().currentCategory();
+        var category = this._categoryToolbar.currentCategory();
         return (category ? category.charAt(0).toUpperCase() + category.slice(1) + ' ' : '') + 'Analysis Tasks';
     }
     routeName() { return 'analysis'; }
@@ -30,26 +31,50 @@
         super.open(state);
     }
 
+    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;
+    }
+
     updateFromSerializedState(state, isOpen)
     {
         if (state.category instanceof Set)
             state.category = Array.from(state.category.values())[0];
+        if (state.filter instanceof Set)
+            state.filter = Array.from(state.filter.values())[0];
 
-        if (this.toolbar().setCategoryIfValid(state.category))
+        if (this._categoryToolbar.setCategoryIfValid(state.category))
             this._renderedList = false;
 
+        if (state.filter)
+            this._categoryToolbar.setFilter(state.filter);
+
         if (!isOpen)
             this.render();
     }
 
+    filterDidChange(shouldUpdateState)
+    {
+        this.render();
+        if (shouldUpdateState)
+            this.scheduleUrlStateUpdate();
+    }
+
     render()
     {
         Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'render');
 
-        if (!this._renderedList) {
-            super.render();
-            this.toolbar().render();
-        }
+        super.render();
+        this._categoryToolbar.render();
 
         if (this._errorMessage) {
             console.assert(!this._fetched);
@@ -69,7 +94,7 @@
             this._renderedList = true;
         }
 
-        var filter = this.toolbar().filter();
+        var filter = this._categoryToolbar.filter();
         if (filter || this._renderedFilter) {
             Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'filterByKeywords');
             var keywordList = filter ? filter.toLowerCase().split(/\s+/) : [];
@@ -98,7 +123,7 @@
         Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'reconstructTaskList');
 
         console.assert(this.router());
-        var currentCategory = this.toolbar().currentCategory();
+        var currentCategory = this._categoryToolbar.currentCategory();
 
         var tasks = AnalysisTask.all().filter(function (task) {
             return task.category() == currentCategory;
@@ -164,6 +189,7 @@
     static htmlTemplate()
     {
         return `
+            <div class="toolbar-container"><analysis-category-toolbar></analysis-category-toolbar></div>
             <div class="analysis-task-category">
                 <table>
                     <thead>
@@ -184,6 +210,10 @@
     static cssTemplate()
     {
         return `
+            .toolbar-container {
+                text-align: center;
+            }
+
             .analysis-task-category {
                 width: calc(100% - 2rem);
                 margin: 1rem;

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-category-toolbar.js (203708 => 203709)


--- 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)
@@ -1,63 +1,57 @@
 
-class AnalysisCategoryToolbar extends Toolbar {
-    constructor()
+class AnalysisCategoryToolbar extends ComponentBase {
+    constructor(categoryPage)
     {
         super('analysis-category-toolbar');
+        this._categoryPage = null;
         this._categories = AnalysisTask.categories();
         this._currentCategory = null;
         this._filter = null;
-        this._filterCallback = null;
         this.setCategoryIfValid(null);
+
+        this._filterInput = this.content().querySelector('input');
+        this._filterInput._oninput_ = this._filterMayHaveChanged.bind(this, false);
+        this._filterInput._onchange_ = this._filterMayHaveChanged.bind(this, true);
     }
 
+    setCategoryPage(categoryPage) { this._categoryPage = categoryPage; }
     currentCategory() { return this._currentCategory; }
-
     filter() { return this._filter; }
-    setFilterCallback(callback)
-    {
-        console.assert(!callback || callback instanceof Function);
-        this._filterCallback = callback;
-    }
+    setFilter(filter) { this._filter = filter; }
 
     render()
     {
-        var router = this.router();
+        if (!this._categoryPage)
+            return;
+
+        var router = this._categoryPage.router();
         console.assert(router);
 
-        var currentPage = router.currentPage();
-        console.assert(currentPage instanceof AnalysisCategoryPage);
-
         super.render();
 
         var element = ComponentBase.createElement;
         var link = ComponentBase.createLink;
 
-        var input = element('input',
-            {
-                oninput: this._filterMayHaveChanged.bind(this),
-                onchange: this._filterMayHaveChanged.bind(this),
-            });
-        if (this._filter != null)
-            input.value = this._filter;
+        if (this._filterInput.value != this._filter)
+            this._filterInput.value = this._filter;
 
         var currentCategory = this._currentCategory;
-        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]);
+        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))));
+            }));
     }
 
-    _filterMayHaveChanged(event)
+    _filterMayHaveChanged(shouldUpdateState, event)
     {
         var input = event.target;
         var oldFilter = this._filter;
         this._filter = input.value;
-        if (this._filter != oldFilter && this._filterCallback)
-            this._filterCallback(this._filter);
+        if (this._filter != oldFilter && this._categoryPage || shouldUpdateState)
+            this._categoryPage.filterDidChange(shouldUpdateState);
     }
 
     setCategoryIfValid(category)
@@ -67,17 +61,28 @@
         if (this._categories.indexOf(category) < 0)
             return false;
         this._currentCategory = category;
-
-        var filterDidChange = !!this._filter;
-        this._filter = null;
-        if (filterDidChange && this._filterCallback)
-            this._filterCallback(this._filter);
-
         return true;
     }
 
+    static cssTemplate()
+    {
+        return Toolbar.cssTemplate() + `
+            .queue-toolbar {
+                position: absolute;
+                right: 1rem;
+            }
+        `
+    }
+
     static htmlTemplate()
     {
-        return `<div class="buttoned-toolbar analysis-task-category-toolbar"></div>`;
+        return `
+            <ul class="analysis-task-category-toolbar buttoned-toolbar"></ul>
+            <input type="text">
+            <ul class="buttoned-toolbar queue-toolbar">
+                <li><a href=""
+            </ul>`;
     }
 }
+
+ComponentBase.defineElement('analysis-category-toolbar', AnalysisCategoryToolbar);

Added: trunk/Websites/perf.webkit.org/public/v3/pages/build-request-queue-page.js (0 => 203709)


--- 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)
@@ -0,0 +1,166 @@
+
+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 && 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 `<div class="triggerable-list"></div>`;
+    }
+
+    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;
+            }
+        `;
+    }
+
+}

Modified: trunk/Websites/perf.webkit.org/public/v3/pages/summary-page.js (203708 => 203709)


--- 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)
@@ -33,7 +33,7 @@
         super.open(state);
 
         var current = Date.now();
-        var timeRange = [current - 7 * 24 * 3600 * 1000, current];
+        var timeRange = [current - 24 * 3600 * 1000, current];
         for (var group of this._configGroups)
             group.fetchAndComputeSummary(timeRange).then(this.render.bind(this));
     }
@@ -250,7 +250,7 @@
         this._measurementSets = [];
         this._configurationList = [];
         this._setToRatio = new Map;
-        this._ratio = null;
+        this._ratio = NaN;
         this._label = null;
         this._missingPlatforms = new Set;
         this._platformsWithoutBaseline = new Set;

Modified: trunk/Websites/perf.webkit.org/server-tests/api-build-requests-tests.js (203708 => 203709)


--- 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)
@@ -177,7 +177,7 @@
             assert.ok(!buildRequests[0].hasFinished());
             assert.ok(!buildRequests[0].hasStarted());
             assert.ok(buildRequests[0].isPending());
-            assert.equal(buildRequests[0].statusLabel(), 'Waiting to be scheduled');
+            assert.equal(buildRequests[0].statusLabel(), 'Waiting');
 
             assert.equal(buildRequests[1].id(), 701);
             assert.equal(buildRequests[1].testGroupId(), 600);
@@ -188,7 +188,7 @@
             assert.ok(!buildRequests[1].hasFinished());
             assert.ok(!buildRequests[1].hasStarted());
             assert.ok(buildRequests[1].isPending());
-            assert.equal(buildRequests[1].statusLabel(), 'Waiting to be scheduled');
+            assert.equal(buildRequests[1].statusLabel(), 'Waiting');
 
             assert.equal(buildRequests[2].id(), 702);
             assert.equal(buildRequests[2].testGroupId(), 600);
@@ -199,7 +199,7 @@
             assert.ok(!buildRequests[2].hasFinished());
             assert.ok(!buildRequests[2].hasStarted());
             assert.ok(buildRequests[2].isPending());
-            assert.equal(buildRequests[2].statusLabel(), 'Waiting to be scheduled');
+            assert.equal(buildRequests[2].statusLabel(), 'Waiting');
 
             assert.equal(buildRequests[3].id(), 703);
             assert.equal(buildRequests[3].testGroupId(), 600);
@@ -210,7 +210,7 @@
             assert.ok(!buildRequests[3].hasFinished());
             assert.ok(!buildRequests[3].hasStarted());
             assert.ok(buildRequests[3].isPending());
-            assert.equal(buildRequests[3].statusLabel(), 'Waiting to be scheduled');
+            assert.equal(buildRequests[3].statusLabel(), 'Waiting');
 
             let osx = Repository.findById(9);
             assert.equal(osx.name(), 'OS X');

Modified: trunk/Websites/perf.webkit.org/unit-tests/analysis-task-tests.js (203708 => 203709)


--- 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)
@@ -175,7 +175,7 @@
                 assert.ok(task.hasResults());
                 assert.ok(task.hasPendingRequests());
                 assert.equal(task.requestLabel(), '6 of 14');
-                assert.equal(task.category(), 'identified');
+                assert.equal(task.category(), 'investigated');
                 assert.equal(task.changeType(), 'regression');
                 assert.equal(task.startMeasurementId(), 37117949);
                 assert.equal(task.startTime(), 1454444458791);

Added: trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js (0 => 203709)


--- 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)
@@ -0,0 +1,158 @@
+'use strict';
+
+const assert = require('assert');
+
+require('../tools/js/v3-models.js');
+let MockModels = require('./resources/mock-v3-models.js').MockModels;
+
+function sampleBuildRequestData()
+{
+    return {
+        "buildRequests": [{
+            "id": "16985",
+            "triggerable": "3",
+            "test": "844",
+            "platform": "31",
+            "testGroup": "2128",
+            "order": "0",
+            "rootSet": "4255",
+            "status": "pending",
+            "url": null,
+            "build": null,
+            "createdAt": 1458688514000
+        }],
+        "rootSets": [{
+            "id": "4255",
+            "roots": ["87832", "93116"]
+        }, {
+            "id": "4256",
+            "roots": ["87832", "96336"]
+        }],
+        "roots": [{
+            "id": "87832",
+            "repository": "9",
+            "revision": "10.11 15A284",
+            "time": 0
+        }, {
+            "id": "93116",
+            "repository": "11",
+            "revision": "191622",
+            "time": 1445945816878
+        }, {
+            "id": "87832",
+            "repository": "9",
+            "revision": "10.11 15A284",
+            "time": 0
+        }, {
+            "id": "96336",
+            "repository": "11",
+            "revision": "192736",
+            "time": 1448225325650
+        }],
+        "status": "OK"
+    };
+}
+
+describe('TestGroup', function () {
+    MockModels.inject();
+
+    describe('waitingTime', function () {
+        it('should return "0 minutes" when the reference time is identically equal to createdAt', function () {
+            const data = ""
+            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 "1 minute" when the reference time is exactly one minute head of createdAt', function () {
+            const data = ""
+            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 "1 minute" when the reference time is 75 seconds head of createdAt', function () {
+            const data = ""
+            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 "2 minutes" when the reference time is 118 seconds head of createdAt', function () {
+            const data = ""
+            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 "75 minutes" when the reference time is 75 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "1 hour 58 minutes" when the reference time is 118 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "3 hours 2 minutes" when the reference time is 182 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "27 hours 14 minutes" when the reference time is 27 hours 14 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "2 days 3 hours" when the reference time is 51 hours 14 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "2 days 0 hours" when the reference time is 48 hours 1 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "2 days 2 hours" when the reference time is 49 hours 59 minutes ahead of createdAt', function () {
+            const data = ""
+            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 "2 weeks 6 days" when the reference time is 20 days 5 hours 21 minutes ahead of createdAt', function () {
+            const data = ""
+            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');
+        });
+
+    });
+
+});
\ No newline at end of file

Modified: trunk/Websites/perf.webkit.org/unit-tests/test-groups-tests.js (203708 => 203709)


--- 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)
@@ -140,7 +140,7 @@
             assert.ok(!buildRequests[0].hasFinished());
             assert.ok(!buildRequests[0].hasStarted());
             assert.ok(buildRequests[0].isPending());
-            assert.equal(buildRequests[0].statusLabel(), 'Waiting to be scheduled');
+            assert.equal(buildRequests[0].statusLabel(), 'Waiting');
             assert.equal(buildRequests[0].buildId(), null);
             assert.equal(buildRequests[0].result(), null);
 
@@ -149,7 +149,7 @@
             assert.ok(!buildRequests[1].hasFinished());
             assert.ok(!buildRequests[1].hasStarted());
             assert.ok(buildRequests[1].isPending());
-            assert.equal(buildRequests[1].statusLabel(), 'Waiting to be scheduled');
+            assert.equal(buildRequests[1].statusLabel(), 'Waiting');
             assert.equal(buildRequests[1].buildId(), null);
             assert.equal(buildRequests[1].result(), null);
         });
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to