power_LoadTest: wake up the base when test navigates to new page

The power difference between an autosuspended base & an always on
base on detachables is significant (so far), thus we autosuspend
the base. However, this would mask real power consumption if during
PLT the base was always autosuspended.
This CL wakes the base every time PLT navigates to a new page for
10 seconds. It also wakes the base for 1s every time PLT scrolls.
On non-detachables it does nothing.

This functionality requires the PLT extension to ping the test
every time something happens. However I assume that any overhead these
HTTP Requests might cause is overshadowed by the requests being sent
and received for the actual PLT page loading.

BUG=b:63933645
BUG=chromium:798684
TEST=test_that <ip> power_LoadTest.1hour
(on DUT)
watch -n 4 -d cat /sys/bus/usb/devices/1-2/power/runtime_active_time
> observe the value increase whenever scroll or page navigation happens

Change-Id: I2a25b351985383cb99710591516d330dc0df348e
Reviewed-on: https://chromium-review.googlesource.com/686066
Commit-Ready: Ruben Rodriguez Buchillon <coconutruben@chromium.org>
Tested-by: Ruben Rodriguez Buchillon <coconutruben@chromium.org>
Reviewed-by: Todd Broch <tbroch@chromium.org>
diff --git a/client/cros/httpd.py b/client/cros/httpd.py
index bbd387e..7525c6c 100644
--- a/client/cros/httpd.py
+++ b/client/cros/httpd.py
@@ -131,7 +131,10 @@
             e.set()
             del wait_urls[self.path]
         else:
-            logging.debug('URL %s not in watch list' % self.path)
+          if self.path not in self.server._urls:
+              # if the url is not in _urls, this means it was neither setup
+              # as a permanent, or event url.
+              logging.debug('URL %s not in watch list' % self.path)
 
 
     @_handle_http_errors
@@ -173,23 +176,51 @@
     def config_server(self, server, docroot, wait_urls, url_handlers):
         # Stuff some convenient data fields into the server object.
         self._server.docroot = docroot
+        self._server._urls = set()
         self._server._wait_urls = wait_urls
         self._server._url_handlers = url_handlers
         self._server._form_entries = {}
         self._server_thread = threading.Thread(
             target=self._server.serve_forever)
 
+    def add_url(self, url):
+        """
+          Add a url to the urls that the http server is actively watching for.
+
+          Not adding a url via add_url or add_wait_url, and only installing a
+          handler will still result in that handler being executed, but this
+          server will warn in the debug logs that it does not expect that url.
+
+          Args:
+            url (string): url suffix to listen to
+        """
+        self._server._urls.add(url)
 
     def add_wait_url(self, url='/', matchParams={}):
+        """
+          Add a wait url to the urls that the http server is aware of.
+
+          Not adding a url via add_url or add_wait_url, and only installing a
+          handler will still result in that handler being executed, but this
+          server will warn in the debug logs that it does not expect that url.
+
+          Args:
+            url (string): url suffix to listen to
+            matchParams (dictionary): an unused dictionary
+
+          Returns:
+            e, and event object. Call e.wait() on the object to wait (block)
+            until the server receives the first request for the wait url.
+
+        """
         e = threading.Event()
         self._server._wait_urls[url] = (matchParams, e)
+        self._server._urls.add(url)
         return e
 
-
     def add_url_handler(self, url, handler_func):
         self._server._url_handlers[url] = handler_func
 
-
     def clear_form_entries(self):
         self._server._form_entries = {}
 
diff --git a/client/cros/power/power_utils.py b/client/cros/power/power_utils.py
index 774d844..b240f84 100644
--- a/client/cros/power/power_utils.py
+++ b/client/cros/power/power_utils.py
@@ -910,3 +910,95 @@
         @returns dictionary of keyvals
         """
         return self._keyvals
+
+class BaseActivityException(Exception):
+  """ Class for base activity simulation exceptions. """
+
+class BaseActivitySimulator(object):
+  """ Class to simulate wake activity on the normally autosuspended base """
+
+  # Note on naming: throughout this class, the word base is used to mean the
+  # base of a detachable (keyboard, touchpad, etc).
+
+  _DETACHABLE_BOARDS = ['poppy', 'soraka']
+
+  # file defines where to look for detachable base.
+  # TODO(coconutruben): check when next wave of detachables come out if this
+  # structure still holds, or if we need to replace it by querying input
+  # devices.
+  _BASE_INIT_FILE = '/etc/init/hammerd.override'
+  _BASE_WAKE_TIME_MS = 10000
+
+  def __init__(self):
+      """ Initializer
+
+          Let the BaseActivitySimulator bootstrap itself by detecting if
+          the board is a detachable, and ensuring the base path exists.
+          Sets the base to autosuspend, and the autosuspend delay to be
+          at most _BASE_WAKE_TIME_MS.
+
+      """
+      board = utils.get_board()
+      self._should_run = board in self._DETACHABLE_BOARDS
+      if self._should_run:
+          if not os.path.exists(self._BASE_INIT_FILE):
+              raise BaseActivityException("Board %s marked as detachable, but "
+                                          "hammerd init file missing." % board)
+
+          with open(self._BASE_INIT_FILE, 'r') as init_file:
+              init_file_content = init_file.read()
+              try:
+                  bus = re.search('env USB_BUS=([0-9]*)',
+                                  init_file_content).group(1)
+                  port = re.search('env USB_PORT=([0-9]*)',
+                                  init_file_content).group(1)
+              except AttributeError:
+                  raise BaseActivityException("Failed to read usb bus "
+                                              "or port from hammerd file.")
+              base_power_path = ('/sys/bus/usb/devices/%s-%s/power/'
+                                 % (bus, port))
+              self._base_control_path =  os.path.join(base_power_path,
+                                                      'control')
+              self._autosuspend_delay_path = os.path.join(base_power_path,
+                                                         'autosuspend_delay_ms')
+          logging.debug("Base activity simulator will be running.")
+          with open(self._base_control_path, 'r+') as f:
+              self._default_control = f.read()
+              if self._default_control != 'auto':
+                  logging.debug("Putting the base into autosuspend.")
+                  f.write('auto')
+
+          with open(self._autosuspend_delay_path, 'r+') as f:
+              self._default_autosuspend_delay_ms = f.read().rstrip('\n')
+              f.write(str(self._BASE_WAKE_TIME_MS))
+
+  def wake_base(self, wake_time_ms=_BASE_WAKE_TIME_MS):
+      """
+          Wake up the base to simulate user activity.
+
+          Args:
+           wake_time_ms:  time the base should be turned on
+                          (taken out of autosuspend) in milliseconds.
+      """
+      if self._should_run:
+          logging.debug("Taking base out of runtime suspend for %d seconds",
+                        wake_time_ms/1000)
+          with open(self._autosuspend_delay_path, 'r+') as f:
+              f.write(str(wake_time_ms))
+          # Toggling the control will keep the base awake for
+          # the duration specified in the autosuspend_delay_ms file.
+          with open(self._base_control_path, 'w') as f:
+              f.write('on')
+          with open(self._base_control_path, 'w') as f:
+              f.write('auto')
+
+  def restore(self):
+      """
+          Restore the original control and autosuspend delay.
+      """
+      with open(self._base_control_path, 'w') as f:
+          f.write(self._default_control)
+
+      with open(self._autosuspend_delay_path, 'w') as f:
+          f.write(self._default_autosuspend_delay_ms)
+
diff --git a/client/site_tests/power_LoadTest/extension/ct.js b/client/site_tests/power_LoadTest/extension/ct.js
old mode 100755
new mode 100644
index 8e612fd..ab9df8c
--- a/client/site_tests/power_LoadTest/extension/ct.js
+++ b/client/site_tests/power_LoadTest/extension/ct.js
@@ -6,6 +6,14 @@
 
 var PLAY_MUSIC_HOSTNAME = 'play.google.com';
 
+function report_scrolling_to_test() {
+  //Sends message to PLT informing that user is scrolling on the browser.
+  var scroll_url = 'http://localhost:8001/scroll';
+  var req = new XMLHttpRequest();
+  req.open('GET', scroll_url, true);
+  req.send("");
+}
+
 //Sends message to the test.js(background script). test.js on
 //receiving a message from content script assumes the page has
 //loaded successfully. It further responds with instructions on
@@ -17,6 +25,7 @@
       lastOffset = window.pageYOffset;
       var start_interval = Math.max(10000, response.scroll_interval);
       function smoothScrollDown() {
+        report_scrolling_to_test();
         window.scrollBy(0, response.scroll_by);
         if (window.pageYOffset != lastOffset) {
           lastOffset = window.pageYOffset;
@@ -26,6 +35,7 @@
         }
       }
       function smoothScrollUp() {
+        report_scrolling_to_test();
         window.scrollBy(0, -1 * response.scroll_by);
         if (window.pageYOffset != lastOffset) {
           lastOffset = window.pageYOffset;
diff --git a/client/site_tests/power_LoadTest/extension/test.js b/client/site_tests/power_LoadTest/extension/test.js
index 595e80c..f2d6b46 100644
--- a/client/site_tests/power_LoadTest/extension/test.js
+++ b/client/site_tests/power_LoadTest/extension/test.js
@@ -14,7 +14,8 @@
 
 function setupTest() {
   //adding these listeners to track request failure codes
-  chrome.webRequest.onCompleted.addListener(capture_completed_status, {urls: ["<all_urls>"]});
+  chrome.webRequest.onCompleted.addListener(capture_completed_status,
+                                            {urls: ["<all_urls>"]});
   chrome.windows.getAll(null, function(windows) {
     preexisting_windows = windows;
     for (var i = 0; i < tasks.length; i++) {
@@ -47,9 +48,10 @@
     cycle = cycle_tabs[sender.tab.id];
     cycle.successful_loads++;
     url = get_active_url(cycle);
-   var page_load_time = end - page_load_time_counter[cycle.id]
-   page_load_times.push({'url': (unique_url_salt++) + url, 'time': page_load_time});
-   console.log(JSON.stringify(page_load_times));
+    var page_load_time = end - page_load_time_counter[cycle.id]
+    page_load_times.push({'url': (unique_url_salt++) + url,
+                          'time': page_load_time});
+    console.log(JSON.stringify(page_load_times));
     record_log_entry(dateToString(new Date()) + " [load success] " + url);
     if (request.action == "should_scroll" && cycle.focus) {
       sendResponse({"should_scroll": should_scroll,
@@ -62,6 +64,14 @@
   }
 }
 
+function report_page_nav_to_test() {
+  //Sends message to PLT informing that user is navigating to new page.
+  var ping_url = 'http://localhost:8001/pagenav';
+  var req = new XMLHttpRequest();
+  req.open('GET', ping_url, true);
+  req.send("");
+}
+
 function capture_completed_status(details) {
   var tabId = details.tabId;
   if (!(details.tabId in error_codes)) {
@@ -90,6 +100,7 @@
   var start = Date.now();
   page_load_time_counter[cycle.id] = start;
   chrome.tabs.update(cycle.id, {'url': url, 'selected': true});
+  report_page_nav_to_test()
   cycle.idx = (cycle.idx + 1) % cycle.urls.length;
   if (cycle.timeout < cycle.delay / time_ratio && cycle.timeout > 0) {
     cycle.timer = setTimeout(cycle_check_timeout, cycle.timeout, cycle);
@@ -99,7 +110,8 @@
 }
 
 function record_error_codes(cycle) {
-  var error_report = dateToString(new Date()) + " [load failure details] " + get_active_url(cycle) + "\n";
+  var error_report = dateToString(new Date()) + " [load failure details] "
+                     + get_active_url(cycle) + "\n";
   var reports = error_codes[cycle.id];
   for (var i = 0; i < reports.length; i++) {
     report = reports[i];
@@ -117,7 +129,8 @@
   if (cycle.id in cycle_tabs) {
     cycle.failed_loads++;
     record_error_codes(cycle);
-    record_log_entry(dateToString(new Date()) + " [load failure] " + get_active_url(cycle));
+    record_log_entry(dateToString(new Date()) + " [load failure] " +
+                                  get_active_url(cycle));
     cycle_navigate(cycle);
   } else {
     cycle.timer = setTimeout(cycle_navigate,
diff --git a/client/site_tests/power_LoadTest/power_LoadTest.py b/client/site_tests/power_LoadTest/power_LoadTest.py
index 2be2e5c..a509b62 100755
--- a/client/site_tests/power_LoadTest/power_LoadTest.py
+++ b/client/site_tests/power_LoadTest/power_LoadTest.py
@@ -276,6 +276,7 @@
         arc_mode = arc_common.ARC_MODE_DISABLED
         if utils.is_arc_available():
             arc_mode = arc_common.ARC_MODE_ENABLED
+        self._detachable_handler = power_utils.BaseActivitySimulator()
 
         try:
             self._browser = chrome.Chrome(extension_paths=[ext_path],
@@ -324,9 +325,24 @@
             pagelt_tracking = self._testServer.add_wait_url(url='/pagelt')
 
             self._testServer.add_url_handler(url='/pagelt',\
-                handler_func=(lambda handler, forms, tracker=self, loop_counter=i:\
-                    _extension_page_load_info_handler(handler, forms, loop_counter, self)))
+                handler_func=(lambda handler, forms, tracker=self,
+                              loop_counter=i:\
+                    _extension_page_load_info_handler(handler, forms,
+                                                      loop_counter, self)))
 
+            # setup a handler to simulate waking up the base of a detachable
+            # on user interaction. On scrolling, wake for 1s, on page
+            # navigation, wake for 10s.
+            self._testServer.add_url(url='/pagenav')
+            self._testServer.add_url(url='/scroll')
+
+            self._testServer.add_url_handler(url='/pagenav',
+                handler_func=(lambda handler, args, plt=self:
+                              plt._detachable_handler.wake_base(10000)))
+
+            self._testServer.add_url_handler(url='/scroll',
+                handler_func=(lambda handler, args, plt=self:
+                              plt._detachable_handler.wake_base(1000)))
             # reset backlight level since powerd might've modified it
             # based on ambient light
             self._set_backlight_level()
@@ -489,6 +505,7 @@
             self._backlight.restore()
         if self._services:
             self._services.restore_services()
+        self._detachable_handler.restore()
 
         # cleanup backchannel interface
         # Prevent wifi congestion in test lab by forcing machines to forget the
@@ -552,7 +569,6 @@
 
         return low_battery
 
-
     def _set_backlight_level(self):
         self._backlight.set_default()
         # record brightness level