rebaseline_server: start HTTP server immediately, auto-reload once results are available
authorepoger@google.com <epoger@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Thu, 5 Dec 2013 16:05:16 +0000 (16:05 +0000)
committerepoger@google.com <epoger@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81>
Thu, 5 Dec 2013 16:05:16 +0000 (16:05 +0000)
BUG=skia:1877
(SkipBuildbotRuns)

R=vandebo@chromium.org

Review URL: https://codereview.chromium.org/106453002

git-svn-id: http://skia.googlecode.com/svn/trunk@12509 2bbb7eff-a529-9590-31e7-b0007b416f81

gm/rebaseline_server/server.py
gm/rebaseline_server/static/loader.js

index 670c94d..e8dbb6b 100755 (executable)
@@ -62,6 +62,10 @@ MIME_TYPE_MAP = {'': 'application/octet-stream',
 DEFAULT_ACTUALS_DIR = '.gm-actuals'
 DEFAULT_PORT = 8888
 
+# How often (in seconds) clients should reload while waiting for initial
+# results to load.
+RELOAD_INTERVAL_UNTIL_READY = 10
+
 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
 
@@ -213,18 +217,24 @@ class Server(object):
           expected_root=EXPECTATIONS_DIR,
           generated_images_root=GENERATED_IMAGES_ROOT)
 
-  def _result_reloader(self):
-    """ Reload results at the appropriate interval.  This never exits, so it
-    should be run in its own thread.
+  def _result_loader(self, reload_seconds=0):
+    """ Call self.update_results(), either once or periodically.
+
+    Params:
+      reload_seconds: integer; if nonzero, reload results at this interval
+          (in which case, this method will never return!)
     """
-    while True:
-      time.sleep(self._reload_seconds)
-      self.update_results()
+    self.update_results()
+    logging.info('Initial results loaded. Ready for requests on %s' % self._url)
+    if reload_seconds:
+      while True:
+        time.sleep(reload_seconds)
+        self.update_results()
 
   def run(self):
-    self.update_results()
-    if self._reload_seconds:
-      thread.start_new_thread(self._result_reloader, ())
+    arg_tuple = (self._reload_seconds,)  # start_new_thread needs a tuple,
+                                         # even though it holds just one param
+    thread.start_new_thread(self._result_loader, arg_tuple)
 
     if self._export:
       server_address = ('', self._port)
@@ -237,8 +247,8 @@ class Server(object):
       host = '127.0.0.1'
       server_address = (host, self._port)
     http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler)
-    logging.info('Ready for requests on http://%s:%d' % (
-        host, http_server.server_port))
+    self._url = 'http://%s:%d' % (host, http_server.server_port)
+    logging.info('Listening for requests on %s' % self._url)
     http_server.serve_forever()
 
 
@@ -287,10 +297,34 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
       # HTTPServer, and then it could be available to the handler via
       # the handler's .server instance variable.
       results_obj = _SERVER.results
-      response_dict = results_obj.get_results_of_type(type)
-      time_updated = results_obj.get_timestamp()
+      if results_obj:
+        response_dict = self.package_results(results_obj, type)
+      else:
+        now = int(time.time())
+        response_dict = {
+            'header': {
+                'resultsStillLoading': True,
+                'timeUpdated': now,
+                'timeNextUpdateAvailable': now + RELOAD_INTERVAL_UNTIL_READY,
+            },
+        }
+      self.send_json_dict(response_dict)
+    except:
+      self.send_error(404)
+      raise
+
+  def package_results(self, results_obj, type):
+    """ Given a nonempty "results" object, package it as a response_dict
+    as needed within do_GET_results.
 
-      response_dict['header'] = {
+    Args:
+      results_obj: nonempty "results" object
+      type: string indicating which set of results to return;
+            must be one of the results.RESULTS_* constants
+    """
+    response_dict = results_obj.get_results_of_type(type)
+    time_updated = results_obj.get_timestamp()
+    response_dict['header'] = {
         # Timestamps:
         # 1. when this data was last updated
         # 2. when the caller should check back for new data (if ever)
@@ -315,11 +349,8 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 
         # Whether the service is accessible from other hosts.
         'isExported': _SERVER.is_exported,
-      }
-      self.send_json_dict(response_dict)
-    except:
-      self.send_error(404)
-      raise
+    }
+    return response_dict
 
   def do_GET_static(self, path):
     """ Handle a GET request for a file under the 'static' directory.
index 3b16e2e..ccd29c2 100644 (file)
@@ -40,7 +40,7 @@ Loader.filter(
 
 Loader.controller(
   'Loader.Controller',
-    function($scope, $http, $filter, $location) {
+    function($scope, $http, $filter, $location, $timeout) {
     $scope.windowTitle = "Loading GM Results...";
     var resultsToLoad = $location.search().resultsToLoad;
     $scope.loadingMessage = "Loading results of type '" + resultsToLoad +
@@ -53,66 +53,75 @@ Loader.controller(
      */
     $http.get("/results/" + resultsToLoad).success(
       function(data, status, header, config) {
-        $scope.loadingMessage = "Processing data, please wait...";
-
-        $scope.header = data.header;
-        $scope.categories = data.categories;
-        $scope.testData = data.testData;
-        $scope.sortColumn = 'weightedDiffMeasure';
-        $scope.showTodos = false;
-
-        $scope.showSubmitAdvancedSettings = false;
-        $scope.submitAdvancedSettings = {};
-        $scope.submitAdvancedSettings['reviewed-by-human'] = true;
-        $scope.submitAdvancedSettings['ignore-failure'] = false;
-        $scope.submitAdvancedSettings['bug'] = '';
-
-        // Create the list of tabs (lists into which the user can file each
-        // test).  This may vary, depending on isEditable.
-        $scope.tabs = [
-          'Unfiled', 'Hidden'
-        ];
-        if (data.header.isEditable) {
-          $scope.tabs = $scope.tabs.concat(
-              ['Pending Approval']);
+        if (data.header.resultsStillLoading) {
+          $scope.loadingMessage =
+              "Server is still loading initial results; will retry at " +
+              $scope.localTimeString(data.header.timeNextUpdateAvailable);
+          $timeout(
+              function(){location.reload();},
+              (data.header.timeNextUpdateAvailable * 1000) - new Date().getTime());
+        } else {
+          $scope.loadingMessage = "Processing data, please wait...";
+
+          $scope.header = data.header;
+          $scope.categories = data.categories;
+          $scope.testData = data.testData;
+          $scope.sortColumn = 'weightedDiffMeasure';
+          $scope.showTodos = false;
+
+          $scope.showSubmitAdvancedSettings = false;
+          $scope.submitAdvancedSettings = {};
+          $scope.submitAdvancedSettings['reviewed-by-human'] = true;
+          $scope.submitAdvancedSettings['ignore-failure'] = false;
+          $scope.submitAdvancedSettings['bug'] = '';
+
+          // Create the list of tabs (lists into which the user can file each
+          // test).  This may vary, depending on isEditable.
+          $scope.tabs = [
+            'Unfiled', 'Hidden'
+          ];
+          if (data.header.isEditable) {
+            $scope.tabs = $scope.tabs.concat(
+                ['Pending Approval']);
+          }
+          $scope.defaultTab = $scope.tabs[0];
+          $scope.viewingTab = $scope.defaultTab;
+
+          // Track the number of results on each tab.
+          $scope.numResultsPerTab = {};
+          for (var i = 0; i < $scope.tabs.length; i++) {
+            $scope.numResultsPerTab[$scope.tabs[i]] = 0;
+          }
+          $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
+
+          // Add index and tab fields to all records.
+          for (var i = 0; i < $scope.testData.length; i++) {
+            $scope.testData[i].index = i;
+            $scope.testData[i].tab = $scope.defaultTab;
+          }
+
+          // Arrays within which the user can toggle individual elements.
+          $scope.selectedItems = [];
+
+          // Sets within which the user can toggle individual elements.
+          $scope.hiddenResultTypes = {
+            'failure-ignored': true,
+            'no-comparison': true,
+            'succeeded': true,
+          };
+          $scope.allResultTypes = Object.keys(data.categories['resultType']);
+          $scope.hiddenConfigs = {};
+          $scope.allConfigs = Object.keys(data.categories['config']);
+
+          // Associative array of partial string matches per category.
+          $scope.categoryValueMatch = {};
+          $scope.categoryValueMatch.builder = "";
+          $scope.categoryValueMatch.test = "";
+
+          $scope.updateResults();
+          $scope.loadingMessage = "";
+          $scope.windowTitle = "Current GM Results";
         }
-        $scope.defaultTab = $scope.tabs[0];
-        $scope.viewingTab = $scope.defaultTab;
-
-        // Track the number of results on each tab.
-        $scope.numResultsPerTab = {};
-        for (var i = 0; i < $scope.tabs.length; i++) {
-          $scope.numResultsPerTab[$scope.tabs[i]] = 0;
-        }
-        $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
-
-        // Add index and tab fields to all records.
-        for (var i = 0; i < $scope.testData.length; i++) {
-          $scope.testData[i].index = i;
-          $scope.testData[i].tab = $scope.defaultTab;
-        }
-
-        // Arrays within which the user can toggle individual elements.
-        $scope.selectedItems = [];
-
-        // Sets within which the user can toggle individual elements.
-        $scope.hiddenResultTypes = {
-          'failure-ignored': true,
-          'no-comparison': true,
-          'succeeded': true,
-        };
-        $scope.allResultTypes = Object.keys(data.categories['resultType']);
-        $scope.hiddenConfigs = {};
-        $scope.allConfigs = Object.keys(data.categories['config']);
-
-        // Associative array of partial string matches per category.
-        $scope.categoryValueMatch = {};
-        $scope.categoryValueMatch.builder = "";
-        $scope.categoryValueMatch.test = "";
-
-        $scope.updateResults();
-        $scope.loadingMessage = "";
-        $scope.windowTitle = "Current GM Results";
       }
     ).error(
       function(data, status, header, config) {