<!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>[210626] 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/210626">210626</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2017-01-11 23:18:28 -0800 (Wed, 11 Jan 2017)</dd>
</dl>

<h3>Log Message</h3>
<pre>Hide the UI to trigger an A/B testing when there are no triggerables
https://bugs.webkit.org/show_bug.cgi?id=166964

Reviewed by Yusuke Suzuki.

Hide the &quot;Start A/B Testing&quot; button on analysis task pages instead of showing it and failing later
when the user tries to create one it with a TriggerableNotFound error.

Added the list of triggerables to the manifest JSON so that we can determine this condition without
having to fetch /api/triggerable for each analysis task as done in v2 UI.

* public/admin/reprocess-report.php:
* public/api/manifest.php:
* public/api/report.php:
* public/include/admin-header.php:
* public/include/manifest-generator.php: Moved from public/include/manifest.php.
(ManifestGenerator::generate):
(ManifestGenerator::triggerables): Added. Include the list of repositories this triggerable accepts
as well as the list of (test, platform) pairs on which this triggerable is available.
Use [testId, platformId] instead of a dictionary to reduce the file size.
* public/v3/components/customizable-test-group-form.js:
(CustomizableTestGroupForm): Removed this._disabled. This variable was used in TestGroupFrom to
disable the &quot;Start A/B Testing&quot; button when no range is selected but this ended up racy. Compute
the visibility of the button in render() function instead.
(CustomizableTestGroupForm.prototype.setRootSetMap):
(CustomizableTestGroupForm.prototype._submitted):
(CustomizableTestGroupForm.prototype.render): Hide the customize link and the button as needed.
The &quot;Start A/B Testing&quot; button must be hidden when either no range is selected or no title is typed.
&quot;Customize&quot; button must be hidden when no range is selected.
* public/v3/components/test-group-form.js:
(TestGroupForm): Removed _disabled since it's no longer used.
(TestGroupForm.prototype.setDisabled): Ditto.
(TestGroupForm.prototype.render): Ditto.
* public/v3/index.html: Include triggerable.js.
* public/v3/models/manifest.js:
(Manifest._didFetchManifest): Modernized. Create Triggerable objects from the manifest JSON.
* public/v3/models/triggerable.js: Added.
(Triggerable): Add this triggerable object to the static map of (test id, platform id) pair.
(Triggerable.prototype.acceptedRepositories): Added.
(Triggerable.findByTestConfiguration): Added. Finds a triggerable in the aforementioned static map.
* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype._updateStatus): Added. Re-render the page since time series data
points that were previously not available may have become available. The lack of this update was
causing a race condition in which the &quot;Start A/B Testing&quot; button for the charts is disabled even
after a group name had been specified because setRootSetMap was never called with a valid set.
(AnalysisTaskPage): Added this._triggerable.
(AnalysisTaskPage.prototype._didFetchTask): Find the triggerable now that we've fetched the task.
(AnalysisTaskPage.prototype.render): Hide the group view (the table of A/B testing results) entirely
when there are no groups to show. Also hide the forms to start A/B testing when there are no matching
triggerable, which is the main feature of this patch.
* server-tests/api-manifest.js: Added a test for including a list of triggerables in the manifest JSON.
* server-tests/resources/mock-data.js:
(MockData.resetV3Models): Reset Triggerable's static map.
* server-tests/tools-buildbot-triggerable-tests.js: Assert that Triggerable objects are  constructed
with appropriate list of repositories and (test, platform) associations.
* tools/js/database.js:
(tableToPrefixMap): Added triggerable_repositories's prefix.
* tools/js/remote.js:
(RemoteAPI.prototype.getJSON): Log the entire response to stderr when JSON.parse fails to aid debugging.
* tools/js/v3-models.js: Import triggerable.js.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicadminreprocessreportphp">trunk/Websites/perf.webkit.org/public/admin/reprocess-report.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapimanifestphp">trunk/Websites/perf.webkit.org/public/api/manifest.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapireportphp">trunk/Websites/perf.webkit.org/public/api/report.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludeadminheaderphp">trunk/Websites/perf.webkit.org/public/include/admin-header.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentscustomizabletestgroupformjs">trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3componentstestgroupformjs">trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3indexhtml">trunk/Websites/perf.webkit.org/public/v3/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelsmanifestjs">trunk/Websites/perf.webkit.org/public/v3/models/manifest.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs">trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsapimanifestjs">trunk/Websites/perf.webkit.org/server-tests/api-manifest.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgservertestsresourcesmockdatajs">trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgserverteststoolsbuildbottriggerabletestsjs">trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsdatabasejs">trunk/Websites/perf.webkit.org/tools/js/database.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsremotejs">trunk/Websites/perf.webkit.org/tools/js/remote.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtoolsjsv3modelsjs">trunk/Websites/perf.webkit.org/tools/js/v3-models.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludemanifestgeneratorphp">trunk/Websites/perf.webkit.org/public/include/manifest-generator.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv3modelstriggerablejs">trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludemanifestphp">trunk/Websites/perf.webkit.org/public/include/manifest.php</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 (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -1,3 +1,66 @@
</span><ins>+2017-01-12  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Hide the UI to trigger an A/B testing when there are no triggerables
+        https://bugs.webkit.org/show_bug.cgi?id=166964
+
+        Reviewed by Yusuke Suzuki.
+
+        Hide the &quot;Start A/B Testing&quot; button on analysis task pages instead of showing it and failing later
+        when the user tries to create one it with a TriggerableNotFound error.
+
+        Added the list of triggerables to the manifest JSON so that we can determine this condition without
+        having to fetch /api/triggerable for each analysis task as done in v2 UI.
+
+        * public/admin/reprocess-report.php:
+        * public/api/manifest.php:
+        * public/api/report.php:
+        * public/include/admin-header.php:
+        * public/include/manifest-generator.php: Moved from public/include/manifest.php.
+        (ManifestGenerator::generate):
+        (ManifestGenerator::triggerables): Added. Include the list of repositories this triggerable accepts
+        as well as the list of (test, platform) pairs on which this triggerable is available.
+        Use [testId, platformId] instead of a dictionary to reduce the file size.
+        * public/v3/components/customizable-test-group-form.js:
+        (CustomizableTestGroupForm): Removed this._disabled. This variable was used in TestGroupFrom to
+        disable the &quot;Start A/B Testing&quot; button when no range is selected but this ended up racy. Compute
+        the visibility of the button in render() function instead.
+        (CustomizableTestGroupForm.prototype.setRootSetMap):
+        (CustomizableTestGroupForm.prototype._submitted):
+        (CustomizableTestGroupForm.prototype.render): Hide the customize link and the button as needed.
+        The &quot;Start A/B Testing&quot; button must be hidden when either no range is selected or no title is typed.
+        &quot;Customize&quot; button must be hidden when no range is selected.
+        * public/v3/components/test-group-form.js:
+        (TestGroupForm): Removed _disabled since it's no longer used.
+        (TestGroupForm.prototype.setDisabled): Ditto.
+        (TestGroupForm.prototype.render): Ditto.
+        * public/v3/index.html: Include triggerable.js.
+        * public/v3/models/manifest.js:
+        (Manifest._didFetchManifest): Modernized. Create Triggerable objects from the manifest JSON.
+        * public/v3/models/triggerable.js: Added.
+        (Triggerable): Add this triggerable object to the static map of (test id, platform id) pair.
+        (Triggerable.prototype.acceptedRepositories): Added.
+        (Triggerable.findByTestConfiguration): Added. Finds a triggerable in the aforementioned static map.
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype._updateStatus): Added. Re-render the page since time series data
+        points that were previously not available may have become available. The lack of this update was
+        causing a race condition in which the &quot;Start A/B Testing&quot; button for the charts is disabled even
+        after a group name had been specified because setRootSetMap was never called with a valid set.
+        (AnalysisTaskPage): Added this._triggerable.
+        (AnalysisTaskPage.prototype._didFetchTask): Find the triggerable now that we've fetched the task.
+        (AnalysisTaskPage.prototype.render): Hide the group view (the table of A/B testing results) entirely
+        when there are no groups to show. Also hide the forms to start A/B testing when there are no matching
+        triggerable, which is the main feature of this patch.
+        * server-tests/api-manifest.js: Added a test for including a list of triggerables in the manifest JSON.
+        * server-tests/resources/mock-data.js:
+        (MockData.resetV3Models): Reset Triggerable's static map.
+        * server-tests/tools-buildbot-triggerable-tests.js: Assert that Triggerable objects are  constructed
+        with appropriate list of repositories and (test, platform) associations.
+        * tools/js/database.js:
+        (tableToPrefixMap): Added triggerable_repositories's prefix.
+        * tools/js/remote.js:
+        (RemoteAPI.prototype.getJSON): Log the entire response to stderr when JSON.parse fails to aid debugging.
+        * tools/js/v3-models.js: Import triggerable.js.
+
</ins><span class="cx"> 2017-01-11  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         fetch-from-remote doesn’t work with some websites
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicadminreprocessreportphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/admin/reprocess-report.php (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/admin/reprocess-report.php        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/admin/reprocess-report.php        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -1,7 +1,7 @@
</span><span class="cx"> &lt;?php
</span><span class="cx"> 
</span><span class="cx"> require_once('../include/json-header.php');
</span><del>-require_once('../include/manifest.php');
</del><ins>+require_once('../include/manifest-generator.php');
</ins><span class="cx"> require_once('../include/report-processor.php');
</span><span class="cx"> 
</span><span class="cx"> $db = new Database;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapimanifestphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/manifest.php (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/manifest.php        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/api/manifest.php        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -1,7 +1,7 @@
</span><span class="cx"> &lt;?php
</span><span class="cx"> 
</span><span class="cx"> require_once('../include/json-header.php');
</span><del>-require_once('../include/manifest.php');
</del><ins>+require_once('../include/manifest-generator.php');
</ins><span class="cx"> 
</span><span class="cx"> function main() {
</span><span class="cx">     $db = new Database;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapireportphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/report.php (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/report.php        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/api/report.php        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -1,7 +1,7 @@
</span><span class="cx"> &lt;?php
</span><span class="cx"> 
</span><span class="cx"> require_once('../include/json-header.php');
</span><del>-require_once('../include/manifest.php');
</del><ins>+require_once('../include/manifest-generator.php');
</ins><span class="cx"> require_once('../include/report-processor.php');
</span><span class="cx"> 
</span><span class="cx"> function main($post_data) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludeadminheaderphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/admin-header.php (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/admin-header.php        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/include/admin-header.php        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -1,7 +1,7 @@
</span><span class="cx"> &lt;?php
</span><span class="cx"> 
</span><span class="cx"> require_once('db.php');
</span><del>-require_once('manifest.php');
</del><ins>+require_once('manifest-generator.php');
</ins><span class="cx"> 
</span><span class="cx"> ?&gt;&lt;!DOCTYPE html&gt;
</span><span class="cx"> &lt;html&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludemanifestgeneratorphpfromrev210625trunkWebsitesperfwebkitorgpublicincludemanifestphp"></a>
<div class="copfile"><h4>Copied: trunk/Websites/perf.webkit.org/public/include/manifest-generator.php (from rev 210625, trunk/Websites/perf.webkit.org/public/include/manifest.php) (0 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/manifest-generator.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/include/manifest-generator.php        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -0,0 +1,218 @@
</span><ins>+&lt;?php
+
+class ManifestGenerator {
+    private $db;
+    private $manifest;
+
+    // FIXME: Compute this value from config.json
+    const MANIFEST_PATH = '../data/manifest.json';
+
+    function __construct($db) {
+        $this-&gt;db = $db;
+    }
+
+    function generate() {
+        $start_time = microtime(true);
+
+        $platform_table = $this-&gt;db-&gt;fetch_table('platforms');
+        $repositories_table = $this-&gt;db-&gt;fetch_table('repositories');
+
+        $repositories_with_commit = $this-&gt;db-&gt;query_and_fetch_all(
+            'SELECT DISTINCT(commit_repository) FROM commits WHERE commit_reported IS TRUE');
+        if (!$repositories_with_commit)
+            $repositories_with_commit = array();
+
+        foreach ($repositories_with_commit as &amp;$row)
+            $row = $row['commit_repository'];
+
+        $tests = (object)$this-&gt;tests();
+        $metrics = (object)$this-&gt;metrics();
+        $platforms = (object)$this-&gt;platforms($platform_table, false);
+        $dashboard = (object)$this-&gt;platforms($platform_table, true);
+        $repositories = (object)$this-&gt;repositories($repositories_table, $repositories_with_commit);
+
+        $this-&gt;manifest = array(
+            'siteTitle' =&gt; config('siteTitle', 'Performance Dashboard'),
+            'tests' =&gt; &amp;$tests,
+            'metrics' =&gt; &amp;$metrics,
+            'all' =&gt; &amp;$platforms,
+            'dashboard' =&gt; &amp;$dashboard,
+            'repositories' =&gt; &amp;$repositories,
+            'builders' =&gt; (object)$this-&gt;builders(),
+            'bugTrackers' =&gt; (object)$this-&gt;bug_trackers($repositories_table),
+            'triggerables'=&gt; (object)$this-&gt;triggerables(),
+            'dashboards' =&gt; (object)config('dashboards'),
+            'summaryPages' =&gt; config('summaryPages'),
+        );
+
+        $this-&gt;manifest['elapsedTime'] = (microtime(true) - $start_time) * 1000;
+
+        return TRUE;
+    }
+
+    function manifest() { return $this-&gt;manifest; }
+
+    function store() {
+        return generate_data_file('manifest.json', json_encode($this-&gt;manifest));
+    }
+
+    private function tests() {
+        $tests = array();
+        $tests_table = $this-&gt;db-&gt;fetch_table('tests');
+        if (!$tests_table)
+            return $tests;
+        foreach ($tests_table as $test_row) {
+            $tests[$test_row['test_id']] = array(
+                'name' =&gt; $test_row['test_name'],
+                'url' =&gt; $test_row['test_url'],
+                'parentId' =&gt; $test_row['test_parent'],
+            );
+        }
+        return $tests;
+    }
+
+    private function metrics() {
+        $metrics = array();
+        $metrics_table = $this-&gt;db-&gt;query_and_fetch_all('SELECT * FROM test_metrics LEFT JOIN aggregators ON metric_aggregator = aggregator_id');
+        if (!$metrics_table)
+            return $metrics;
+        foreach ($metrics_table as $row) {
+            $metrics[$row['metric_id']] = array(
+                'name' =&gt; $row['metric_name'],
+                'test' =&gt; $row['metric_test'],
+                'aggregator' =&gt; $row['aggregator_name']);
+        }
+        return $metrics;
+    }
+
+    private function platforms($platform_table, $is_dashboard) {
+        $metrics = $this-&gt;db-&gt;query_and_fetch_all('SELECT config_metric AS metric_id, config_platform AS platform_id,
+            extract(epoch from max(config_runs_last_modified) at time zone \'utc\') * 1000 AS last_modified, bool_or(config_is_in_dashboard) AS in_dashboard
+            FROM test_configurations GROUP BY config_metric, config_platform ORDER BY config_platform');
+
+        $platform_metrics = array();
+
+        if ($metrics) {
+            $current_platform_entry = null;
+            foreach ($metrics as $metric_row) {
+                if ($is_dashboard &amp;&amp; !Database::is_true($metric_row['in_dashboard']))
+                    continue;
+
+                $platform_id = $metric_row['platform_id'];
+                if (!$current_platform_entry || $current_platform_entry['id'] != $platform_id) {
+                    $current_platform_entry = &amp;array_ensure_item_has_array($platform_metrics, $platform_id);
+                    $current_platform_entry['id'] = $platform_id;
+                    array_ensure_item_has_array($current_platform_entry, 'metrics');
+                    array_ensure_item_has_array($current_platform_entry, 'last_modified');
+                }
+
+                array_push($current_platform_entry['metrics'], $metric_row['metric_id']);
+                array_push($current_platform_entry['last_modified'], intval($metric_row['last_modified']));
+            }
+        }
+        $configurations = array();
+
+        $platforms = array();
+        if ($platform_table) {
+            foreach ($platform_table as $platform_row) {
+                if (Database::is_true($platform_row['platform_hidden']))
+                    continue;
+                $id = $platform_row['platform_id'];
+                if (array_key_exists($id, $platform_metrics)) {
+                    $platforms[$id] = array(
+                        'name' =&gt; $platform_row['platform_name'],
+                        'metrics' =&gt; $platform_metrics[$id]['metrics'],
+                        'lastModified' =&gt; $platform_metrics[$id]['last_modified']);
+                }
+            }
+        }
+        return $platforms;
+    }
+
+    private function repositories($repositories_table, $repositories_with_commit) {
+        $repositories = array();
+        if (!$repositories_table)
+            return $repositories;
+        foreach ($repositories_table as $row) {
+            $repositories[$row['repository_id']] = array(
+                'name' =&gt; $row['repository_name'],
+                'url' =&gt; $row['repository_url'],
+                'blameUrl' =&gt; $row['repository_blame_url'],
+                'hasReportedCommits' =&gt; in_array($row['repository_id'], $repositories_with_commit));
+        }
+
+        return $repositories;
+    }
+
+    private function builders() {
+        $builders_table = $this-&gt;db-&gt;fetch_table('builders');
+        if (!$builders_table)
+            return array();
+        $builders = array();
+        foreach ($builders_table as $row)
+            $builders[$row['builder_id']] = array('name' =&gt; $row['builder_name'], 'buildUrl' =&gt; $row['builder_build_url']);
+
+        return $builders;
+    }
+
+    private function bug_trackers($repositories_table) {
+        $tracker_id_to_repositories = array();
+        $tracker_repositories_table = $this-&gt;db-&gt;fetch_table('tracker_repositories');
+        if ($tracker_repositories_table) {
+            foreach ($tracker_repositories_table as $row) {
+                array_push(array_ensure_item_has_array($tracker_id_to_repositories, $row['tracrepo_tracker']),
+                    $row['tracrepo_repository']);
+            }
+        }
+
+        $bug_trackers = array();
+        $bug_trackers_table = $this-&gt;db-&gt;fetch_table('bug_trackers');
+        if ($bug_trackers_table) {
+            foreach ($bug_trackers_table as $row) {
+                $bug_trackers[$row['tracker_id']] = array(
+                    'name' =&gt; $row['tracker_name'],
+                    'bugUrl' =&gt; $row['tracker_bug_url'],
+                    'newBugUrl' =&gt; $row['tracker_new_bug_url'],
+                    'repositories' =&gt; array_get($tracker_id_to_repositories, $row['tracker_id']));
+            }
+        }
+
+        return $bug_trackers;
+    }
+
+    private function triggerables() {
+
+        $triggerables = $this-&gt;db-&gt;fetch_table('build_triggerables');
+        if (!$triggerables)
+            return array();
+
+        $id_to_triggerable = array();
+        foreach ($triggerables as $row) {
+            $id = $row['triggerable_id'];
+            $id_to_triggerable[$id] = array(
+                'name' =&gt; $row['triggerable_name'],
+                'acceptedRepositories' =&gt; array(),
+                'configurations' =&gt; array());
+        }
+
+        $repository_map = $this-&gt;db-&gt;fetch_table('triggerable_repositories');
+        if ($repository_map) {
+            foreach ($repository_map as $row) {
+                $triggerable = &amp;$id_to_triggerable[$row['trigrepo_triggerable']];
+                array_push($triggerable['acceptedRepositories'], $row['trigrepo_repository']);
+            }
+        }
+
+        $configuration_map = $this-&gt;db-&gt;fetch_table('triggerable_configurations');
+        if ($configuration_map) {
+            foreach ($configuration_map as $row) {
+                $triggerable = &amp;$id_to_triggerable[$row['trigconfig_triggerable']];
+                array_push($triggerable['configurations'], array($row['trigconfig_test'], $row['trigconfig_platform']));
+            }
+        }
+
+        return $id_to_triggerable;
+    }
+}
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludemanifestphp"></a>
<div class="delfile"><h4>Deleted: trunk/Websites/perf.webkit.org/public/include/manifest.php (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/manifest.php        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/include/manifest.php        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -1,183 +0,0 @@
</span><del>-&lt;?php
-
-class ManifestGenerator {
-    private $db;
-    private $manifest;
-
-    // FIXME: Compute this value from config.json
-    const MANIFEST_PATH = '../data/manifest.json';
-
-    function __construct($db) {
-        $this-&gt;db = $db;
-    }
-
-    function generate() {
-        $start_time = microtime(true);
-
-        $platform_table = $this-&gt;db-&gt;fetch_table('platforms');
-        $repositories_table = $this-&gt;db-&gt;fetch_table('repositories');
-
-        $repositories_with_commit = $this-&gt;db-&gt;query_and_fetch_all(
-            'SELECT DISTINCT(commit_repository) FROM commits WHERE commit_reported IS TRUE');
-        if (!$repositories_with_commit)
-            $repositories_with_commit = array();
-
-        foreach ($repositories_with_commit as &amp;$row)
-            $row = $row['commit_repository'];
-
-        $tests = (object)$this-&gt;tests();
-        $metrics = (object)$this-&gt;metrics();
-        $platforms = (object)$this-&gt;platforms($platform_table, false);
-        $dashboard = (object)$this-&gt;platforms($platform_table, true);
-        $repositories = (object)$this-&gt;repositories($repositories_table, $repositories_with_commit);
-
-        $this-&gt;manifest = array(
-            'siteTitle' =&gt; config('siteTitle', 'Performance Dashboard'),
-            'tests' =&gt; &amp;$tests,
-            'metrics' =&gt; &amp;$metrics,
-            'all' =&gt; &amp;$platforms,
-            'dashboard' =&gt; &amp;$dashboard,
-            'repositories' =&gt; &amp;$repositories,
-            'builders' =&gt; (object)$this-&gt;builders(),
-            'bugTrackers' =&gt; (object)$this-&gt;bug_trackers($repositories_table),
-            'dashboards' =&gt; (object)config('dashboards'),
-            'summaryPages' =&gt; config('summaryPages'),
-        );
-
-        $this-&gt;manifest['elapsedTime'] = (microtime(true) - $start_time) * 1000;
-
-        return TRUE;
-    }
-
-    function manifest() { return $this-&gt;manifest; }
-
-    function store() {
-        return generate_data_file('manifest.json', json_encode($this-&gt;manifest));
-    }
-
-    private function tests() {
-        $tests = array();
-        $tests_table = $this-&gt;db-&gt;fetch_table('tests');
-        if (!$tests_table)
-            return $tests;
-        foreach ($tests_table as $test_row) {
-            $tests[$test_row['test_id']] = array(
-                'name' =&gt; $test_row['test_name'],
-                'url' =&gt; $test_row['test_url'],
-                'parentId' =&gt; $test_row['test_parent'],
-            );
-        }
-        return $tests;
-    }
-
-    private function metrics() {
-        $metrics = array();
-        $metrics_table = $this-&gt;db-&gt;query_and_fetch_all('SELECT * FROM test_metrics LEFT JOIN aggregators ON metric_aggregator = aggregator_id');
-        if (!$metrics_table)
-            return $metrics;
-        foreach ($metrics_table as $row) {
-            $metrics[$row['metric_id']] = array(
-                'name' =&gt; $row['metric_name'],
-                'test' =&gt; $row['metric_test'],
-                'aggregator' =&gt; $row['aggregator_name']);
-        }
-        return $metrics;
-    }
-
-    private function platforms($platform_table, $is_dashboard) {
-        $metrics = $this-&gt;db-&gt;query_and_fetch_all('SELECT config_metric AS metric_id, config_platform AS platform_id,
-            extract(epoch from max(config_runs_last_modified)) * 1000 AS last_modified, bool_or(config_is_in_dashboard) AS in_dashboard
-            FROM test_configurations GROUP BY config_metric, config_platform ORDER BY config_platform');
-
-        $platform_metrics = array();
-
-        if ($metrics) {
-            $current_platform_entry = null;
-            foreach ($metrics as $metric_row) {
-                if ($is_dashboard &amp;&amp; !Database::is_true($metric_row['in_dashboard']))
-                    continue;
-
-                $platform_id = $metric_row['platform_id'];
-                if (!$current_platform_entry || $current_platform_entry['id'] != $platform_id) {
-                    $current_platform_entry = &amp;array_ensure_item_has_array($platform_metrics, $platform_id);
-                    $current_platform_entry['id'] = $platform_id;
-                    array_ensure_item_has_array($current_platform_entry, 'metrics');
-                    array_ensure_item_has_array($current_platform_entry, 'last_modified');
-                }
-
-                array_push($current_platform_entry['metrics'], $metric_row['metric_id']);
-                array_push($current_platform_entry['last_modified'], intval($metric_row['last_modified']));
-            }
-        }
-        $configurations = array();
-
-        $platforms = array();
-        if ($platform_table) {
-            foreach ($platform_table as $platform_row) {
-                if (Database::is_true($platform_row['platform_hidden']))
-                    continue;
-                $id = $platform_row['platform_id'];
-                if (array_key_exists($id, $platform_metrics)) {
-                    $platforms[$id] = array(
-                        'name' =&gt; $platform_row['platform_name'],
-                        'metrics' =&gt; $platform_metrics[$id]['metrics'],
-                        'lastModified' =&gt; $platform_metrics[$id]['last_modified']);
-                }
-            }
-        }
-        return $platforms;
-    }
-
-    private function repositories($repositories_table, $repositories_with_commit) {
-        $repositories = array();
-        if (!$repositories_table)
-            return $repositories;
-        foreach ($repositories_table as $row) {
-            $repositories[$row['repository_id']] = array(
-                'name' =&gt; $row['repository_name'],
-                'url' =&gt; $row['repository_url'],
-                'blameUrl' =&gt; $row['repository_blame_url'],
-                'hasReportedCommits' =&gt; in_array($row['repository_id'], $repositories_with_commit));
-        }
-
-        return $repositories;
-    }
-
-    private function builders() {
-        $builders_table = $this-&gt;db-&gt;fetch_table('builders');
-        if (!$builders_table)
-            return array();
-        $builders = array();
-        foreach ($builders_table as $row)
-            $builders[$row['builder_id']] = array('name' =&gt; $row['builder_name'], 'buildUrl' =&gt; $row['builder_build_url']);
-
-        return $builders;
-    }
-
-    private function bug_trackers($repositories_table) {
-        $tracker_id_to_repositories = array();
-        $tracker_repositories_table = $this-&gt;db-&gt;fetch_table('tracker_repositories');
-        if ($tracker_repositories_table) {
-            foreach ($tracker_repositories_table as $row) {
-                array_push(array_ensure_item_has_array($tracker_id_to_repositories, $row['tracrepo_tracker']),
-                    $row['tracrepo_repository']);
-            }
-        }
-
-        $bug_trackers = array();
-        $bug_trackers_table = $this-&gt;db-&gt;fetch_table('bug_trackers');
-        if ($bug_trackers_table) {
-            foreach ($bug_trackers_table as $row) {
-                $bug_trackers[$row['tracker_id']] = array(
-                    'name' =&gt; $row['tracker_name'],
-                    'bugUrl' =&gt; $row['tracker_bug_url'],
-                    'newBugUrl' =&gt; $row['tracker_new_bug_url'],
-                    'repositories' =&gt; array_get($tracker_id_to_repositories, $row['tracker_id']));
-            }
-        }
-
-        return $bug_trackers;
-    }
-}
-
-?&gt;
</del></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentscustomizabletestgroupformjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -5,9 +5,10 @@
</span><span class="cx">     {
</span><span class="cx">         super('customizable-test-group-form');
</span><span class="cx">         this._rootSetMap = null;
</span><del>-        this._disabled = true;
</del><span class="cx">         this._renderedRepositorylist = null;
</span><span class="cx">         this._customized = false;
</span><ins>+        this._nameControl = this.content().querySelector('.name');
+        this._nameControl.oninput = this.render.bind(this);
</ins><span class="cx">         this.content().querySelector('a').onclick = this._customize.bind(this);
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -15,13 +16,12 @@
</span><span class="cx">     {
</span><span class="cx">         this._rootSetMap = map;
</span><span class="cx">         this._customized = false;
</span><del>-        this.setDisabled(!map);
</del><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _submitted()
</span><span class="cx">     {
</span><span class="cx">         if (this._startCallback)
</span><del>-            this._startCallback(this.content().querySelector('.name').value, this._repetitionCount, this._computeRootSetMap());
</del><ins>+            this._startCallback(this._nameControl.value, this._repetitionCount, this._computeRootSetMap());
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     _customize(event)
</span><span class="lines">@@ -56,13 +56,15 @@
</span><span class="cx">     render()
</span><span class="cx">     {
</span><span class="cx">         super.render();
</span><del>-        this.content().querySelector('.customize-link').style.display = this._disabled ? 'none' : null;
</del><ins>+        var map = this._rootSetMap;
</ins><span class="cx"> 
</span><ins>+        this.content().querySelector('button').disabled = !(map &amp;&amp; this._nameControl.value);
+        this.content().querySelector('.customize-link').style.display = !map ? 'none' : null;
+
</ins><span class="cx">         if (!this._customized) {
</span><span class="cx">             this.renderReplace(this.content().querySelector('.custom-table-container'), []);
</span><span class="cx">             return;
</span><span class="cx">         }
</span><del>-        var map = this._rootSetMap;
</del><span class="cx">         console.assert(map);
</span><span class="cx"> 
</span><span class="cx">         var repositorySet = new Set;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3componentstestgroupformjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/v3/components/test-group-form.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -5,7 +5,6 @@
</span><span class="cx">     {
</span><span class="cx">         super(name || 'test-group-form');
</span><span class="cx">         this._startCallback = null;
</span><del>-        this._disabled = false;
</del><span class="cx">         this._label = undefined;
</span><span class="cx">         this._repetitionCount = 4;
</span><span class="cx"> 
</span><span class="lines">@@ -24,7 +23,6 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     setStartCallback(callback) { this._startCallback = callback; }
</span><del>-    setDisabled(disabled) { this._disabled = !!disabled; }
</del><span class="cx">     setLabel(label) { this._label = label; }
</span><span class="cx">     setRepetitionCount(count) { this._repetitionCount = count; }
</span><span class="cx"> 
</span><span class="lines">@@ -33,7 +31,6 @@
</span><span class="cx">         var button = this.content().querySelector('button');
</span><span class="cx">         if (this._label)
</span><span class="cx">             button.textContent = this._label;
</span><del>-        button.disabled = this._disabled;
</del><span class="cx">         this._repetitionCountControl.value = this._repetitionCount;
</span><span class="cx">     }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3indexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/index.html        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -63,6 +63,7 @@
</span><span class="cx">         &lt;script src=&quot;models/test-group.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;models/build-request.js&quot;&gt;&lt;/script&gt;
</span><span class="cx">         &lt;script src=&quot;models/root-set.js&quot;&gt;&lt;/script&gt;
</span><ins>+        &lt;script src=&quot;models/triggerable.js&quot;&gt;&lt;/script&gt;
</ins><span class="cx">         &lt;script src=&quot;models/manifest.js&quot;&gt;&lt;/script&gt;
</span><span class="cx"> 
</span><span class="cx">         &lt;script src=&quot;components/base.js&quot;&gt;&lt;/script&gt;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelsmanifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/models/manifest.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/manifest.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/v3/models/manifest.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -13,35 +13,41 @@
</span><span class="cx">     {
</span><span class="cx">         Instrumentation.startMeasuringTime('Manifest', '_didFetchManifest');
</span><span class="cx"> 
</span><del>-        var tests = [];
-        var testParentMap = {};
-        for (var testId in rawResponse.tests)
</del><ins>+        const tests = [];
+        const testParentMap = {};
+        for (let testId in rawResponse.tests)
</ins><span class="cx">             tests.push(new Test(testId, rawResponse.tests[testId]));
</span><span class="cx"> 
</span><span class="cx">         function buildObjectsFromIdMap(idMap, constructor, resolver) {
</span><del>-            for (var id in idMap) {
</del><ins>+            for (let id in idMap) {
</ins><span class="cx">                 if (resolver)
</span><span class="cx">                     resolver(idMap[id]);
</span><span class="cx">                 new constructor(id, idMap[id]);
</span><span class="cx">             }
</span><span class="cx">         }
</span><del>-        buildObjectsFromIdMap(rawResponse.metrics, Metric, function (raw) {
</del><ins>+
+        buildObjectsFromIdMap(rawResponse.metrics, Metric, (raw) =&gt; {
</ins><span class="cx">             raw.test = Test.findById(raw.test);
</span><span class="cx">         });
</span><span class="cx"> 
</span><del>-        buildObjectsFromIdMap(rawResponse.all, Platform, function (raw) {
</del><ins>+        buildObjectsFromIdMap(rawResponse.all, Platform, (raw) =&gt; {
</ins><span class="cx">             raw.lastModifiedByMetric = {};
</span><del>-            raw.lastModified.forEach(function (lastModified, index) {
</del><ins>+            raw.lastModified.forEach((lastModified, index) =&gt; {
</ins><span class="cx">                 raw.lastModifiedByMetric[raw.metrics[index]] = lastModified;
</span><span class="cx">             });
</span><del>-            raw.metrics = raw.metrics.map(function (id) { return Metric.findById(id); });
</del><ins>+            raw.metrics = raw.metrics.map((id) =&gt; { return Metric.findById(id); });
</ins><span class="cx">         });
</span><span class="cx">         buildObjectsFromIdMap(rawResponse.builders, Builder);
</span><span class="cx">         buildObjectsFromIdMap(rawResponse.repositories, Repository);
</span><del>-        buildObjectsFromIdMap(rawResponse.bugTrackers, BugTracker, function (raw) {
</del><ins>+        buildObjectsFromIdMap(rawResponse.bugTrackers, BugTracker, (raw) =&gt; {
</ins><span class="cx">             if (raw.repositories)
</span><del>-                raw.repositories = raw.repositories.map(function (id) { return Repository.findById(id); });
</del><ins>+                raw.repositories = raw.repositories.map((id) =&gt; { return Repository.findById(id); });
</ins><span class="cx">         });
</span><ins>+        buildObjectsFromIdMap(rawResponse.triggerables, Triggerable, (raw) =&gt; {
+            raw.acceptedRepositories = raw.acceptedRepositories.map((repositoryId) =&gt; {
+                return Repository.findById(repositoryId);
+            });
+        });
</ins><span class="cx"> 
</span><span class="cx">         Instrumentation.endMeasuringTime('Manifest', '_didFetchManifest');
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3modelstriggerablejs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js (0 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/models/triggerable.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -0,0 +1,37 @@
</span><ins>+class Triggerable extends LabeledObject {
+
+    constructor(id, object)
+    {
+        super(id, object);
+        this._name = object.name;
+        this._acceptedRepositories = object.acceptedRepositories;
+        this._configurationList = object.configurations;
+
+        let configurationMap = this.ensureNamedStaticMap('testConfigurations');
+        for (const config of object.configurations) {
+            const [testId, platformId] = config;
+            const key = `${testId}-${platformId}`;
+            console.assert(!(key in configurationMap));
+            configurationMap[key] = this;
+        }
+    }
+
+    acceptedRepositories() { return this._acceptedRepositories; }
+
+    static findByTestConfiguration(test, platform)
+    {
+        let configurationMap = this.ensureNamedStaticMap('testConfigurations');
+        if (!configurationMap)
+            return null;
+        for (; test; test = test.parentTest()) {
+            const key = `${test.id()}-${platform.id()}`;
+            if (key in configurationMap)
+                return configurationMap[key];
+        }
+        return null;
+    }
+
+}
+
+if (typeof module != 'undefined')
+    module.exports.Triggerable = Triggerable;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv3pagesanalysistaskpagejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -16,6 +16,12 @@
</span><span class="cx">             this._page._chartSelectionDidChange();
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    _updateStatus()
+    {
+        super._updateStatus();
+        this._page.render();
+    }
+
</ins><span class="cx">     selectedPoints()
</span><span class="cx">     {
</span><span class="cx">         var selection = this._mainChart ? this._mainChart.currentSelection() : null;
</span><span class="lines">@@ -33,6 +39,7 @@
</span><span class="cx">     {
</span><span class="cx">         super('Analysis Task');
</span><span class="cx">         this._task = null;
</span><ins>+        this._triggerable = null;
</ins><span class="cx">         this._relatedTasks = null;
</span><span class="cx">         this._testGroups = null;
</span><span class="cx">         this._renderedTestGroups = null;
</span><span class="lines">@@ -120,20 +127,22 @@
</span><span class="cx">         console.assert(!this._task);
</span><span class="cx"> 
</span><span class="cx">         this._task = task;
</span><del>-        var platform = task.platform();
-        var metric = task.metric();
-        var lastModified = platform.lastModified(metric);
</del><ins>+        const platform = task.platform();
+        const metric = task.metric();
+        const lastModified = platform.lastModified(metric);
</ins><span class="cx"> 
</span><ins>+        this._triggerable = Triggerable.findByTestConfiguration(metric.test(), platform);
+
</ins><span class="cx">         this._measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), lastModified);
</span><span class="cx">         this._measurementSet.fetchBetween(task.startTime(), task.endTime(), this._didFetchMeasurement.bind(this));
</span><span class="cx"> 
</span><del>-        var formatter = metric.makeFormatter(4);
</del><ins>+        const formatter = metric.makeFormatter(4);
</ins><span class="cx">         this._analysisResultsViewer.setValueFormatter(formatter);
</span><span class="cx">         this._testGroupResultsTable.setValueFormatter(formatter);
</span><span class="cx"> 
</span><span class="cx">         this._chartPane.configure(platform.id(), metric.id());
</span><span class="cx"> 
</span><del>-        var domain = ChartsPage.createDomainForAnalysisTask(task);
</del><ins>+        const domain = ChartsPage.createDomainForAnalysisTask(task);
</ins><span class="cx">         this._chartPane.setOverviewDomain(domain[0], domain[1]);
</span><span class="cx">         this._chartPane.setMainDomain(domain[0], domain[1]);
</span><span class="cx"> 
</span><span class="lines">@@ -265,7 +274,7 @@
</span><span class="cx"> 
</span><span class="cx">         this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
</span><span class="cx">         this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
</span><del>-        this.content().querySelector('.test-group-view').style.display = this._task ? null : 'none';
</del><ins>+        this.content().querySelector('.test-group-view').style.display = this._task &amp;&amp; this._testGroups &amp;&amp; this._testGroups.length ? null : 'none';
</ins><span class="cx">         this._taskNameLabel.render();
</span><span class="cx"> 
</span><span class="cx">         if (this._relatedTasks &amp;&amp; this._task) {
</span><span class="lines">@@ -288,6 +297,7 @@
</span><span class="cx">         var b = selectedRange['B'];
</span><span class="cx">         this._newTestGroupFormForViewer.setRootSetMap(a &amp;&amp; b ? {'A': a.rootSet(), 'B': b.rootSet()} : null);
</span><span class="cx">         this._newTestGroupFormForViewer.render();
</span><ins>+        this._newTestGroupFormForViewer.element().style.display = this._triggerable ? null : 'none';
</ins><span class="cx"> 
</span><span class="cx">         this._renderTestGroupList();
</span><span class="cx">         this._renderTestGroupDetails();
</span><span class="lines">@@ -299,6 +309,7 @@
</span><span class="cx">         this._newTestGroupFormForChart.setRootSetMap(points &amp;&amp; points.length &gt;= 2 ?
</span><span class="cx">                 {'A': points[0].rootSet(), 'B': points[points.length - 1].rootSet()} : null);
</span><span class="cx">         this._newTestGroupFormForChart.render();
</span><ins>+        this._newTestGroupFormForChart.element().style.display = this._triggerable ? null : 'none';
</ins><span class="cx"> 
</span><span class="cx">         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
</span><span class="cx">         this._analysisResultsViewer.render();
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsapimanifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/api-manifest.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/api-manifest.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/server-tests/api-manifest.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -18,7 +18,7 @@
</span><span class="cx">     it(&quot;should generate an empty manifest when database is empty&quot;, function (done) {
</span><span class="cx">         TestServer.remoteAPI().getJSON('/api/manifest').then(function (manifest) {
</span><span class="cx">             assert.deepEqual(Object.keys(manifest).sort(), ['all', 'bugTrackers', 'builders', 'dashboard', 'dashboards',
</span><del>-                'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'summaryPages', 'tests']);
</del><ins>+                'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'summaryPages', 'tests', 'triggerables']);
</ins><span class="cx"> 
</span><span class="cx">             assert.equal(typeof(manifest.elapsedTime), 'number');
</span><span class="cx">             delete manifest.elapsedTime;
</span><span class="lines">@@ -33,6 +33,7 @@
</span><span class="cx">                 metrics: {},
</span><span class="cx">                 repositories: {},
</span><span class="cx">                 tests: {},
</span><ins>+                triggerables: {},
</ins><span class="cx">                 summaryPages: [],
</span><span class="cx">                 status: 'OK'
</span><span class="cx">             });
</span><span class="lines">@@ -275,4 +276,78 @@
</span><span class="cx">         }).catch(done);
</span><span class="cx">     });
</span><span class="cx"> 
</span><ins>+    it(&quot;should generate manifest with triggerables&quot;, function (done) {
+        let db = TestServer.database();
+        db.connect();
+        Promise.all([
+            db.insert('repositories', {id: 11, name: 'WebKit', url: 'https://trac.webkit.org/$1'}),
+            db.insert('repositories', {id: 9, name: 'OS X'}),
+            db.insert('build_triggerables', {id: 200, name: 'build.webkit.org'}),
+            db.insert('build_triggerables', {id: 201, name: 'ios-build.webkit.org'}),
+            db.insert('tests', {id: 1, name: 'SomeTest'}),
+            db.insert('tests', {id: 2, name: 'SomeOtherTest'}),
+            db.insert('tests', {id: 3, name: 'ChildTest', parent: 1}),
+            db.insert('platforms', {id: 23, name: 'iOS 9 iPhone 5s'}),
+            db.insert('platforms', {id: 46, name: 'Trunk Mavericks'}),
+            db.insert('test_metrics', {id: 5, test: 1, name: 'Time'}),
+            db.insert('test_metrics', {id: 8, test: 2, name: 'FrameRate'}),
+            db.insert('test_metrics', {id: 9, test: 3, name: 'Time'}),
+            db.insert('test_configurations', {id: 101, metric: 5, platform: 46, type: 'current'}),
+            db.insert('test_configurations', {id: 102, metric: 8, platform: 46, type: 'current'}),
+            db.insert('test_configurations', {id: 103, metric: 9, platform: 46, type: 'current'}),
+            db.insert('test_configurations', {id: 104, metric: 5, platform: 23, type: 'current'}),
+            db.insert('test_configurations', {id: 105, metric: 8, platform: 23, type: 'current'}),
+            db.insert('test_configurations', {id: 106, metric: 9, platform: 23, type: 'current'}),
+            db.insert('triggerable_repositories', {triggerable: 200, repository: 11}),
+            db.insert('triggerable_repositories', {triggerable: 201, repository: 11}),
+            db.insert('triggerable_configurations', {triggerable: 200, test: 1, platform: 46}),
+            db.insert('triggerable_configurations', {triggerable: 200, test: 2, platform: 46}),
+            db.insert('triggerable_configurations', {triggerable: 201, test: 1, platform: 23}),
+            db.insert('triggerable_configurations', {triggerable: 201, test: 2, platform: 23}),
+        ]).then(function () {
+            return Manifest.fetch();
+        }).then(function () {
+            let webkit = Repository.findById(11);
+            assert.equal(webkit.name(), 'WebKit');
+            assert.equal(webkit.urlForRevision(123), 'https://trac.webkit.org/123');
+
+            let osx = Repository.findById(9);
+            assert.equal(osx.name(), 'OS X');
+
+            let someTest = Test.findById(1);
+            assert.equal(someTest.name(), 'SomeTest');
+
+            let someOtherTest = Test.findById(2);
+            assert.equal(someOtherTest.name(), 'SomeOtherTest');
+
+            let childTest = Test.findById(3);
+            assert.equal(childTest.name(), 'ChildTest');
+
+            let ios9iphone5s = Platform.findById(23);
+            assert.equal(ios9iphone5s.name(), 'iOS 9 iPhone 5s');
+
+            let mavericks = Platform.findById(46);
+            assert.equal(mavericks.name(), 'Trunk Mavericks');
+
+            assert.equal(Triggerable.all().length, 2);
+
+            let osxTriggerable = Triggerable.findByTestConfiguration(someTest, mavericks);
+            assert.equal(osxTriggerable.name(), 'build.webkit.org');
+            assert.deepEqual(osxTriggerable.acceptedRepositories(), [webkit]);
+
+            assert.equal(Triggerable.findByTestConfiguration(someOtherTest, mavericks), osxTriggerable);
+            assert.equal(Triggerable.findByTestConfiguration(childTest, mavericks), osxTriggerable);
+
+            let iosTriggerable = Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s);
+            assert.notEqual(iosTriggerable, osxTriggerable);
+            assert.equal(iosTriggerable.name(), 'ios-build.webkit.org');
+            assert.deepEqual(iosTriggerable.acceptedRepositories(), [webkit]);
+
+            assert.equal(Triggerable.findByTestConfiguration(someOtherTest, ios9iphone5s), iosTriggerable);
+            assert.equal(Triggerable.findByTestConfiguration(childTest, ios9iphone5s), iosTriggerable);
+
+            done();
+        }).catch(done);
+    });
+
</ins><span class="cx"> });
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgservertestsresourcesmockdatajs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/server-tests/resources/mock-data.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -15,6 +15,7 @@
</span><span class="cx">         RootSet.clearStaticMap();
</span><span class="cx">         Test.clearStaticMap();
</span><span class="cx">         TestGroup.clearStaticMap();
</span><ins>+        Triggerable.clearStaticMap();
</ins><span class="cx">     },
</span><span class="cx">     someTestId() { return 200; },
</span><span class="cx">     somePlatformId() { return 65; },
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgserverteststoolsbuildbottriggerabletestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/server-tests/tools-buildbot-triggerable-tests.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -910,23 +910,46 @@
</span><span class="cx"> 
</span><span class="cx">         it('should update available triggerables', function (done) {
</span><span class="cx">             let db = TestServer.database();
</span><del>-            MockData.addMockData(db).then(function () {
</del><ins>+            MockData.addMockData(db).then(() =&gt; {
</ins><span class="cx">                 return Manifest.fetch();
</span><del>-            }).then(function () {
</del><ins>+            }).then(() =&gt; {
</ins><span class="cx">                 return db.selectAll('triggerable_configurations', 'test');
</span><del>-            }).then(function (configurations) {
</del><ins>+            }).then((configurations) =&gt; {
</ins><span class="cx">                 assert.equal(configurations.length, 0);
</span><ins>+                assert.equal(Triggerable.all().length, 1);
+
+                let triggerable = Triggerable.all()[0];
+                assert.equal(triggerable.name(), 'build-webkit');
+                assert.deepEqual(triggerable.acceptedRepositories(), []);
+
+                let test = Test.findById(MockData.someTestId());
+                let platform = Platform.findById(MockData.somePlatformId());
+                assert.equal(Triggerable.findByTestConfiguration(test, platform), null);
+
</ins><span class="cx">                 let config = MockData.mockTestSyncConfigWithSingleBuilder();
</span><span class="cx">                 let logger = new MockLogger;
</span><span class="cx">                 let slaveInfo = {name: 'sync-slave', password: 'password'};
</span><del>-                let triggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
-                return triggerable.updateTriggerable();
-            }).then(function () {
</del><ins>+                let buildbotTriggerable = new BuildbotTriggerable(config, TestServer.remoteAPI(), MockRemoteAPI, slaveInfo, logger);
+                return buildbotTriggerable.updateTriggerable();
+            }).then(() =&gt; {
+                MockData.resetV3Models();
+                assert.equal(Triggerable.all().length, 0);
+                return TestServer.remoteAPI().getJSON('/api/manifest');
+            }).then((manifestContent) =&gt; {
+                Manifest._didFetchManifest(manifestContent);
</ins><span class="cx">                 return db.selectAll('triggerable_configurations', 'test');
</span><del>-            }).then(function (configurations) {
</del><ins>+            }).then((configurations) =&gt; {
</ins><span class="cx">                 assert.equal(configurations.length, 1);
</span><span class="cx">                 assert.equal(configurations[0].test, MockData.someTestId());
</span><span class="cx">                 assert.equal(configurations[0].platform, MockData.somePlatformId());
</span><ins>+
+                assert.equal(Triggerable.all().length, 1);
+
+                let test = Test.findById(MockData.someTestId());
+                let platform = Platform.findById(MockData.somePlatformId());
+                let triggerable = Triggerable.findByTestConfiguration(test, platform);
+                assert.equal(triggerable.name(), 'build-webkit');
+
</ins><span class="cx">                 done();
</span><span class="cx">             }).catch(done);
</span><span class="cx">         });
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsdatabasejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/database.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/database.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/tools/js/database.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -143,6 +143,7 @@
</span><span class="cx">     'tests': 'test',
</span><span class="cx">     'tracker_repositories': 'tracrepo',
</span><span class="cx">     'triggerable_configurations': 'trigconfig',
</span><ins>+    'triggerable_repositories': 'trigrepo',
</ins><span class="cx">     'platforms': 'platform',
</span><span class="cx">     'reports': 'report',
</span><span class="cx">     'repositories': 'repository',
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsremotejs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/remote.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/remote.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/tools/js/remote.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -56,7 +56,12 @@
</span><span class="cx">     getJSON(path)
</span><span class="cx">     {
</span><span class="cx">         return this.sendHttpRequest(path, 'GET', null, null).then(function (result) {
</span><del>-            return JSON.parse(result.responseText);
</del><ins>+            try {
+                return JSON.parse(result.responseText);
+            } catch (error) {
+                console.error(result.responseText);
+                throw error;
+            }
</ins><span class="cx">         });
</span><span class="cx">     }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtoolsjsv3modelsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tools/js/v3-models.js (210625 => 210626)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2017-01-12 06:50:16 UTC (rev 210625)
+++ trunk/Websites/perf.webkit.org/tools/js/v3-models.js        2017-01-12 07:18:28 UTC (rev 210626)
</span><span class="lines">@@ -27,6 +27,7 @@
</span><span class="cx"> importFromV3('models/test.js', 'Test');
</span><span class="cx"> importFromV3('models/test-group.js', 'TestGroup');
</span><span class="cx"> importFromV3('models/time-series.js', 'TimeSeries');
</span><ins>+importFromV3('models/triggerable.js', 'Triggerable');
</ins><span class="cx"> 
</span><span class="cx"> importFromV3('privileged-api.js', 'PrivilegedAPI');
</span><span class="cx"> importFromV3('instrumentation.js', 'Instrumentation');
</span></span></pre>
</div>
</div>

</body>
</html>