Loading the perf dashboard takes multiple seconds
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 21 Feb 2015 01:28:34 +0000 (01:28 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 21 Feb 2015 01:28:34 +0000 (01:28 +0000)
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 ("last modified time"). 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 "useCache" 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.

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

13 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/admin/test-configurations.php
Websites/perf.webkit.org/public/api/runs.php
Websites/perf.webkit.org/public/include/db.php
Websites/perf.webkit.org/public/include/json-header.php
Websites/perf.webkit.org/public/include/manifest.php
Websites/perf.webkit.org/public/index.html
Websites/perf.webkit.org/public/v2/app.js
Websites/perf.webkit.org/public/v2/data.js
Websites/perf.webkit.org/public/v2/manifest.js
Websites/perf.webkit.org/run-tests.js
Websites/perf.webkit.org/tests/api-report.js

index 2305adf3722e4eb2f51465a6ffec0b75c1c9a0fd..064c923eca1c6df7e02ea0fee1a3ca810b31c196 100644 (file)
@@ -1,3 +1,85 @@
+2015-02-20  Ryosuke Niwa  <rniwa@webkit.org>
+
+        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 ("last modified time"). 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 "useCache" 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  <rniwa@webkit.org>
 
         REGRESSION(r180333): Analysis tasks can't be associated with bugs
index 6e51da94423d32d54ae73d178b186441da0d740c..1fc3cd0d05f1f470060dfadc2e3e86c34e2ca4e0 100644 (file)
@@ -35,9 +35,11 @@ CREATE TABLE platforms (
 
 CREATE TABLE repositories (
     repository_id serial PRIMARY KEY,
+    repository_parent integer REFERENCES repositories ON DELETE CASCADE,
     repository_name varchar(64) NOT NULL,
     repository_url varchar(1024),
-    repository_blame_url varchar(1024));
+    repository_blame_url varchar(1024),
+    CONSTRAINT repository_name_must_be_unique UNIQUE(repository_parent, repository_name));
 
 CREATE TABLE bug_trackers (
     tracker_id serial PRIMARY KEY,
@@ -121,6 +123,7 @@ CREATE TABLE test_configurations (
     config_platform integer NOT NULL REFERENCES platforms ON DELETE CASCADE,
     config_type test_configuration_type NOT NULL,
     config_is_in_dashboard boolean NOT NULL DEFAULT FALSE,
+    config_runs_last_modified timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
     CONSTRAINT configuration_must_be_unique UNIQUE(config_metric, config_platform, config_type));
 CREATE INDEX config_platform_index ON test_configurations(config_platform);
 
@@ -144,6 +147,20 @@ CREATE TABLE run_iterations (
     iteration_relative_time float,
     PRIMARY KEY (iteration_run, iteration_order));
 
+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();
+
 CREATE TABLE reports (
     report_id serial PRIMARY KEY,
     report_builder integer NOT NULL REFERENCES builders ON DELETE RESTRICT,
index ae3b978cd462b0c28c5b84c1c4fa3884a31764e0..c550cb239f591929230f0df05c1eb2a2ae56995d 100644 (file)
@@ -29,6 +29,8 @@ function add_run($metric_id, $platform_id, $type, $date, $mean) {
 
     $db->commit_transaction();
     notice("Added a baseline test run.");
+
+    regenerate_manifest();
 }
 
 function delete_run($run_id, $build_id) {
@@ -65,6 +67,8 @@ function delete_run($run_id, $build_id) {
     }
 
     $db->commit_transaction();
+
+    regenerate_manifest();
 }
 
 if ($db) {
index 3516de464a091d552392952dbf60ed3fc6acea50..83bc972b7fdd08f70221b14db7707215b9920cab 100644 (file)
@@ -37,7 +37,7 @@ function main($path) {
 
     $platform_id = intval($parts[0]);
     $metric_id = intval($parts[1]);
-    $config_rows = $db->query_and_fetch_all('SELECT config_id, config_type, config_platform, config_metric
+    $config_rows = $db->query_and_fetch_all('SELECT *
         FROM test_configurations WHERE config_metric = $1 AND config_platform = $2', array($metric_id, $platform_id));
     if (!$config_rows)
         exit_with_error('ConfigurationNotFound');
@@ -52,7 +52,7 @@ function main($path) {
         header("Cache-Control: maxage=$maxage");
     }
 
-    $generator = new RunsGenerator();
+    $generator = new RunsGenerator($config_rows);
 
     foreach ($config_rows as $config) {
         if ($test_group_id)
@@ -62,15 +62,28 @@ function main($path) {
         $generator->add_runs($config['config_type'], $raw_runs);
     }
 
-    exit_with_success($generator->results());
+    $content = success_json($generator->results());
+    if (!$test_group_id)
+        generate_data_file("$platform_id-$metric_id.json", $content);
+    echo $content;
 }
 
 class RunsGenerator {
-    function __construct() {
+    function __construct($config_rows) {
         $this->results = array();
+        $last_modified_times = array();
+        foreach ($config_rows as $row)
+            array_push($last_modified_times, Database::to_js_time($row['config_runs_last_modified']));
+        $this->last_modified = max($last_modified_times);
+        $this->start_time = microtime(true);
     }
 
-    function &results() { return $this->results; }
+    function results() {
+        return array(
+            'configurations' => &$this->results,
+            'lastModified' => $this->last_modified,
+            'elapsedTime' => microtime(true) - $this->start_time);
+    }
 
     function add_runs($name, $raw_runs) {
         $formatted_runs = array();
@@ -91,7 +104,7 @@ class RunsGenerator {
             'squareSum' => floatval($run['run_square_sum_cache']),
             'revisions' => self::parse_revisions_array($run['revisions']),
             'build' => $run['build_id'],
-            'buildTime' => strtotime($run['build_time']) * 1000,
+            'buildTime' => Database::to_js_time($run['build_time']),
             'buildNumber' => intval($run['build_number']),
             'builder' => $run['build_builder']);
     }
@@ -104,7 +117,7 @@ class RunsGenerator {
             $name_and_revision = explode(',', trim($item, '()'));
             if (!$name_and_revision[0])
                 continue;
-            $time = strtotime(trim($name_and_revision[2], '"')) * 1000;
+            $time = Database::to_js_time(trim($name_and_revision[2], '"'));
             $revisions[trim($name_and_revision[0], '"')] = array(trim($name_and_revision[1], '"'), $time);
         }
         return $revisions;
index 82ea1b9b5e0515f082a2c4fdda07e6bf146eed24..1917c094abe800ccea47bcfcaa7150bae296de85 100644 (file)
@@ -27,13 +27,21 @@ function array_set_default(&$array, $key, $default) {
 
 $_config = NULL;
 
+define('CONFIG_DIR', dirname(__FILE__) . '/../../');
+
 function config($key) {
     global $_config;
     if (!$_config)
-        $_config = json_decode(file_get_contents(dirname(__FILE__) . '/../../config.json'), true);
+        $_config = json_decode(file_get_contents(CONFIG_DIR . 'config.json'), true);
     return $_config[$key];
 }
 
+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);
+}
+
 if (config('debug')) {
     error_reporting(E_ALL | E_STRICT);
     ini_set('display_errors', 'On');
@@ -56,6 +64,10 @@ class Database
         return $value == 't';
     }
 
+    static function to_js_time($time_str) {
+        return strtotime($time_str) * 1000;
+    }
+
     function connect() {
         $databaseConfig = config('database');
         $this->connection = pg_connect('host=' . $databaseConfig['host'] . ' port=' . $databaseConfig['port']
index 7d54cfde3b2ced356a1c7eda3daac5e4ed99f4c8..e77f1060ce780a46425d4d45451a7e1d7aef6ef0 100644 (file)
@@ -13,15 +13,15 @@ function exit_with_error($status, $details = array()) {
     exit(1);
 }
 
-function echo_success($details = array()) {
+function success_json($details = array()) {
     $details['status'] = 'OK';
     merge_additional_details($details);
 
-    echo json_encode($details);
+    return json_encode($details);
 }
 
 function exit_with_success($details = array()) {
-    echo_success($details);
+    echo success_json($details);
     exit(0);
 }
 
index 50794a83915cbc495b81e4e9259d5ca09d4e6409..538410a021b021a0c6fe1cd101fb0789ec4ed681 100644 (file)
@@ -12,6 +12,8 @@ class ManifestGenerator {
     }
 
     function generate() {
+        $start_time = microtime(true);
+
         $config_table = $this->db->fetch_table('test_configurations');
         $platform_table = $this->db->fetch_table('platforms');
         $repositories_table = $this->db->fetch_table('repositories');
@@ -34,11 +36,14 @@ class ManifestGenerator {
             'bugTrackers' => $this->bug_trackers($repositories_table),
             'dashboards' => config('dashboards'),
         );
-        return $this->manifest;
+
+        $this->manifest['elapsedTime'] = (microtime(true) - $start_time) * 1000;
+
+        return TRUE;
     }
 
     function store() {
-        return file_put_contents(self::MANIFEST_PATH, json_encode($this->manifest));
+        return generate_data_file('manifest.json', json_encode($this->manifest));
     }
 
     private function tests() {
@@ -77,19 +82,37 @@ class ManifestGenerator {
                 if ($is_dashboard && !$this->db->is_true($config_row['config_is_in_dashboard']))
                     continue;
 
+                $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;
+
                 $platform = &array_ensure_item_has_array($platform_metrics, $config_row['config_platform']);
-                if (!in_array($config_row['config_metric'], $platform))
-                    array_push($platform, $config_row['config_metric']);
+                $metrics = &array_ensure_item_has_array($platform, 'metrics');
+                $last_modified = &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);
             }
         }
+        $configurations = array();
+        
         $platforms = array();
         if ($platform_table) {
             foreach ($platform_table as $platform_row) {
                 if ($this->db->is_true($platform_row['platform_hidden']))
                     continue;
                 $id = $platform_row['platform_id'];
-                if (array_key_exists($id, $platform_metrics))
-                    $platforms[$id] = array('name' => $platform_row['platform_name'], 'metrics' => $platform_metrics[$id]);
+                if (array_key_exists($id, $platform_metrics)) {
+                    $platforms[$id] = array(
+                        'name' => $platform_row['platform_name'],
+                        'metrics' => $platform_metrics[$id]['metrics'],
+                        'lastModified' => $platform_metrics[$id]['last_modified']);
+                }
             }
         }
         return $platforms;
index b37c27129316a54612d93a41420baf7825ba7ecc..e4e220750ea6903119c06f0f7d774226e8dbd4c6 100644 (file)
@@ -840,7 +840,8 @@ function fetchTest(repositories, builders, filename, platform, metric, callback)
         return runs;
     }
 
-    $.getJSON('api/runs/' + filename, function (data) {
+    $.getJSON('api/runs/' + filename, function (response) {
+        var data = response.configurations;
         callback(createRunAndResults(data.current), createRunAndResults(data.baseline), createRunAndResults(data.target));
     });
 }
index 91838d521bfdf4adb8cc24a0a582fee9f2e8deee..f5a74e05e2f12638a8a318cacabd0f8ad3d2c968 100755 (executable)
@@ -349,27 +349,41 @@ App.Pane = Ember.Object.extend({
         else if (!this._isValidId(metricId))
             this.set('failure', metricId ? 'Invalid metric id:' + metricId : 'Metric id was not specified');
         else {
-            var self = this;
-
-            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) === "string")
-                    self.set('failure', 'Failed to fetch the JSON with an error: ' + result);
-                else if (!result.platform)
-                    self.set('failure', 'Could not find the platform "' + platformId + '"');
-                else if (!result.metric)
-                    self.set('failure', 'Could not find the metric "' + metricId + '"');
-                else
-                    self.set('failure', 'An internal error');
-            });
-
+            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;
+
+                    useCache = false;
+                    App.Manifest.fetchRunsWithPlatformAndMetric(store, platformId, metricId, null, useCache)
+                        .then(updateChartData, handleErrors);
+                }, handleErrors);
             this.fetchAnalyticRanges();
         }
     }.observes('platformId', 'metricId').on('init'),
+    _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) === "string")
+            this.set('failure', 'Failed to fetch the JSON with an error: ' + result);
+        else if (!result.platform)
+            this.set('failure', 'Could not find the platform "' + platformId + '"');
+        else if (!result.metric)
+            this.set('failure', 'Could not find the metric "' + metricId + '"');
+        else
+            this.set('failure', 'An internal error');
+    },
     fetchAnalyticRanges: function ()
     {
         var platformId = this.get('platformId');
index 9e521130f258453a07217020fad275a8377c2ab0..f2ee899ab177c14d09f9f602d776702422501a39 100755 (executable)
@@ -325,27 +325,35 @@ RunsData.prototype.timeSeriesByBuildTime = function ()
 
 // FIXME: We need to devise a way to fetch runs in multiple chunks so that
 // we don't have to fetch the entire time series to just show the last 3 days.
-RunsData.fetchRuns = function (platformId, metricId, testGroupId)
+RunsData.fetchRuns = function (platformId, metricId, testGroupId, useCache)
 {
-    var filename = platformId + '-' + metricId + '.json';
+    var url = useCache ? '../data/' : '../api/runs/';
 
+    url += platformId + '-' + metricId + '.json';
     if (testGroupId)
-        filename += '?testGroup=' + testGroupId;
+        url += '?testGroup=' + testGroupId;
 
     return new Ember.RSVP.Promise(function (resolve, reject) {
-        $.getJSON('../api/runs/' + filename, function (data) {
-            if (data.status != 'OK') {
-                reject(data.status);
+        $.getJSON(url, function (response) {
+            if (response.status != 'OK') {
+                reject(response.status);
                 return;
             }
-            delete data.status;
+            delete response.status;
 
+            var data = response.configurations;
             for (var config in data)
                 data[config] = new RunsData(data[config]);
+            
+            if (response.lastModified)
+                response.lastModified = new Date(response.lastModified);
 
-            resolve(data);
+            resolve(response);
         }).fail(function (xhr, status, error) {
-            reject(xhr.status + (error ? ', ' + error : ''));
+            if (xhr.status == 404 && useCache)
+                resolve(null);
+            else
+                reject(xhr.status + (error ? ', ' + error : ''));
         })
     });
 }
index 7d93aefc42aa3d890f1d2a7df4115c0bfe28bb55..9d03bf232f83f94e62511bc8d9b62b6836affb21 100755 (executable)
@@ -34,7 +34,7 @@ App.Metric = App.NameLabelModel.extend({
     }.property('name', 'test'),
     fullName: function ()
     {
-        return this.get('path').join(' \u220b ') /* &ni; */
+        return this.get('path').join(' \u220b ') /* &in; */
             + ' : ' + this.get('label');
     }.property('path', 'label'),
 });
@@ -54,16 +54,31 @@ App.BugTracker = App.NameLabelModel.extend({
     repositories: DS.hasMany('repository'),
 });
 
+App.DateArrayTransform = DS.Transform.extend({
+    deserialize: function (serialized)
+    {
+        return serialized.map(function (time) { return new Date(time); });
+    }
+});
+
 App.Platform = App.NameLabelModel.extend({
     _metricSet: null,
     _testSet: null,
     metrics: DS.hasMany('metric'),
+    lastModified: DS.attr('dateArray'),
     containsMetric: function (metric)
     {
         if (!this._metricSet)
             this._metricSet = new Ember.Set(this.get('metrics'));
         return this._metricSet.contains(metric);
     },
+    lastModifiedTimeForMetric: function (metric)
+    {
+        var index = this.get('metrics').indexOf(metric);
+        if (index < 0)
+            return null;
+        return this.get('lastModified').objectAt(index);
+    },
     containsTest: function (test)
     {
         if (!this._testSet) {
@@ -279,49 +294,58 @@ App.Manifest = Ember.Controller.extend({
         dashboards.forEach(function (dashboard) { self._dashboardByName[dashboard.get('name')] = dashboard; });
         this._defaultDashboardName = dashboards.length ? dashboards[0].get('name') : null;
     },
-    fetchRunsWithPlatformAndMetric: function (store, platformId, metricId, testGroupId)
+    fetchRunsWithPlatformAndMetric: function (store, platformId, metricId, testGroupId, useCache)
     {
+        Ember.assert("Can't cache results for test groups", !(testGroupId && useCache));
+        var self = this;
         return Ember.RSVP.all([
-            RunsData.fetchRuns(platformId, metricId, testGroupId),
+            RunsData.fetchRuns(platformId, metricId, testGroupId, useCache),
             this.fetch(store),
         ]).then(function (values) {
-            var runs = values[0];
+            var response = values[0];
 
             var platform = App.Manifest.platform(platformId);
             var metric = App.Manifest.metric(metricId);
 
-            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' && 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 {
                 platform: platform,
                 metric: metric,
-                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 && !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
-                    },
-                    formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
-                    deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
-                    smallerIsBetter: smallerIsBetter,
-                }
+                data: response ? self._formatFetchedData(metric.get('name'), response.configurations) : null,
+                shouldRefetch: !response || +response.lastModified < +platform.lastModifiedTimeForMetric(metric),
             };
         });
     },
+    _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' && 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 && !isNaN(delta) ? ' \u00b1 ' + deltaFormatterWithoutSign(delta) : '') + unitSuffix;
+            },
+            formatter: useSI ? d3.format('.4s') : d3.format('.4g'),
+            deltaFormatter: useSI ? d3.format('+.2s') : d3.format('+.2g'),
+            smallerIsBetter: smallerIsBetter,
+        };
+    }
 }).create();
index f71c4199e8c59c030942ba2b04bd8dc9378ee026..a730f591e6356d5c07c791b63ab0df89738bf0da 100644 (file)
@@ -199,7 +199,7 @@ function initializeDatabase(client, callback) {
 
     var firstError;
     var queue = new TaskQueue();
-    commaSeparatedSqlStatements.split(/;\s*/).forEach(function (statement) {
+    commaSeparatedSqlStatements.split(/;\s*(?=CREATE|DROP)/).forEach(function (statement) {
         queue.addTask(function (error, callback) {
             client.query(statement, function (error) {
                 if (error && !firstError)
index 8a917f340e7132460fca69ae96bd0edf8d286e4d..d5945e8c9508fb5c1ec0e4fa7c16af22da6e78a4 100644 (file)
@@ -687,4 +687,73 @@ describe("/api/report", function () {
             });
         });
     });
+
+    var reportsUpdatingDifferentTests = [
+        [{
+            "buildNumber": "123",
+            "buildTime": "2013-02-28T10:12:03",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {"test1": {"metrics": {"Time": {"current": 3}}}}
+        }],
+        [{
+            "buildNumber": "124",
+            "buildTime": "2013-02-28T11:31:21",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {"test2": {"metrics": {"Time": {"current": 3}}}}
+        }],
+        [{
+            "buildNumber": "125",
+            "buildTime": "2013-02-28T12:45:34",
+            "builderName": "someBuilder",
+            "builderPassword": "somePassword",
+            "platform": "Mountain Lion",
+            "tests": {"test1": {"metrics": {"Time": {"current": 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("should update the last modified date of test configurations with new runs", 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("should update the last modified date of unrelated test configurations", 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();
+                        });
+                    });
+                });
+            });
+        });
+    });
 });