Add /api/measurement-set for v3 UI
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 15 Dec 2015 23:57:25 +0000 (23:57 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 15 Dec 2015 23:57:25 +0000 (23:57 +0000)
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 "cluster" 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=<platform-id>&metric=<metric-id>.
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-<platform-id>-<metric-id>-<cluster-end-time>.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-<platform-id>-<metric-id>.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.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@194120 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/config.json
Websites/perf.webkit.org/public/api/measurement-set.php [new file with mode: 0644]
Websites/perf.webkit.org/tests/api-measurement-set.js [new file with mode: 0644]

index a9671b8..eda716c 100644 (file)
@@ -1,3 +1,47 @@
+2015-12-15  Ryosuke Niwa  <rniwa@webkit.org>
+
+        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 "cluster" 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=<platform-id>&metric=<metric-id>.
+        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-<platform-id>-<metric-id>-<cluster-end-time>.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-<platform-id>-<metric-id>.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.
+
 2015-12-14  Ryosuke Niwa  <rniwa@webkit.org>
 
         Using fake timestamp in OS version make some results invisible
index 8991e55..913dd21 100644 (file)
@@ -13,6 +13,8 @@
         "hostname": "localhost",
         "port": 80
     },
+    "clusterStart": [2000, 1, 1, 0, 0],
+    "clusterSize": [0, 2, 0],
     "cacheDirectory": "public/data/remote-cache/",
     "remoteServer": {
         "httpdConfig": "tools/remote-server-relay.conf",
diff --git a/Websites/perf.webkit.org/public/api/measurement-set.php b/Websites/perf.webkit.org/public/api/measurement-set.php
new file mode 100644 (file)
index 0000000..bee5d8a
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+
+require('../include/json-header.php');
+
+function main() {
+    $program_start_time = microtime(true);
+
+    $arguments = validate_arguments($_GET, array(
+        'platform' => 'int',
+        'metric' => 'int',
+        'testGroup' => 'int?',
+        'startTime' => 'int?',
+        'endTime' => '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' => $start_time, 'endTime' => $end_time));
+
+    $db = new Database;
+    if (!$db->connect())
+        exit_with_error('DatabaseConnectionFailure');
+
+    $fetcher = new MeasurementSetFetcher($db);
+    if (!$fetcher->fetch_config_list($platform_id, $metric_id)) {
+        exit_with_error('ConfigurationNotFound',
+            array('platform' => $platform_id, 'metric' => $metric_id));
+    }
+
+    $cluster_count = 0;
+    while (!$fetcher->at_end()) {
+        $content = $fetcher->fetch_next_cluster();
+        $cluster_count++;
+        if ($fetcher->at_end()) {
+            $cache_filename = "measurement-set-$platform_id-$metric_id.json";
+            $content['clusterCount'] = $cluster_count;
+            $content['elapsedTime'] = (microtime(true) - $program_start_time) * 1000;
+        } else
+            $cache_filename = "measurement-set-$platform_id-$metric_id-{$content['endTime']}.json";
+
+        $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->db = $db;
+        $this->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->cluster_start = mktime($start_time[3], $start_time[4], 0, $start_time[1], $start_time[2], $start_time[0]) * 1000;
+        $this->next_cluster_start = $this->cluster_start;
+        $this->next_cluster_results = NULL;
+        $this->cluster_size = $size[0] * YEAR + $size[1] * MONTH + $size[2] * DAY;
+        $this->last_modified = 0;
+
+        $this->start_time = microtime(TRUE);
+    }
+
+    function fetch_config_list($platform_id, $metric_id) {
+        $config_rows = $this->db->query_and_fetch_all('SELECT *
+            FROM test_configurations WHERE config_metric = $1 AND config_platform = $2',
+            array($metric_id, $platform_id));
+        $this->config_rows = $config_rows;
+        if (!$config_rows)
+            return FALSE;
+
+        $this->queries = array();
+        $this->next_cluster_results = array();
+        $min_commit_time = microtime(TRUE) * 1000;
+        foreach ($config_rows as &$config_row) {
+            $query = $this->execute_query($config_row['config_id']);
+
+            $this->last_modified = max($this->last_modified, $config_row['config_runs_last_modified']);
+
+            $measurement_row = $this->db->fetch_next_row($query);
+            if ($measurement_row) {
+                $commit_time = 0;
+                $formatted_row = self::format_run($measurement_row, $commit_time);
+                $this->next_cluster_results[$config_row['config_type']] = array($formatted_row);
+                $min_commit_time = min($min_commit_time, $commit_time);
+            } else
+                $query = NULL;
+
+            $this->queries[$config_row['config_type']] = $query;
+        }
+
+        while ($this->next_cluster_start + $this->cluster_size < $min_commit_time)
+            $this->next_cluster_start += $this->cluster_size;
+
+        return TRUE;
+    }
+
+    function at_end() {
+        if ($this->queries === NULL)
+            return FALSE;
+        foreach ($this->queries as $name => &$query) {
+            if ($query)
+                return FALSE;
+        }
+        return TRUE;
+    }
+    
+    function fetch_next_cluster() {
+        assert($this->queries);
+
+        $results_by_config = array();
+        $current_cluster_start = $this->next_cluster_start;
+        $this->next_cluster_start += $this->cluster_size;
+
+        foreach ($this->queries as $name => &$query) {
+            assert($this->next_cluster_start);
+
+            $carry_over = array_get($this->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->db->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 > $this->next_cluster_start;
+                if ($row_belongs_to_next_cluster)
+                    break;
+            }
+
+            $reached_end = !$row;
+            if ($reached_end)
+                $this->queries[$name] = NULL;
+            else {
+                $this->next_cluster_results[$name] = array_slice($results_by_config[$name], -2);
+            }
+        }
+
+        return array(
+            'clusterStart' => $this->cluster_start,
+            'clusterSize' => $this->cluster_size,
+            'configurations' => &$results_by_config,
+            'formatMap' => self::format_map(),
+            'startTime' => $current_cluster_start,
+            'endTime' => $this->next_cluster_start,
+            'lastModified' => $this->last_modified);
+    }
+
+    function execute_query($config_id) {
+        return $this->db->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, &$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. {"(WebKit,131456,\"2012-10-16 14:53:00\")","(Chromium,162004,)"}
+        $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], '"'));
+            array_push($revisions, array(intval(trim($name_and_revision[0], '"')), trim($name_and_revision[1], '"'), $time));
+        }
+        return $revisions;
+    }
+}
+
+main();
+
+?>
diff --git a/Websites/perf.webkit.org/tests/api-measurement-set.js b/Websites/perf.webkit.org/tests/api-measurement-set.js
new file mode 100644 (file)
index 0000000..b9eb3c2
--- /dev/null
@@ -0,0 +1,403 @@
+describe("/api/measurement-set", 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 < 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 = [{
+        "buildNumber": "123",
+        "buildTime": clusterTime(7.8).toISOString(),
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [1, 2, 3, 4, 5] }}
+                    },
+                }
+            },
+        }}];
+    reportWithBuildTime.startTime = +clusterTime(7);
+
+    var reportWithRevision = [{
+        "buildNumber": "124",
+        "buildTime": "2013-02-28T15:34:51",
+        "revisions": {
+            "WebKit": {
+                "revision": "144000",
+                "timestamp": clusterTime(10.3).toISOString(),
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [11, 12, 13, 14, 15] }}
+                    }
+                }
+            },
+        }}];
+
+    var reportWithNewRevision = [{
+        "buildNumber": "125",
+        "buildTime": "2013-02-28T21:45:17",
+        "revisions": {
+            "WebKit": {
+                "revision": "160609",
+                "timestamp": clusterTime(12.1).toISOString()
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [16, 17, 18, 19, 20] }}
+                    }
+                }
+            },
+        }}];
+
+    var reportWithAncentRevision = [{
+        "buildNumber": "126",
+        "buildTime": "2013-02-28T23:07:25",
+        "revisions": {
+            "WebKit": {
+                "revision": "137793",
+                "timestamp": clusterTime(1.8).toISOString()
+            },
+        },
+        "builderName": "someBuilder",
+        "builderPassword": "somePassword",
+        "platform": "Mountain Lion",
+        "tests": {
+            "Suite": {
+                "tests": {
+                    "test1": {
+                        "metrics": {"Time": { "current": [21, 22, 23, 24, 25] }}
+                    }
+                }
+            },
+        }}];
+
+    it("should reject when platform ID is missing", 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("should reject when metric ID is missing", 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("should reject an invalid platform name", 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&metric=' + metricId, function (response) {
+                        assert.equal(JSON.parse(response.responseText)['status'], 'InvalidPlatform');
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it("should reject an invalid metric name", 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 + '&metric=' + metricId + 'b', function (response) {
+                        assert.equal(JSON.parse(response.responseText)['status'], 'InvalidMetric');
+                        notifyDone();
+                    });
+                });
+
+            });
+        });
+    });
+
+    it("should be able to retrieve a reported value", 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 + '&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("should return return the right IDs for measurement, build, and builder", 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 + '&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("should order results by commit time", function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithBuildTime, reportWithRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&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("should include one data point after the current time range", function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithAncentRevision, reportWithNewRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&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("should always include one old data point before the current time range", function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithBuildTime, reportWithAncentRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&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("should create cache results", function () {
+        addBuilder(reportWithBuildTime, function () {
+            postReports([reportWithAncentRevision, reportWithRevision, reportWithNewRevision], function () {
+                queryPlatformAndMetricWithRepository('Mountain Lion', 'Time', 'WebKit', function (platformId, metricId, repositoryId) {
+                    httpGet('/api/measurement-set/?platform=' + platformId + '&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 >= 2, 'The old cluster should contain at least two data points');
+                                assert(newBuildNumbers.length >= 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();
+                            });
+                        });
+                    });
+                });                
+            });
+        });
+    });
+
+});