<!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>[194120] 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/194120">194120</a></dd>
<dt>Author</dt> <dd>rniwa@webkit.org</dd>
<dt>Date</dt> <dd>2015-12-15 15:57:25 -0800 (Tue, 15 Dec 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add /api/measurement-set for v3 UI
https://bugs.webkit.org/show_bug.cgi?id=152312

Rubber-stamped by Chris Dumez.

The new API JSON allows the front end to fetch measured data in chunks called a &quot;cluster&quot; as specified
in config.json for each measurement set specified by the pair of a platform and a metric.

When the front end needs measured data in a given time range (t_0, t_1) for a measurement set, it first
fetches the primary cluster by /api/measurement-set/?platform=&lt;platform-id&gt;&amp;metric=&lt;metric-id&gt;.
The primary cluster is the last cluster in the set (returning the first cluster here is not useful
since we don't typically show very old data), and provides the information needed to fetch other clusters.

Fetching the primary cluster also creates JSON files at:
/data/measurement-set-&lt;platform-id&gt;-&lt;metric-id&gt;-&lt;cluster-end-time&gt;.json
to allow latency free access for secondary clusters. The front end code can also fetch the cache of
the primary cluster at: /data/measurement-set-&lt;platform-id&gt;-&lt;metric-id&gt;.json.

Because the front end code has to behave as if all data is fetched, each cluster contains one data point
immediately before the first data point and one immediately after the last data point. This avoids having
to fetch multiple empty clusters for manually specified baseline data. To support this behavior, we generate
all clusters for a given measurement set at once when the primary cluster is requested.

Furthermore, all measurement sets are divided at the same time into clusters so that the boundary of clusters
won't shift as more data are reported to the server.

* config.json: Added clusterStart and clusterSize as options.
* public/api/measurement-set.php: Added.
(main):
(MeasurementSetFetcher::__construct):
(MeasurementSetFetcher::fetch_config_list): Finds configurations that belongs to this (platform, metric) pair.
(MeasurementSetFetcher::at_end): Returns true if we've reached the end of all clusters for this set.
(MeasurementSetFetcher::fetch_next_cluster): Generates the JSON data for the next cluster. We generate clusters
in increasing chronological order (the oldest first and the newest last).
(MeasurementSetFetcher::execute_query): Executes the main query.
(MeasurementSetFetcher::format_map): Returns the mapping of a measurement field to an array index. This removes
the need to have key names for each measurement and reduces the JSON size by ~10%.
(MeasurementSetFetcher::format_run): Creates an array that contains data for a single measurement. The order
matches that of keys in format_map.
(MeasurementSetFetcher::parse_revisions_array): Added. Copied from runs.php.
* tests/api-measurement-set.js: Added. Added tests for /api/measurement-set.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgChangeLog">trunk/Websites/perf.webkit.org/ChangeLog</a></li>
<li><a href="#trunkWebsitesperfwebkitorgconfigjson">trunk/Websites/perf.webkit.org/config.json</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkWebsitesperfwebkitorgpublicapimeasurementsetphp">trunk/Websites/perf.webkit.org/public/api/measurement-set.php</a></li>
<li><a href="#trunkWebsitesperfwebkitorgtestsapimeasurementsetjs">trunk/Websites/perf.webkit.org/tests/api-measurement-set.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 (194119 => 194120)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/ChangeLog        2015-12-15 23:07:45 UTC (rev 194119)
+++ trunk/Websites/perf.webkit.org/ChangeLog        2015-12-15 23:57:25 UTC (rev 194120)
</span><span class="lines">@@ -1,3 +1,47 @@
</span><ins>+2015-12-15  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
+
+        Add /api/measurement-set for v3 UI
+        https://bugs.webkit.org/show_bug.cgi?id=152312
+
+        Rubber-stamped by Chris Dumez.
+
+        The new API JSON allows the front end to fetch measured data in chunks called a &quot;cluster&quot; as specified
+        in config.json for each measurement set specified by the pair of a platform and a metric.
+
+        When the front end needs measured data in a given time range (t_0, t_1) for a measurement set, it first
+        fetches the primary cluster by /api/measurement-set/?platform=&lt;platform-id&gt;&amp;metric=&lt;metric-id&gt;.
+        The primary cluster is the last cluster in the set (returning the first cluster here is not useful
+        since we don't typically show very old data), and provides the information needed to fetch other clusters.
+
+        Fetching the primary cluster also creates JSON files at:
+        /data/measurement-set-&lt;platform-id&gt;-&lt;metric-id&gt;-&lt;cluster-end-time&gt;.json
+        to allow latency free access for secondary clusters. The front end code can also fetch the cache of
+        the primary cluster at: /data/measurement-set-&lt;platform-id&gt;-&lt;metric-id&gt;.json.
+
+        Because the front end code has to behave as if all data is fetched, each cluster contains one data point
+        immediately before the first data point and one immediately after the last data point. This avoids having
+        to fetch multiple empty clusters for manually specified baseline data. To support this behavior, we generate
+        all clusters for a given measurement set at once when the primary cluster is requested.
+
+        Furthermore, all measurement sets are divided at the same time into clusters so that the boundary of clusters
+        won't shift as more data are reported to the server.
+
+        * config.json: Added clusterStart and clusterSize as options.
+        * public/api/measurement-set.php: Added.
+        (main):
+        (MeasurementSetFetcher::__construct):
+        (MeasurementSetFetcher::fetch_config_list): Finds configurations that belongs to this (platform, metric) pair.
+        (MeasurementSetFetcher::at_end): Returns true if we've reached the end of all clusters for this set.
+        (MeasurementSetFetcher::fetch_next_cluster): Generates the JSON data for the next cluster. We generate clusters
+        in increasing chronological order (the oldest first and the newest last).
+        (MeasurementSetFetcher::execute_query): Executes the main query.
+        (MeasurementSetFetcher::format_map): Returns the mapping of a measurement field to an array index. This removes
+        the need to have key names for each measurement and reduces the JSON size by ~10%.
+        (MeasurementSetFetcher::format_run): Creates an array that contains data for a single measurement. The order
+        matches that of keys in format_map.
+        (MeasurementSetFetcher::parse_revisions_array): Added. Copied from runs.php.
+        * tests/api-measurement-set.js: Added. Added tests for /api/measurement-set.
+
</ins><span class="cx"> 2015-12-14  Ryosuke Niwa  &lt;rniwa@webkit.org&gt;
</span><span class="cx"> 
</span><span class="cx">         Using fake timestamp in OS version make some results invisible
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgconfigjson"></a>
<div class="modfile"><h4>Modified: trunk/Websites/perf.webkit.org/config.json (194119 => 194120)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/config.json        2015-12-15 23:07:45 UTC (rev 194119)
+++ trunk/Websites/perf.webkit.org/config.json        2015-12-15 23:57:25 UTC (rev 194120)
</span><span class="lines">@@ -13,6 +13,8 @@
</span><span class="cx">         &quot;hostname&quot;: &quot;localhost&quot;,
</span><span class="cx">         &quot;port&quot;: 80
</span><span class="cx">     },
</span><ins>+    &quot;clusterStart&quot;: [2000, 1, 1, 0, 0],
+    &quot;clusterSize&quot;: [0, 2, 0],
</ins><span class="cx">     &quot;cacheDirectory&quot;: &quot;public/data/remote-cache/&quot;,
</span><span class="cx">     &quot;remoteServer&quot;: {
</span><span class="cx">         &quot;httpdConfig&quot;: &quot;tools/remote-server-relay.conf&quot;,
</span></span></pre></div>
<a id="trunkWebsitesperfwebkitorgpublicapimeasurementsetphp"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/public/api/measurement-set.php (0 => 194120)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/public/api/measurement-set.php                                (rev 0)
+++ trunk/Websites/perf.webkit.org/public/api/measurement-set.php        2015-12-15 23:57:25 UTC (rev 194120)
</span><span class="lines">@@ -0,0 +1,219 @@
</span><ins>+&lt;?php
+
+require('../include/json-header.php');
+
+function main() {
+    $program_start_time = microtime(true);
+
+    $arguments = validate_arguments($_GET, array(
+        'platform' =&gt; 'int',
+        'metric' =&gt; 'int',
+        'testGroup' =&gt; 'int?',
+        'startTime' =&gt; 'int?',
+        'endTime' =&gt; 'int?'));
+
+    $platform_id = $arguments['platform'];
+    $metric_id = $arguments['metric'];
+
+    $start_time = $arguments['startTime'];
+    $end_time = $arguments['endTime'];
+    if (!!$start_time != !!$end_time)
+        exit_with_error('InvalidTimeRange', array('startTime' =&gt; $start_time, 'endTime' =&gt; $end_time));
+
+    $db = new Database;
+    if (!$db-&gt;connect())
+        exit_with_error('DatabaseConnectionFailure');
+
+    $fetcher = new MeasurementSetFetcher($db);
+    if (!$fetcher-&gt;fetch_config_list($platform_id, $metric_id)) {
+        exit_with_error('ConfigurationNotFound',
+            array('platform' =&gt; $platform_id, 'metric' =&gt; $metric_id));
+    }
+
+    $cluster_count = 0;
+    while (!$fetcher-&gt;at_end()) {
+        $content = $fetcher-&gt;fetch_next_cluster();
+        $cluster_count++;
+        if ($fetcher-&gt;at_end()) {
+            $cache_filename = &quot;measurement-set-$platform_id-$metric_id.json&quot;;
+            $content['clusterCount'] = $cluster_count;
+            $content['elapsedTime'] = (microtime(true) - $program_start_time) * 1000;
+        } else
+            $cache_filename = &quot;measurement-set-$platform_id-$metric_id-{$content['endTime']}.json&quot;;
+
+        $json = success_json($content);
+        generate_data_file($cache_filename, $json);
+    }
+
+    echo $json;
+}
+
+define('DAY', 24 * 3600 * 1000);
+define('YEAR', 365.24 * DAY);
+define('MONTH', 30 * DAY);
+
+class MeasurementSetFetcher {
+    function __construct($db) {
+        $this-&gt;db = $db;
+        $this-&gt;queries = NULL;
+
+        // Each cluster contains data points between two commit time
+        // as well as a point immediately before and a point immediately after these points.
+        // Clusters are fetched in chronological order.
+        $start_time = config('clusterStart');
+        $size = config('clusterSize');
+        $this-&gt;cluster_start = mktime($start_time[3], $start_time[4], 0, $start_time[1], $start_time[2], $start_time[0]) * 1000;
+        $this-&gt;next_cluster_start = $this-&gt;cluster_start;
+        $this-&gt;next_cluster_results = NULL;
+        $this-&gt;cluster_size = $size[0] * YEAR + $size[1] * MONTH + $size[2] * DAY;
+        $this-&gt;last_modified = 0;
+
+        $this-&gt;start_time = microtime(TRUE);
+    }
+
+    function fetch_config_list($platform_id, $metric_id) {
+        $config_rows = $this-&gt;db-&gt;query_and_fetch_all('SELECT *
+            FROM test_configurations WHERE config_metric = $1 AND config_platform = $2',
+            array($metric_id, $platform_id));
+        $this-&gt;config_rows = $config_rows;
+        if (!$config_rows)
+            return FALSE;
+
+        $this-&gt;queries = array();
+        $this-&gt;next_cluster_results = array();
+        $min_commit_time = microtime(TRUE) * 1000;
+        foreach ($config_rows as &amp;$config_row) {
+            $query = $this-&gt;execute_query($config_row['config_id']);
+
+            $this-&gt;last_modified = max($this-&gt;last_modified, $config_row['config_runs_last_modified']);
+
+            $measurement_row = $this-&gt;db-&gt;fetch_next_row($query);
+            if ($measurement_row) {
+                $commit_time = 0;
+                $formatted_row = self::format_run($measurement_row, $commit_time);
+                $this-&gt;next_cluster_results[$config_row['config_type']] = array($formatted_row);
+                $min_commit_time = min($min_commit_time, $commit_time);
+            } else
+                $query = NULL;
+
+            $this-&gt;queries[$config_row['config_type']] = $query;
+        }
+
+        while ($this-&gt;next_cluster_start + $this-&gt;cluster_size &lt; $min_commit_time)
+            $this-&gt;next_cluster_start += $this-&gt;cluster_size;
+
+        return TRUE;
+    }
+
+    function at_end() {
+        if ($this-&gt;queries === NULL)
+            return FALSE;
+        foreach ($this-&gt;queries as $name =&gt; &amp;$query) {
+            if ($query)
+                return FALSE;
+        }
+        return TRUE;
+    }
+    
+    function fetch_next_cluster() {
+        assert($this-&gt;queries);
+
+        $results_by_config = array();
+        $current_cluster_start = $this-&gt;next_cluster_start;
+        $this-&gt;next_cluster_start += $this-&gt;cluster_size;
+
+        foreach ($this-&gt;queries as $name =&gt; &amp;$query) {
+            assert($this-&gt;next_cluster_start);
+
+            $carry_over = array_get($this-&gt;next_cluster_results, $name);
+            if ($carry_over)
+                $results_by_config[$name] = $carry_over;
+            else
+                $results_by_config[$name] = array();
+
+            if (!$query)
+                continue;
+
+            while ($row = $this-&gt;db-&gt;fetch_next_row($query)) {
+                $commit_time = NULL;
+                $formatted_row = self::format_run($row, $commit_time);
+                array_push($results_by_config[$name], $formatted_row);
+                $row_belongs_to_next_cluster = $commit_time &gt; $this-&gt;next_cluster_start;
+                if ($row_belongs_to_next_cluster)
+                    break;
+            }
+
+            $reached_end = !$row;
+            if ($reached_end)
+                $this-&gt;queries[$name] = NULL;
+            else {
+                $this-&gt;next_cluster_results[$name] = array_slice($results_by_config[$name], -2);
+            }
+        }
+
+        return array(
+            'clusterStart' =&gt; $this-&gt;cluster_start,
+            'clusterSize' =&gt; $this-&gt;cluster_size,
+            'configurations' =&gt; &amp;$results_by_config,
+            'formatMap' =&gt; self::format_map(),
+            'startTime' =&gt; $current_cluster_start,
+            'endTime' =&gt; $this-&gt;next_cluster_start,
+            'lastModified' =&gt; $this-&gt;last_modified);
+    }
+
+    function execute_query($config_id) {
+        return $this-&gt;db-&gt;query('
+            SELECT test_runs.*, builds.*,
+            array_agg((commit_repository, commit_revision, commit_time)) AS revisions,
+            max(commit_time) AS revision_time, max(commit_order) AS revision_order
+                FROM builds
+                    LEFT OUTER JOIN build_commits ON commit_build = build_id
+                    LEFT OUTER JOIN commits ON build_commit = commit_id, test_runs
+                WHERE run_build = build_id AND run_config = $1 AND NOT EXISTS (SELECT * FROM build_requests WHERE request_build = build_id)
+                GROUP BY build_id, run_id ORDER BY revision_time, revision_order, build_time', array($config_id));
+    }
+
+    static function format_map()
+    {
+        return array('id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier', 'revisions',
+            'commitTime', 'build', 'buildTime', 'buildNumber', 'builder');
+    }
+
+    private static function format_run($run, &amp;$commit_time) {
+        $commit_time = Database::to_js_time($run['revision_time']);
+        $build_time = Database::to_js_time($run['build_time']);
+        if (!$commit_time)
+            $commit_time = $build_time;
+        return array(
+            intval($run['run_id']),
+            floatval($run['run_mean_cache']),
+            intval($run['run_iteration_count_cache']),
+            floatval($run['run_sum_cache']),
+            floatval($run['run_square_sum_cache']),
+            Database::is_true($run['run_marked_outlier']),
+            self::parse_revisions_array($run['revisions']),
+            $commit_time,
+            intval($run['build_id']),
+            $build_time,
+            $run['build_number'],
+            intval($run['build_builder']));
+    }
+
+    private static function parse_revisions_array($postgres_array) {
+        // e.g. {&quot;(WebKit,131456,\&quot;2012-10-16 14:53:00\&quot;)&quot;,&quot;(Chromium,162004,)&quot;}
+        $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
+        $revisions = array();
+        foreach ($outer_array as $item) {
+            $name_and_revision = explode(',', trim($item, '()'));
+            if (!$name_and_revision[0])
+                continue;
+            $time = Database::to_js_time(trim($name_and_revision[2], '&quot;'));
+            array_push($revisions, array(intval(trim($name_and_revision[0], '&quot;')), trim($name_and_revision[1], '&quot;'), $time));
+        }
+        return $revisions;
+    }
+}
+
+main();
+
+?&gt;
</ins></span></pre></div>
<a id="trunkWebsitesperfwebkitorgtestsapimeasurementsetjs"></a>
<div class="addfile"><h4>Added: trunk/Websites/perf.webkit.org/tests/api-measurement-set.js (0 => 194120)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Websites/perf.webkit.org/tests/api-measurement-set.js                                (rev 0)
+++ trunk/Websites/perf.webkit.org/tests/api-measurement-set.js        2015-12-15 23:57:25 UTC (rev 194120)
</span><span class="lines">@@ -0,0 +1,403 @@
</span><ins>+describe(&quot;/api/measurement-set&quot;, function () {
+    function addBuilder(report, callback) {
+        queryAndFetchAll('INSERT INTO builders (builder_name, builder_password_hash) values ($1, $2)',
+            [report[0].builderName, sha256(report[0].builderPassword)], callback);
+    }
+
+    function queryPlatformAndMetric(platformName, metricName, callback) {
+        queryAndFetchAll('SELECT * FROM platforms WHERE platform_name = $1', [platformName], function (platformRows) {
+            queryAndFetchAll('SELECT * FROM test_metrics WHERE metric_name = $1', [metricName], function (metricRows) {
+                callback(platformRows[0]['platform_id'], metricRows[0]['metric_id']);
+            });
+        });
+    }
+
+    function format(formatMap, row) {
+        var result = {};
+        for (var i = 0; i &lt; formatMap.length; i++) {
+            var key = formatMap[i];
+            if (key == 'id' || key == 'build' || key == 'builder')
+                continue;
+            result[key] = row[i];
+        }
+        return result;
+    }
+
+    var clusterStart = config('clusterStart');
+    clusterStart = +Date.UTC(clusterStart[0], clusterStart[1] - 1, clusterStart[2], clusterStart[3], clusterStart[4]);
+
+    var clusterSize = config('clusterSize');
+    var DAY = 24 * 3600 * 1000;
+    var YEAR = 365.24 * DAY;
+    var MONTH = 30 * DAY;
+    clusterSize = clusterSize[0] * YEAR + clusterSize[1] * MONTH + clusterSize[2] * DAY;
+
+    function clusterTime(index) { return new Date(clusterStart + clusterSize * index); }
+
+    var reportWithBuildTime = [{
+        &quot;buildNumber&quot;: &quot;123&quot;,
+        &quot;buildTime&quot;: clusterTime(7.8).toISOString(),
+        &quot;builderName&quot;: &quot;someBuilder&quot;,
+        &quot;builderPassword&quot;: &quot;somePassword&quot;,
+        &quot;platform&quot;: &quot;Mountain Lion&quot;,
+        &quot;tests&quot;: {
+            &quot;Suite&quot;: {
+                &quot;tests&quot;: {
+                    &quot;test1&quot;: {
+                        &quot;metrics&quot;: {&quot;Time&quot;: { &quot;current&quot;: [1, 2, 3, 4, 5] }}
+                    },
+                }
+            },
+        }}];
+    reportWithBuildTime.startTime = +clusterTime(7);
+
+    var reportWithRevision = [{
+        &quot;buildNumber&quot;: &quot;124&quot;,
+        &quot;buildTime&quot;: &quot;2013-02-28T15:34:51&quot;,
+        &quot;revisions&quot;: {
+            &quot;WebKit&quot;: {
+                &quot;revision&quot;: &quot;144000&quot;,
+                &quot;timestamp&quot;: clusterTime(10.3).toISOString(),
+            },
+        },
+        &quot;builderName&quot;: &quot;someBuilder&quot;,
+        &quot;builderPassword&quot;: &quot;somePassword&quot;,
+        &quot;platform&quot;: &quot;Mountain Lion&quot;,
+        &quot;tests&quot;: {
+            &quot;Suite&quot;: {
+                &quot;tests&quot;: {
+                    &quot;test1&quot;: {
+                        &quot;metrics&quot;: {&quot;Time&quot;: { &quot;current&quot;: [11, 12, 13, 14, 15] }}
+                    }
+                }
+            },
+        }}];
+
+    var reportWithNewRevision = [{
+        &quot;buildNumber&quot;: &quot;125&quot;,
+        &quot;buildTime&quot;: &quot;2013-02-28T21:45:17&quot;,
+        &quot;revisions&quot;: {
+            &quot;WebKit&quot;: {
+                &quot;revision&quot;: &quot;160609&quot;,
+                &quot;timestamp&quot;: clusterTime(12.1).toISOString()
+            },
+        },
+        &quot;builderName&quot;: &quot;someBuilder&quot;,
+        &quot;builderPassword&quot;: &quot;somePassword&quot;,
+        &quot;platform&quot;: &quot;Mountain Lion&quot;,
+        &quot;tests&quot;: {
+            &quot;Suite&quot;: {
+                &quot;tests&quot;: {
+                    &quot;test1&quot;: {
+                        &quot;metrics&quot;: {&quot;Time&quot;: { &quot;current&quot;: [16, 17, 18, 19, 20] }}
+                    }
+                }
+            },
+        }}];
+
+    var reportWithAncentRevision = [{
+        &quot;buildNumber&quot;: &quot;126&quot;,
+        &quot;buildTime&quot;: &quot;2013-02-28T23:07:25&quot;,
+        &quot;revisions&quot;: {
+            &quot;WebKit&quot;: {
+                &quot;revision&quot;: &quot;137793&quot;,
+                &quot;timestamp&quot;: clusterTime(1.8).toISOString()
+            },
+        },
+        &quot;builderName&quot;: &quot;someBuilder&quot;,
+        &quot;builderPassword&quot;: &quot;somePassword&quot;,
+        &quot;platform&quot;: &quot;Mountain Lion&quot;,
+        &quot;tests&quot;: {
+            &quot;Suite&quot;: {
+                &quot;tests&quot;: {
+                    &quot;test1&quot;: {
+                        &quot;metrics&quot;: {&quot;Time&quot;: { &quot;current&quot;: [21, 22, 23, 24, 25] }}
+                    }
+                }
+            },
+        }}];
+
+    it(&quot;should reject when platform ID is missing&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postJSON('/api/report/', reportWithBuildTime, function (response) {
+                assert.equal(response.statusCode, 200);
+                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
+                    httpGet('/api/measurement-set/?metric=' + metricId, function (response) {
+                        assert.notEqual(JSON.parse(response.responseText)['status'], 'InvalidMetric');
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it(&quot;should reject when metric ID is missing&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postJSON('/api/report/', reportWithBuildTime, function (response) {
+                assert.equal(response.statusCode, 200);
+                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId, function (response) {
+                        assert.notEqual(JSON.parse(response.responseText)['status'], 'InvalidPlatform');
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it(&quot;should reject an invalid platform name&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postJSON('/api/report/', reportWithBuildTime, function (response) {
+                assert.equal(response.statusCode, 200);
+                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + 'a&amp;metric=' + metricId, function (response) {
+                        assert.equal(JSON.parse(response.responseText)['status'], 'InvalidPlatform');
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it(&quot;should reject an invalid metric name&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postJSON('/api/report/', reportWithBuildTime, function (response) {
+                assert.equal(response.statusCode, 200);
+                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId + 'b', function (response) {
+                        assert.equal(JSON.parse(response.responseText)['status'], 'InvalidMetric');
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it(&quot;should be able to retrieve a reported value&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postJSON('/api/report/', reportWithBuildTime, function (response) {
+                assert.equal(response.statusCode, 200);
+                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId, function (response) {
+                        try {
+                            var paresdResult = JSON.parse(response.responseText);
+                        } catch (error) {
+                            assert.fail(error, null, response.responseText);
+                        }
+
+                        var buildTime = +(new Date(reportWithBuildTime[0]['buildTime']));
+
+                        assert.deepEqual(Object.keys(paresdResult).sort(),
+                            ['clusterCount', 'clusterSize', 'clusterStart',
+                              'configurations', 'elapsedTime', 'endTime', 'formatMap', 'lastModified', 'startTime', 'status']);
+                        assert.equal(paresdResult['status'], 'OK');
+                        assert.equal(paresdResult['clusterCount'], 1);
+                        assert.deepEqual(paresdResult['formatMap'], [
+                            'id', 'mean', 'iterationCount', 'sum', 'squareSum', 'markedOutlier',
+                            'revisions', 'commitTime', 'build', 'buildTime', 'buildNumber', 'builder']);
+
+                        assert.equal(paresdResult['startTime'], reportWithBuildTime.startTime);
+
+                        assert.deepEqual(Object.keys(paresdResult['configurations']), ['current']);
+
+                        var currentRows = paresdResult['configurations']['current'];
+                        assert.equal(currentRows.length, 1);
+                        assert.equal(currentRows[0].length, paresdResult['formatMap'].length);
+                        assert.deepEqual(format(paresdResult['formatMap'], currentRows[0]), {
+                            mean: 3,
+                            iterationCount: 5,
+                            sum: 15,
+                            squareSum: 55,
+                            markedOutlier: false,
+                            revisions: [],
+                            commitTime: buildTime,
+                            buildTime: buildTime,
+                            buildNumber: '123'});
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it(&quot;should return return the right IDs for measurement, build, and builder&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postJSON('/api/report/', reportWithBuildTime, function (response) {
+                assert.equal(response.statusCode, 200);
+                assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+                queryPlatformAndMetric('Mountain Lion', 'Time', function (platformId, metricId) {
+                    queryAndFetchAll('SELECT * FROM test_runs', [], function (runs) {
+                        assert.equal(runs.length, 1);
+                        var measurementId = runs[0]['run_id'];
+                        queryAndFetchAll('SELECT * FROM builds', [], function (builds) {
+                            assert.equal(builds.length, 1);
+                            var buildId = builds[0]['build_id'];
+                            queryAndFetchAll('SELECT * FROM builders', [], function (builders) {
+                                assert.equal(builders.length, 1);
+                                var builderId = builders[0]['builder_id'];
+                                httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId, function (response) {
+                                    var paresdResult = JSON.parse(response.responseText);
+                                    assert.equal(paresdResult['configurations']['current'].length, 1);
+                                    var measurement = paresdResult['configurations']['current'][0];
+                                    assert.equal(paresdResult['status'], 'OK');
+                                    assert.equal(measurement[paresdResult['formatMap'].indexOf('id')], measurementId);
+                                    assert.equal(measurement[paresdResult['formatMap'].indexOf('build')], buildId);
+                                    assert.equal(measurement[paresdResult['formatMap'].indexOf('builder')], builderId);
+                                    notifyDone();
+                                });
+                            });
+                        });
+                    });
+                });
+            });
+        });
+    });
+
+    function postReports(reports, callback) {
+        if (!reports.length)
+            return callback();
+
+        postJSON('/api/report/', reports[0], function (response) {
+            assert.equal(response.statusCode, 200);
+            assert.equal(JSON.parse(response.responseText)['status'], 'OK');
+
+            postReports(reports.slice(1), callback);
+        });
+    }
+
+    function queryPlatformAndMetricWithRepository(platformName, metricName, repositoryName, callback) {
+        queryPlatformAndMetric(platformName, metricName, function (platformId, metricId) {
+            queryAndFetchAll('SELECT * FROM repositories WHERE repository_name = $1', [repositoryName], function (rows) {
+                callback(platformId, metricId, rows[0]['repository_id']);
+            });
+        });
+    }
+
+    it(&quot;should order results by commit time&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithBuildTime, reportWithRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId, function (response) {
+                        var parsedResult = JSON.parse(response.responseText);
+                        assert.equal(parsedResult['status'], 'OK');
+
+                        var buildTime = +(new Date(reportWithBuildTime[0]['buildTime']));
+                        var revisionTime = +(new Date(reportWithRevision[0]['revisions']['WebKit']['timestamp']));
+                        var revisionBuildTime = +(new Date(reportWithRevision[0]['buildTime']));
+
+                        var currentRows = parsedResult['configurations']['current'];
+                        assert.equal(currentRows.length, 2);
+                        assert.deepEqual(format(parsedResult['formatMap'], currentRows[0]), {
+                           mean: 13,
+                           iterationCount: 5,
+                           sum: 65,
+                           squareSum: 855,
+                           markedOutlier: false,
+                           revisions: [[repositoryId, '144000', revisionTime]],
+                           commitTime: revisionTime,
+                           buildTime: revisionBuildTime,
+                           buildNumber: '124' });
+                        assert.deepEqual(format(parsedResult['formatMap'], currentRows[1]), {
+                            mean: 3,
+                            iterationCount: 5,
+                            sum: 15,
+                            squareSum: 55,
+                            markedOutlier: false,
+                            revisions: [],
+                            commitTime: buildTime,
+                            buildTime: buildTime,
+                            buildNumber: '123' });
+                        notifyDone();
+                    });
+                });                
+            });
+        });
+    });
+
+    function buildNumbers(parsedResult, config) {
+        return parsedResult['configurations'][config].map(function (row) {
+            return format(parsedResult['formatMap'], row)['buildNumber'];
+        });
+    }
+
+    it(&quot;should include one data point after the current time range&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithAncentRevision, reportWithNewRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId, function (response) {
+                        var parsedResult = JSON.parse(response.responseText);
+                        assert.equal(parsedResult['status'], 'OK');
+                        assert.equal(parsedResult['clusterCount'], 2, 'should have two clusters');
+                        assert.deepEqual(buildNumbers(parsedResult, 'current'),
+                            [reportWithAncentRevision[0]['buildNumber'], reportWithNewRevision[0]['buildNumber']]);
+                        notifyDone();
+                    });
+                });                
+            });
+        });
+    });
+
+    // FIXME: This test assumes a cluster step of 2-3 months
+    it(&quot;should always include one old data point before the current time range&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithBuildTime, reportWithAncentRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId, function (response) {
+                        var parsedResult = JSON.parse(response.responseText);
+                        assert.equal(parsedResult['status'], 'OK');
+                        assert.equal(parsedResult['clusterCount'], 2, 'should have two clusters');
+
+                        var currentRows = parsedResult['configurations']['current'];
+                        assert.equal(currentRows.length, 2, 'should contain at least two data points');
+                        assert.deepEqual(buildNumbers(parsedResult, 'current'),
+                            [reportWithAncentRevision[0]['buildNumber'], reportWithBuildTime[0]['buildNumber']]);
+                        notifyDone();
+                    });
+                });                
+            });
+        });
+    });
+
+    // FIXME: This test assumes a cluster step of 2-3 months
+    it(&quot;should create cache results&quot;, function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithAncentRevision, reportWithRevision, reportWithNewRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&amp;metric=' + metricId, function (response) {
+                        var parsedResult = JSON.parse(response.responseText);
+                        assert.equal(parsedResult['status'], 'OK');
+                        var cachePrefix = '/data/measurement-set-' + platformId + '-' + metricId;
+                        httpGet(cachePrefix + '.json', function (response) {
+                            var parsedCachedResult = JSON.parse(response.responseText);
+                            assert.deepEqual(parsedResult, parsedCachedResult);
+
+                            httpGet(cachePrefix + '-' + parsedResult['startTime'] + '.json', function (response) {
+                                var parsedOldResult = JSON.parse(response.responseText);
+
+                                var oldBuildNumbers = buildNumbers(parsedOldResult, 'current');
+                                var newBuildNumbers = buildNumbers(parsedResult, 'current');
+                                assert(oldBuildNumbers.length &gt;= 2, 'The old cluster should contain at least two data points');
+                                assert(newBuildNumbers.length &gt;= 2, 'The new cluster should contain at least two data points');
+                                assert.deepEqual(oldBuildNumbers.slice(oldBuildNumbers.length - 2), newBuildNumbers.slice(0, 2),
+                                    'Two conseqcutive clusters should share two data points');
+
+                                notifyDone();
+                            });
+                        });
+                    });
+                });                
+            });
+        });
+    });
+
+});
</ins></span></pre>
</div>
</div>

</body>
</html>