<!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>[180468] 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/180468">180468</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2015-02-20 17:28:34 -0800 (Fri, 20 Feb 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Loading the perf dashboard takes multiple seconds
https://bugs.webkit.org/show_bug.cgi?id=141860

Reviewed by Andreas Kling.

This patch introduces the caches of JSON files returned by /api/ in /data/ directory. It also records
the last time test_runs rows associated with the requested platforms and metrics are inserted, updated,
or removed in the caches as well as the manifest JSON files (&quot;last modified time&quot;). Because the manifest
is regenerated each time a new test result is reported, the front end can compare last modified time in
the manifest file with that in a /api/runs JSON cache to detect the stale-ness.

More concretely, the front end first optimistically fetches the JSON in /data/. If the cache doesn't exit
or the last modified time in the cache doesn't match with that in the manifest file, it would fetch it
again via /api/runs. In the case the cache did exist, we render the charts based on the cache meanwhile.
This dramatically reduces the perceived latency for the page load since charts are drawn immediately using
the cache and we would only re-render the charts as new up-to-date JSON comes in.

This patch also changes the format of runs JSONs by pushing the exiting properties into 'configurations'
and adding 'lastModified' and 'elapsedTime' at the top level.

* init-database.sql: Added config_runs_last_modified to test_configurations table as well as a trigger to
auto-update this column upon changes to test_runs table.

* public/admin/test-configurations.php:
(add_run): Regenerate the manifest file to invalidate the /api/runs JSON cache.
(delete_run): Ditto.

* public/api/runs.php:
(main): Fetch all columns of test_configurations table including config_runs_last_modified. Also generate
the cache in /data/ directory.
(RunsGenerator::__construct): Compute the last modified time for this (platform, metric) pair.
(RunsGenerator::results): Put the old content in 'configurations' property and include 'lastModified' and
'elapsedTime' properties. 'elapsedTime' is added for debugging purposes.
(RunsGenerator::add_runs):
(RunsGenerator::parse_revisions_array):

* public/include/db.php:
(CONFIG_DIR): Added.
(generate_data_file): Added based on ManifestGenerator::store.
(Database::to_js_time): Extracted from RunsGenerator::add_runs to share code.

* public/include/json-header.php:
(echo_success): Renamed from success_json. Return the serialized JSON instead of echo'ing it so that we can
generate caches in /api/runs/.
(exit_with_success):

* public/include/manifest.php:
(ManifestGenerator::generate): Added 'elapsedTime' property for the time taken to generate the manifest.
It seems like we're generating it in 200-300ms for now so that's good.
(ManifestGenerator::store): Uses generate_data_file.
(ManifestGenerator::platforms): Added 'lastModified' array to each platform entry. This array contains the
last modified time for each (platform, metric) pair.

* public/index.html:
(fetchTest): Updated per the format change in runs JSON.

* public/v2/app.js:
(App.Pane._fetch): Fetch the cached JSON first. Refetch the uncached version if instructed as such.
(App.Pane._updateChartData): Extracted from App.Pane._fetch.
(App.Pane._handleFetchErrors): Ditto.

* public/v2/data.js:
(RunsData.fetchRuns): Takes the fourth argument indicating whether we should fetch the cached version or not.
The cached JSON is located in /data/ with the same filename. When fetching a cached JSON results in 404,
fulfill the promise with null as the result instead of rejecting it. The only client of this function which
sets useCache to true is App.Manifest.fetchRunsWithPlatformAndMetric, and it handles this special case.

* public/v2/manifest.js:
(App.DateArrayTransform): Added. Handles the array of last modified dates in platform objects.
(App.Platform.lastModifiedTimeForMetric): Added. Returns the last modified date in the manifest JSON.
(App.Manifest.fetchRunsWithPlatformAndMetric): Takes &quot;useCache&quot; like RunsData.fetchRuns. Set shouldRefetch
to true if response is null (the cache didn't exit) or the cache is out-of-date.
(App.Manifest._formatFetchedData): Extracted from App.Manifest.fetchRunsWithPlatformAndMetric.

* run-tests.js:
(initializeDatabase): Avoid splitting function definitions in the middle.

* tests/api-report.js: Added tests to verify that reporting new test results updates the last modified time
in test_configurations.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorginitdatabasesql">trunk/Websites/perf.webkit.org/init-database.sql</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicadmintestconfigurationsphp">trunk/Websites/perf.webkit.org/public/admin/test-configurations.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicapirunsphp">trunk/Websites/perf.webkit.org/public/api/runs.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludedbphp">trunk/Websites/perf.webkit.org/public/include/db.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludejsonheaderphp">trunk/Websites/perf.webkit.org/public/include/json-header.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicincludemanifestphp">trunk/Websites/perf.webkit.org/public/include/manifest.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicindexhtml">trunk/Websites/perf.webkit.org/public/index.html</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2appjs">trunk/Websites/perf.webkit.org/public/v2/app.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2datajs">trunk/Websites/perf.webkit.org/public/v2/data.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgpublicv2manifestjs">trunk/Websites/perf.webkit.org/public/v2/manifest.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgruntestsjs">trunk/Websites/perf.webkit.org/run-tests.js</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtestsapireportjs">trunk/Websites/perf.webkit.org/tests/api-report.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebsitesperfwebkitorgChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/ChangeLog (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -1,5 +1,87 @@
</span><span class="cx"> 2015-02-20  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><ins>+        Loading the perf dashboard takes multiple seconds
+        https://bugs.webkit.org/show_bug.cgi?id=141860
+
+        Reviewed by Andreas Kling.
+
+        This patch introduces the caches of JSON files returned by /api/ in /data/ directory. It also records
+        the last time test_runs rows associated with the requested platforms and metrics are inserted, updated,
+        or removed in the caches as well as the manifest JSON files (&quot;last modified time&quot;). Because the manifest
+        is regenerated each time a new test result is reported, the front end can compare last modified time in
+        the manifest file with that in a /api/runs JSON cache to detect the stale-ness.
+
+        More concretely, the front end first optimistically fetches the JSON in /data/. If the cache doesn't exit
+        or the last modified time in the cache doesn't match with that in the manifest file, it would fetch it
+        again via /api/runs. In the case the cache did exist, we render the charts based on the cache meanwhile.
+        This dramatically reduces the perceived latency for the page load since charts are drawn immediately using
+        the cache and we would only re-render the charts as new up-to-date JSON comes in.
+
+        This patch also changes the format of runs JSONs by pushing the exiting properties into 'configurations'
+        and adding 'lastModified' and 'elapsedTime' at the top level.
+
+        * init-database.sql: Added config_runs_last_modified to test_configurations table as well as a trigger to
+        auto-update this column upon changes to test_runs table.
+
+        * public/admin/test-configurations.php:
+        (add_run): Regenerate the manifest file to invalidate the /api/runs JSON cache.
+        (delete_run): Ditto.
+
+        * public/api/runs.php:
+        (main): Fetch all columns of test_configurations table including config_runs_last_modified. Also generate
+        the cache in /data/ directory.
+        (RunsGenerator::__construct): Compute the last modified time for this (platform, metric) pair.
+        (RunsGenerator::results): Put the old content in 'configurations' property and include 'lastModified' and
+        'elapsedTime' properties. 'elapsedTime' is added for debugging purposes.
+        (RunsGenerator::add_runs):
+        (RunsGenerator::parse_revisions_array):
+
+        * public/include/db.php:
+        (CONFIG_DIR): Added.
+        (generate_data_file): Added based on ManifestGenerator::store.
+        (Database::to_js_time): Extracted from RunsGenerator::add_runs to share code.
+
+        * public/include/json-header.php:
+        (echo_success): Renamed from success_json. Return the serialized JSON instead of echo'ing it so that we can
+        generate caches in /api/runs/.
+        (exit_with_success):
+
+        * public/include/manifest.php:
+        (ManifestGenerator::generate): Added 'elapsedTime' property for the time taken to generate the manifest.
+        It seems like we're generating it in 200-300ms for now so that's good.
+        (ManifestGenerator::store): Uses generate_data_file.
+        (ManifestGenerator::platforms): Added 'lastModified' array to each platform entry. This array contains the
+        last modified time for each (platform, metric) pair.
+
+        * public/index.html:
+        (fetchTest): Updated per the format change in runs JSON.
+
+        * public/v2/app.js:
+        (App.Pane._fetch): Fetch the cached JSON first. Refetch the uncached version if instructed as such.
+        (App.Pane._updateChartData): Extracted from App.Pane._fetch.
+        (App.Pane._handleFetchErrors): Ditto.
+
+        * public/v2/data.js:
+        (RunsData.fetchRuns): Takes the fourth argument indicating whether we should fetch the cached version or not.
+        The cached JSON is located in /data/ with the same filename. When fetching a cached JSON results in 404,
+        fulfill the promise with null as the result instead of rejecting it. The only client of this function which
+        sets useCache to true is App.Manifest.fetchRunsWithPlatformAndMetric, and it handles this special case.
+
+        * public/v2/manifest.js:
+        (App.DateArrayTransform): Added. Handles the array of last modified dates in platform objects.
+        (App.Platform.lastModifiedTimeForMetric): Added. Returns the last modified date in the manifest JSON.
+        (App.Manifest.fetchRunsWithPlatformAndMetric): Takes &quot;useCache&quot; like RunsData.fetchRuns. Set shouldRefetch
+        to true if response is null (the cache didn't exit) or the cache is out-of-date.
+        (App.Manifest._formatFetchedData): Extracted from App.Manifest.fetchRunsWithPlatformAndMetric.
+
+        * run-tests.js:
+        (initializeDatabase): Avoid splitting function definitions in the middle.
+
+        * tests/api-report.js: Added tests to verify that reporting new test results updates the last modified time
+        in test_configurations.
+
+2015-02-20  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
</ins><span class="cx">         REGRESSION(r180333): Analysis tasks can't be associated with bugs
</span><span class="cx">         https://bugs.webkit.org/show_bug.cgi?id=141858
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorginitdatabasesql"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/init-database.sql (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/init-database.sql        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/init-database.sql        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -35,9 +35,11 @@
</span><span class="cx"> 
</span><span class="cx"> CREATE TABLE repositories (
</span><span class="cx">     repository_id serial PRIMARY KEY,
</span><ins>+    repository_parent integer REFERENCES repositories ON DELETE CASCADE,
</ins><span class="cx">     repository_name varchar(64) NOT NULL,
</span><span class="cx">     repository_url varchar(1024),
</span><del>-    repository_blame_url varchar(1024));
</del><ins>+    repository_blame_url varchar(1024),
+    CONSTRAINT repository_name_must_be_unique UNIQUE(repository_parent, repository_name));
</ins><span class="cx"> 
</span><span class="cx"> CREATE TABLE bug_trackers (
</span><span class="cx">     tracker_id serial PRIMARY KEY,
</span><span class="lines">@@ -121,6 +123,7 @@
</span><span class="cx">     config_platform integer NOT NULL REFERENCES platforms ON DELETE CASCADE,
</span><span class="cx">     config_type test_configuration_type NOT NULL,
</span><span class="cx">     config_is_in_dashboard boolean NOT NULL DEFAULT FALSE,
</span><ins>+    config_runs_last_modified timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
</ins><span class="cx">     CONSTRAINT configuration_must_be_unique UNIQUE(config_metric, config_platform, config_type));
</span><span class="cx"> CREATE INDEX config_platform_index ON test_configurations(config_platform);
</span><span class="cx"> 
</span><span class="lines">@@ -144,6 +147,20 @@
</span><span class="cx">     iteration_relative_time float,
</span><span class="cx">     PRIMARY KEY (iteration_run, iteration_order));
</span><span class="cx"> 
</span><ins>+CREATE OR REPLACE FUNCTION update_config_last_modified() RETURNS TRIGGER AS $update_config_last_modified$
+    BEGIN
+        IF TG_OP != 'DELETE' THEN
+            UPDATE test_configurations SET config_runs_last_modified = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') WHERE config_id = NEW.run_config;
+        ELSE
+            UPDATE test_configurations SET config_runs_last_modified = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') WHERE config_id = OLD.run_config;
+        END IF;
+        RETURN NULL;
+    END;
+$update_config_last_modified$ LANGUAGE plpgsql;
+
+CREATE TRIGGER update_config_last_modified AFTER INSERT OR UPDATE OR DELETE ON test_runs
+    FOR EACH ROW EXECUTE PROCEDURE update_config_last_modified();
+
</ins><span class="cx"> CREATE TABLE reports (
</span><span class="cx">     report_id serial PRIMARY KEY,
</span><span class="cx">     report_builder integer NOT NULL REFERENCES builders ON DELETE RESTRICT,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicadmintestconfigurationsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/admin/test-configurations.php (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/admin/test-configurations.php        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/admin/test-configurations.php        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -29,6 +29,8 @@
</span><span class="cx"> 
</span><span class="cx">     $db-&gt;commit_transaction();
</span><span class="cx">     notice(&quot;Added a baseline test run.&quot;);
</span><ins>+
+    regenerate_manifest();
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> function delete_run($run_id, $build_id) {
</span><span class="lines">@@ -65,6 +67,8 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     $db-&gt;commit_transaction();
</span><ins>+
+    regenerate_manifest();
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> if ($db) {
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapirunsphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/api/runs.php (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/runs.php        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/api/runs.php        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -37,7 +37,7 @@
</span><span class="cx"> 
</span><span class="cx">     $platform_id = intval($parts[0]);
</span><span class="cx">     $metric_id = intval($parts[1]);
</span><del>-    $config_rows = $db-&gt;query_and_fetch_all('SELECT config_id, config_type, config_platform, config_metric
</del><ins>+    $config_rows = $db-&gt;query_and_fetch_all('SELECT *
</ins><span class="cx">         FROM test_configurations WHERE config_metric = $1 AND config_platform = $2', array($metric_id, $platform_id));
</span><span class="cx">     if (!$config_rows)
</span><span class="cx">         exit_with_error('ConfigurationNotFound');
</span><span class="lines">@@ -52,7 +52,7 @@
</span><span class="cx">         header(&quot;Cache-Control: maxage=$maxage&quot;);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    $generator = new RunsGenerator();
</del><ins>+    $generator = new RunsGenerator($config_rows);
</ins><span class="cx"> 
</span><span class="cx">     foreach ($config_rows as $config) {
</span><span class="cx">         if ($test_group_id)
</span><span class="lines">@@ -62,15 +62,28 @@
</span><span class="cx">         $generator-&gt;add_runs($config['config_type'], $raw_runs);
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    exit_with_success($generator-&gt;results());
</del><ins>+    $content = success_json($generator-&gt;results());
+    if (!$test_group_id)
+        generate_data_file(&quot;$platform_id-$metric_id.json&quot;, $content);
+    echo $content;
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> class RunsGenerator {
</span><del>-    function __construct() {
</del><ins>+    function __construct($config_rows) {
</ins><span class="cx">         $this-&gt;results = array();
</span><ins>+        $last_modified_times = array();
+        foreach ($config_rows as $row)
+            array_push($last_modified_times, Database::to_js_time($row['config_runs_last_modified']));
+        $this-&gt;last_modified = max($last_modified_times);
+        $this-&gt;start_time = microtime(true);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    function &amp;results() { return $this-&gt;results; }
</del><ins>+    function results() {
+        return array(
+            'configurations' =&gt; &amp;$this-&gt;results,
+            'lastModified' =&gt; $this-&gt;last_modified,
+            'elapsedTime' =&gt; microtime(true) - $this-&gt;start_time);
+    }
</ins><span class="cx"> 
</span><span class="cx">     function add_runs($name, $raw_runs) {
</span><span class="cx">         $formatted_runs = array();
</span><span class="lines">@@ -91,7 +104,7 @@
</span><span class="cx">             'squareSum' =&gt; floatval($run['run_square_sum_cache']),
</span><span class="cx">             'revisions' =&gt; self::parse_revisions_array($run['revisions']),
</span><span class="cx">             'build' =&gt; $run['build_id'],
</span><del>-            'buildTime' =&gt; strtotime($run['build_time']) * 1000,
</del><ins>+            'buildTime' =&gt; Database::to_js_time($run['build_time']),
</ins><span class="cx">             'buildNumber' =&gt; intval($run['build_number']),
</span><span class="cx">             'builder' =&gt; $run['build_builder']);
</span><span class="cx">     }
</span><span class="lines">@@ -104,7 +117,7 @@
</span><span class="cx">             $name_and_revision = explode(',', trim($item, '()'));
</span><span class="cx">             if (!$name_and_revision[0])
</span><span class="cx">                 continue;
</span><del>-            $time = strtotime(trim($name_and_revision[2], '&quot;')) * 1000;
</del><ins>+            $time = Database::to_js_time(trim($name_and_revision[2], '&quot;'));
</ins><span class="cx">             $revisions[trim($name_and_revision[0], '&quot;')] = array(trim($name_and_revision[1], '&quot;'), $time);
</span><span class="cx">         }
</span><span class="cx">         return $revisions;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludedbphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/db.php (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/db.php        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/include/db.php        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -27,13 +27,21 @@
</span><span class="cx"> 
</span><span class="cx"> $_config = NULL;
</span><span class="cx"> 
</span><ins>+define('CONFIG_DIR', dirname(__FILE__) . '/../../');
+
</ins><span class="cx"> function config($key) {
</span><span class="cx">     global $_config;
</span><span class="cx">     if (!$_config)
</span><del>-        $_config = json_decode(file_get_contents(dirname(__FILE__) . '/../../config.json'), true);
</del><ins>+        $_config = json_decode(file_get_contents(CONFIG_DIR . 'config.json'), true);
</ins><span class="cx">     return $_config[$key];
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+function generate_data_file($filename, $content) {
+    if (!assert(ctype_alnum(str_replace(array('-', '_', '.'), '', $filename))))
+        return FALSE;
+    return file_put_contents(CONFIG_DIR . config('dataDirectory') . '/' . $filename, $content);
+}
+
</ins><span class="cx"> if (config('debug')) {
</span><span class="cx">     error_reporting(E_ALL | E_STRICT);
</span><span class="cx">     ini_set('display_errors', 'On');
</span><span class="lines">@@ -56,6 +64,10 @@
</span><span class="cx">         return $value == 't';
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static function to_js_time($time_str) {
+        return strtotime($time_str) * 1000;
+    }
+
</ins><span class="cx">     function connect() {
</span><span class="cx">         $databaseConfig = config('database');
</span><span class="cx">         $this-&gt;connection = pg_connect('host=' . $databaseConfig['host'] . ' port=' . $databaseConfig['port']
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludejsonheaderphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/json-header.php (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/json-header.php        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/include/json-header.php        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -13,15 +13,15 @@
</span><span class="cx">     exit(1);
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-function echo_success($details = array()) {
</del><ins>+function success_json($details = array()) {
</ins><span class="cx">     $details['status'] = 'OK';
</span><span class="cx">     merge_additional_details($details);
</span><span class="cx"> 
</span><del>-    echo json_encode($details);
</del><ins>+    return json_encode($details);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> function exit_with_success($details = array()) {
</span><del>-    echo_success($details);
</del><ins>+    echo success_json($details);
</ins><span class="cx">     exit(0);
</span><span class="cx"> }
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicincludemanifestphp"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/include/manifest.php (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/include/manifest.php        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/include/manifest.php        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -12,6 +12,8 @@
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     function generate() {
</span><ins>+        $start_time = microtime(true);
+
</ins><span class="cx">         $config_table = $this-&gt;db-&gt;fetch_table('test_configurations');
</span><span class="cx">         $platform_table = $this-&gt;db-&gt;fetch_table('platforms');
</span><span class="cx">         $repositories_table = $this-&gt;db-&gt;fetch_table('repositories');
</span><span class="lines">@@ -34,11 +36,14 @@
</span><span class="cx">             'bugTrackers' =&gt; $this-&gt;bug_trackers($repositories_table),
</span><span class="cx">             'dashboards' =&gt; config('dashboards'),
</span><span class="cx">         );
</span><del>-        return $this-&gt;manifest;
</del><ins>+
+        $this-&gt;manifest['elapsedTime'] = (microtime(true) - $start_time) * 1000;
+
+        return TRUE;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     function store() {
</span><del>-        return file_put_contents(self::MANIFEST_PATH, json_encode($this-&gt;manifest));
</del><ins>+        return generate_data_file('manifest.json', json_encode($this-&gt;manifest));
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     private function tests() {
</span><span class="lines">@@ -77,19 +82,37 @@
</span><span class="cx">                 if ($is_dashboard &amp;&amp; !$this-&gt;db-&gt;is_true($config_row['config_is_in_dashboard']))
</span><span class="cx">                     continue;
</span><span class="cx"> 
</span><ins>+                $new_last_modified = array_get($config_row, 'config_runs_last_modified', 0);
+                if ($new_last_modified)
+                    $new_last_modified = strtotime($config_row['config_runs_last_modified']) * 1000;
+
</ins><span class="cx">                 $platform = &amp;array_ensure_item_has_array($platform_metrics, $config_row['config_platform']);
</span><del>-                if (!in_array($config_row['config_metric'], $platform))
-                    array_push($platform, $config_row['config_metric']);
</del><ins>+                $metrics = &amp;array_ensure_item_has_array($platform, 'metrics');
+                $last_modified = &amp;array_ensure_item_has_array($platform, 'last_modified');
+
+                $metric_id = $config_row['config_metric'];
+                $index = array_search($metric_id, $metrics);
+                if ($index === FALSE) {
+                    array_push($metrics, $metric_id);
+                    array_push($last_modified, $new_last_modified);
+                } else
+                    $last_modified[$index] = max($last_modified[$index], $new_last_modified);
</ins><span class="cx">             }
</span><span class="cx">         }
</span><ins>+        $configurations = array();
+        
</ins><span class="cx">         $platforms = array();
</span><span class="cx">         if ($platform_table) {
</span><span class="cx">             foreach ($platform_table as $platform_row) {
</span><span class="cx">                 if ($this-&gt;db-&gt;is_true($platform_row['platform_hidden']))
</span><span class="cx">                     continue;
</span><span class="cx">                 $id = $platform_row['platform_id'];
</span><del>-                if (array_key_exists($id, $platform_metrics))
-                    $platforms[$id] = array('name' =&gt; $platform_row['platform_name'], 'metrics' =&gt; $platform_metrics[$id]);
</del><ins>+                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']);
+                }
</ins><span class="cx">             }
</span><span class="cx">         }
</span><span class="cx">         return $platforms;
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicindexhtml"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/index.html (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/index.html        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/index.html        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -840,7 +840,8 @@
</span><span class="cx">         return runs;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    $.getJSON('api/runs/' + filename, function (data) {
</del><ins>+    $.getJSON('api/runs/' + filename, function (response) {
+        var data = response.configurations;
</ins><span class="cx">         callback(createRunAndResults(data.current), createRunAndResults(data.baseline), createRunAndResults(data.target));
</span><span class="cx">     });
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2appjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/app.js (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/app.js        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/v2/app.js        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -349,27 +349,41 @@
</span><span class="cx">         else if (!this._isValidId(metricId))
</span><span class="cx">             this.set('failure', metricId ? 'Invalid metric id:' + metricId : 'Metric id was not specified');
</span><span class="cx">         else {
</span><del>-            var self = this;
</del><ins>+            var store = this.get('store');
+            var updateChartData = this._updateChartData.bind(this);
+            var handleErrors = this._handleFetchErrors.bind(this, platformId, metricId);
+            var useCache = true;
+            App.Manifest.fetchRunsWithPlatformAndMetric(store, platformId, metricId, null, useCache).then(function (result) {
+                    updateChartData(result);
+                    if (!result.shouldRefetch)
+                        return;
</ins><span class="cx"> 
</span><del>-            App.Manifest.fetchRunsWithPlatformAndMetric(this.get('store'), platformId, metricId).then(function (result) {
-                self.set('platform', result.platform);
-                self.set('metric', result.metric);
-                self.set('chartData', result.data);
-                self._updateMovingAverageAndEnvelope();
-            }, function (result) {
-                if (!result || typeof(result) === &quot;string&quot;)
-                    self.set('failure', 'Failed to fetch the JSON with an error: ' + result);
-                else if (!result.platform)
-                    self.set('failure', 'Could not find the platform &quot;' + platformId + '&quot;');
-                else if (!result.metric)
-                    self.set('failure', 'Could not find the metric &quot;' + metricId + '&quot;');
-                else
-                    self.set('failure', 'An internal error');
-            });
-
</del><ins>+                    useCache = false;
+                    App.Manifest.fetchRunsWithPlatformAndMetric(store, platformId, metricId, null, useCache)
+                        .then(updateChartData, handleErrors);
+                }, handleErrors);
</ins><span class="cx">             this.fetchAnalyticRanges();
</span><span class="cx">         }
</span><span class="cx">     }.observes('platformId', 'metricId').on('init'),
</span><ins>+    _updateChartData: function (result)
+    {
+        this.set('platform', result.platform);
+        this.set('metric', result.metric);
+        this.set('chartData', result.data);
+        this._updateMovingAverageAndEnvelope();
+    },
+    _handleFetchErrors: function (platformId, metricId, result)
+    {
+        console.log(platformId, metricId, result)
+        if (!result || typeof(result) === &quot;string&quot;)
+            this.set('failure', 'Failed to fetch the JSON with an error: ' + result);
+        else if (!result.platform)
+            this.set('failure', 'Could not find the platform &quot;' + platformId + '&quot;');
+        else if (!result.metric)
+            this.set('failure', 'Could not find the metric &quot;' + metricId + '&quot;');
+        else
+            this.set('failure', 'An internal error');
+    },
</ins><span class="cx">     fetchAnalyticRanges: function ()
</span><span class="cx">     {
</span><span class="cx">         var platformId = this.get('platformId');
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2datajs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/data.js (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/data.js        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/v2/data.js        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -325,27 +325,35 @@
</span><span class="cx"> 
</span><span class="cx"> // FIXME: We need to devise a way to fetch runs in multiple chunks so that
</span><span class="cx"> // we don't have to fetch the entire time series to just show the last 3 days.
</span><del>-RunsData.fetchRuns = function (platformId, metricId, testGroupId)
</del><ins>+RunsData.fetchRuns = function (platformId, metricId, testGroupId, useCache)
</ins><span class="cx"> {
</span><del>-    var filename = platformId + '-' + metricId + '.json';
</del><ins>+    var url = useCache ? '../data/' : '../api/runs/';
</ins><span class="cx"> 
</span><ins>+    url += platformId + '-' + metricId + '.json';
</ins><span class="cx">     if (testGroupId)
</span><del>-        filename += '?testGroup=' + testGroupId;
</del><ins>+        url += '?testGroup=' + testGroupId;
</ins><span class="cx"> 
</span><span class="cx">     return new Ember.RSVP.Promise(function (resolve, reject) {
</span><del>-        $.getJSON('../api/runs/' + filename, function (data) {
-            if (data.status != 'OK') {
-                reject(data.status);
</del><ins>+        $.getJSON(url, function (response) {
+            if (response.status != 'OK') {
+                reject(response.status);
</ins><span class="cx">                 return;
</span><span class="cx">             }
</span><del>-            delete data.status;
</del><ins>+            delete response.status;
</ins><span class="cx"> 
</span><ins>+            var data = response.configurations;
</ins><span class="cx">             for (var config in data)
</span><span class="cx">                 data[config] = new RunsData(data[config]);
</span><ins>+            
+            if (response.lastModified)
+                response.lastModified = new Date(response.lastModified);
</ins><span class="cx"> 
</span><del>-            resolve(data);
</del><ins>+            resolve(response);
</ins><span class="cx">         }).fail(function (xhr, status, error) {
</span><del>-            reject(xhr.status + (error ? ', ' + error : ''));
</del><ins>+            if (xhr.status == 404 &amp;&amp; useCache)
+                resolve(null);
+            else
+                reject(xhr.status + (error ? ', ' + error : ''));
</ins><span class="cx">         })
</span><span class="cx">     });
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicv2manifestjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/public/v2/manifest.js (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/v2/manifest.js        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/public/v2/manifest.js        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -34,7 +34,7 @@
</span><span class="cx">     }.property('name', 'test'),
</span><span class="cx">     fullName: function ()
</span><span class="cx">     {
</span><del>-        return this.get('path').join(' \u220b ') /* &amp;ni; */
</del><ins>+        return this.get('path').join(' \u220b ') /* &amp;in; */
</ins><span class="cx">             + ' : ' + this.get('label');
</span><span class="cx">     }.property('path', 'label'),
</span><span class="cx"> });
</span><span class="lines">@@ -54,16 +54,31 @@
</span><span class="cx">     repositories: DS.hasMany('repository'),
</span><span class="cx"> });
</span><span class="cx"> 
</span><ins>+App.DateArrayTransform = DS.Transform.extend({
+    deserialize: function (serialized)
+    {
+        return serialized.map(function (time) { return new Date(time); });
+    }
+});
+
</ins><span class="cx"> App.Platform = App.NameLabelModel.extend({
</span><span class="cx">     _metricSet: null,
</span><span class="cx">     _testSet: null,
</span><span class="cx">     metrics: DS.hasMany('metric'),
</span><ins>+    lastModified: DS.attr('dateArray'),
</ins><span class="cx">     containsMetric: function (metric)
</span><span class="cx">     {
</span><span class="cx">         if (!this._metricSet)
</span><span class="cx">             this._metricSet = new Ember.Set(this.get('metrics'));
</span><span class="cx">         return this._metricSet.contains(metric);
</span><span class="cx">     },
</span><ins>+    lastModifiedTimeForMetric: function (metric)
+    {
+        var index = this.get('metrics').indexOf(metric);
+        if (index &lt; 0)
+            return null;
+        return this.get('lastModified').objectAt(index);
+    },
</ins><span class="cx">     containsTest: function (test)
</span><span class="cx">     {
</span><span class="cx">         if (!this._testSet) {
</span><span class="lines">@@ -279,49 +294,58 @@
</span><span class="cx">         dashboards.forEach(function (dashboard) { self._dashboardByName[dashboard.get('name')] = dashboard; });
</span><span class="cx">         this._defaultDashboardName = dashboards.length ? dashboards[0].get('name') : null;
</span><span class="cx">     },
</span><del>-    fetchRunsWithPlatformAndMetric: function (store, platformId, metricId, testGroupId)
</del><ins>+    fetchRunsWithPlatformAndMetric: function (store, platformId, metricId, testGroupId, useCache)
</ins><span class="cx">     {
</span><ins>+        Ember.assert(&quot;Can't cache results for test groups&quot;, !(testGroupId &amp;&amp; useCache));
+        var self = this;
</ins><span class="cx">         return Ember.RSVP.all([
</span><del>-            RunsData.fetchRuns(platformId, metricId, testGroupId),
</del><ins>+            RunsData.fetchRuns(platformId, metricId, testGroupId, useCache),
</ins><span class="cx">             this.fetch(store),
</span><span class="cx">         ]).then(function (values) {
</span><del>-            var runs = values[0];
</del><ins>+            var response = values[0];
</ins><span class="cx"> 
</span><span class="cx">             var platform = App.Manifest.platform(platformId);
</span><span class="cx">             var metric = App.Manifest.metric(metricId);
</span><span class="cx"> 
</span><del>-            var suffix = metric.get('name').match('([A-z][a-z]+|FrameRate)$')[0];
-            var unit = {
-                'FrameRate': 'fps',
-                'Runs': '/s',
-                'Time': 'ms',
-                'Malloc': 'bytes',
-                'Heap': 'bytes',
-                'Allocations': 'bytes'
-            }[suffix];
-            var smallerIsBetter = unit != 'fps' &amp;&amp; unit != '/s'; // Assume smaller is better for unit-less metrics.
-
-            var useSI = unit == 'bytes';
-            var unitSuffix = unit ? ' ' + unit : '';
-            var deltaFormatterWithoutSign = useSI ? d3.format('.2s') : d3.format('.2g');
</del><span class="cx">             return {
</span><span class="cx">                 platform: platform,
</span><span class="cx">                 metric: metric,
</span><del>-                data: {
-                    current: runs.current.timeSeriesByCommitTime(),
-                    baseline: runs.baseline ? runs.baseline.timeSeriesByCommitTime() : null,
-                    target: runs.target ? runs.target.timeSeriesByCommitTime() : null,
-                    unit: unit,
-                    formatWithUnit: function (value) { return this.formatter(value) + unitSuffix; },
-                    formatWithDeltaAndUnit: function (value, delta)
-                    {
-                        return this.formatter(value) + (delta &amp;&amp; !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
-                    },
-                    formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
-                    deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
-                    smallerIsBetter: smallerIsBetter,
-                }
</del><ins>+                data: response ? self._formatFetchedData(metric.get('name'), response.configurations) : null,
+                shouldRefetch: !response || +response.lastModified &lt; +platform.lastModifiedTimeForMetric(metric),
</ins><span class="cx">             };
</span><span class="cx">         });
</span><span class="cx">     },
</span><ins>+    _formatFetchedData: function (metricName, configurations)
+    {
+        var suffix = metricName.match('([A-z][a-z]+|FrameRate)$')[0];
+        var unit = {
+            'FrameRate': 'fps',
+            'Runs': '/s',
+            'Time': 'ms',
+            'Malloc': 'bytes',
+            'Heap': 'bytes',
+            'Allocations': 'bytes'
+        }[suffix];
+
+        var smallerIsBetter = unit != 'fps' &amp;&amp; unit != '/s'; // Assume smaller is better for unit-less metrics.
+
+        var useSI = unit == 'bytes';
+        var unitSuffix = unit ? ' ' + unit : '';
+        var deltaFormatterWithoutSign = useSI ? d3.format('.2s') : d3.format('.2g');
+
+        return {
+            current: configurations.current.timeSeriesByCommitTime(),
+            baseline: configurations.baseline ? configurations.baseline.timeSeriesByCommitTime() : null,
+            target: configurations.target ? configurations.target.timeSeriesByCommitTime() : null,
+            unit: unit,
+            formatWithUnit: function (value) { return this.formatter(value) + unitSuffix; },
+            formatWithDeltaAndUnit: function (value, delta)
+            {
+                return this.formatter(value) + (delta &amp;&amp; !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
+            },
+            formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
+            deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
+            smallerIsBetter: smallerIsBetter,
+        };
+    }
</ins><span class="cx"> }).create();
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgruntestsjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/run-tests.js (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/run-tests.js        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/run-tests.js        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -199,7 +199,7 @@
</span><span class="cx"> 
</span><span class="cx">     var firstError;
</span><span class="cx">     var queue = new TaskQueue();
</span><del>-    commaSeparatedSqlStatements.split(/;\s*/).forEach(function (statement) {
</del><ins>+    commaSeparatedSqlStatements.split(/;\s*(?=CREATE|DROP)/).forEach(function (statement) {
</ins><span class="cx">         queue.addTask(function (error, callback) {
</span><span class="cx">             client.query(statement, function (error) {
</span><span class="cx">                 if (error &amp;&amp; !firstError)
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtestsapireportjs"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/tests/api-report.js (180467 => 180468)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tests/api-report.js        2015-02-21 00:36:27 UTC (rev 180467)
+++ trunk/Websites/perf.webkit.org/tests/api-report.js        2015-02-21 01:28:34 UTC (rev 180468)
</span><span class="lines">@@ -687,4 +687,73 @@
</span><span class="cx">             });
</span><span class="cx">         });
</span><span class="cx">     });
</span><ins>+
+    var reportsUpdatingDifferentTests = [
+        [{
+            &quot;buildNumber&quot;: &quot;123&quot;,
+            &quot;buildTime&quot;: &quot;2013-02-28T10:12:03&quot;,
+            &quot;builderName&quot;: &quot;someBuilder&quot;,
+            &quot;builderPassword&quot;: &quot;somePassword&quot;,
+            &quot;platform&quot;: &quot;Mountain Lion&quot;,
+            &quot;tests&quot;: {&quot;test1&quot;: {&quot;metrics&quot;: {&quot;Time&quot;: {&quot;current&quot;: 3}}}}
+        }],
+        [{
+            &quot;buildNumber&quot;: &quot;124&quot;,
+            &quot;buildTime&quot;: &quot;2013-02-28T11:31:21&quot;,
+            &quot;builderName&quot;: &quot;someBuilder&quot;,
+            &quot;builderPassword&quot;: &quot;somePassword&quot;,
+            &quot;platform&quot;: &quot;Mountain Lion&quot;,
+            &quot;tests&quot;: {&quot;test2&quot;: {&quot;metrics&quot;: {&quot;Time&quot;: {&quot;current&quot;: 3}}}}
+        }],
+        [{
+            &quot;buildNumber&quot;: &quot;125&quot;,
+            &quot;buildTime&quot;: &quot;2013-02-28T12:45:34&quot;,
+            &quot;builderName&quot;: &quot;someBuilder&quot;,
+            &quot;builderPassword&quot;: &quot;somePassword&quot;,
+            &quot;platform&quot;: &quot;Mountain Lion&quot;,
+            &quot;tests&quot;: {&quot;test1&quot;: {&quot;metrics&quot;: {&quot;Time&quot;: {&quot;current&quot;: 3}}}}
+        }],
+    ];
+
+    function fetchTestConfig(testName, metricName, callback) {
+         queryAndFetchAll('SELECT * FROM tests, test_metrics, test_configurations WHERE test_id = metric_test AND metric_id = config_metric'
+            + ' AND test_name = $1 AND metric_name = $2', [testName, metricName], function (runRows) {
+                assert.equal(runRows.length, 1);
+                callback(runRows[0]);
+            });
+    }
+
+    it(&quot;should update the last modified date of test configurations with new runs&quot;, function () {
+        addBuilder(reportsUpdatingDifferentTests[0], function () {
+            postJSON('/api/report/', reportsUpdatingDifferentTests[0], function (response) {
+                assert.equal(response.statusCode, 200);
+                fetchTestConfig('test1', 'Time', function (originalConfig) {
+                    postJSON('/api/report/', reportsUpdatingDifferentTests[2], function (response) {
+                        assert.equal(response.statusCode, 200);
+                        fetchTestConfig('test1', 'Time', function (config) {
+                            assert.notEqual(+originalConfig['config_runs_last_modified'], +config['config_runs_last_modified']);
+                            notifyDone();
+                        });
+                    });
+                });
+            });
+        });
+    });
+
+    it(&quot;should update the last modified date of unrelated test configurations&quot;, function () {
+        addBuilder(reportsUpdatingDifferentTests[0], function () {
+            postJSON('/api/report/', reportsUpdatingDifferentTests[0], function (response) {
+                assert.equal(response.statusCode, 200);
+                fetchTestConfig('test1', 'Time', function (originalConfig) {
+                    postJSON('/api/report/', reportsUpdatingDifferentTests[1], function (response) {
+                        assert.equal(response.statusCode, 200);
+                        fetchTestConfig('test1', 'Time', function (config) {
+                            assert.equal(+originalConfig['config_runs_last_modified'], +config['config_runs_last_modified']);
+                            notifyDone();
+                        });
+                    });
+                });
+            });
+        });
+    });
</ins><span class="cx"> });
</span></span></pre>
</div>
</div>

</body>
</html>