power_LoadTest: Track tab usage, # of videos playing and audio playing

When something goes wrong it is very hard to figure out what happened.
This change adds monitoring for chrome. It will show how much CPU each
process is using, which tabs are associated with which processes, and
how much time is spent playing video and audio.

The updated callback happens every few seconds, so adding the monitoring
by default shouldn't cause any significant impact to battery life.

BUG=b:110872956
TEST=Ran power_LoadTest. Saw that the task-monitor file was created.
Verified the audible and video tags were set correctly.

Change-Id: I9ae7f1157f02ba727ca4f3e298594e4f3ae09ccf
Signed-off-by: Raul E Rangel <rrangel@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1440187
Reviewed-by: Derek Basehore <dbasehore@chromium.org>
Commit-Queue: Pranay Shoroff <pshoroff@google.com>
Tested-by: Pranay Shoroff <pshoroff@google.com>
diff --git a/client/site_tests/power_LoadTest/extension/bg.html b/client/site_tests/power_LoadTest/extension/bg.html
old mode 100755
new mode 100644
index 7eaa6b5..8742a24
--- a/client/site_tests/power_LoadTest/extension/bg.html
+++ b/client/site_tests/power_LoadTest/extension/bg.html
@@ -15,6 +15,9 @@
 <script src='/params.js'>
 </script>
 
+<script src='/task_monitor.js'>
+</script>
+
 <script src='/test.js'>
 </script>
 
diff --git a/client/site_tests/power_LoadTest/extension/ct.js b/client/site_tests/power_LoadTest/extension/ct.js
index acea6c4..579ef45 100644
--- a/client/site_tests/power_LoadTest/extension/ct.js
+++ b/client/site_tests/power_LoadTest/extension/ct.js
@@ -14,6 +14,25 @@
   req.send("");
 }
 
+
+chrome.runtime.onMessage.addListener(
+  function(message, sender, callback) {
+    if (message == "numberOfVideosPlaying") {
+      callback(numberOfVideosPlaying());
+    }
+ });
+
+function numberOfVideosPlaying() {
+  let number_of_videos_playing = 0;
+  for (let video of document.querySelectorAll('video')) {
+    if (!video.paused) {
+      number_of_videos_playing++;
+    }
+  }
+
+  return number_of_videos_playing;
+}
+
 //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
diff --git a/client/site_tests/power_LoadTest/extension/manifest.json b/client/site_tests/power_LoadTest/extension/manifest.json
old mode 100755
new mode 100644
index 1de1035..568b543
--- a/client/site_tests/power_LoadTest/extension/manifest.json
+++ b/client/site_tests/power_LoadTest/extension/manifest.json
@@ -10,7 +10,7 @@
   "icons": { "48": "skin/chrome_bug.png",
           "128": "skin/chrome_bug.png" },
   "permissions": [
-    "webRequest", "http://*/", "https://*/", "tabs"
+    "webRequest", "http://*/", "https://*/", "tabs", "processes"
   ],
   "background": {
     "persistent": true,
diff --git a/client/site_tests/power_LoadTest/extension/task_monitor.js b/client/site_tests/power_LoadTest/extension/task_monitor.js
new file mode 100644
index 0000000..b893158
--- /dev/null
+++ b/client/site_tests/power_LoadTest/extension/task_monitor.js
@@ -0,0 +1,129 @@
+/** Convert chrome tabs API to promise */
+const chromeTabs = {
+  get: function(tabId) {
+    return new Promise(function(resolve) {
+      chrome.tabs.get(tabId, resolve);
+    });
+  },
+
+  sendMessage: function(tabId, message, options) {
+    return new Promise(function(resolve) {
+      chrome.tabs.sendMessage(tabId, message, options, resolve);
+    });
+  },
+};
+
+class TaskMonitor {
+  constructor() {
+    // Bind the callback
+    this._updated = this._updated.bind(this);
+    this._records = [];
+  }
+
+  bind() {
+    chrome.processes.onUpdated.addListener(this._updated);
+  }
+
+  unbind() {
+    chrome.processes.onUpdated.removeListener(this._updated);
+  }
+
+  _updated(processes) {
+    // Structure to hold synthesized data
+    const data = {
+      timestamp: Date.now(),
+      processes: [],
+      tabInfo: [],
+    };
+
+    // Produce a set of tabId's to populate data.tabInfo in a separate loop
+    const tabset = new Set();
+
+    // Only take a subset of the process data
+    for (const process of Object.values(processes)) {
+      // Skip processes that don't consume CPU
+      if (process.cpu == 0) {
+        continue;
+      }
+
+      // Only take a subset of the data
+      const currProcess = {
+        pid: process.osProcessId,
+        network: process.network,
+        cpu: process.cpu,
+        tasks: [],
+      };
+
+      // Populate process info with 'tabId' and 'title' information
+      for (const task of Object.values(process.tasks)) {
+        if ('tabId' in task) {
+          tabset.add(task.tabId);
+          currProcess.tasks.push({
+            tabId: task.tabId,
+            title: task.title,
+          });
+        } else {
+          if ('title' in task) {
+            currProcess.tasks.push({title: task.title});
+          }
+        }
+      }
+      data.processes.push(currProcess);
+    }
+
+    // Populate data.tabInfo with unique tabId's
+    for (const tabId of tabset) {
+      data.tabInfo.push({tabId: tabId});
+    }
+
+    const promises = [];
+
+    // Record url, audio, muted, and video count
+    // information per currently open tab
+    for (const tabInfo of data.tabInfo) {
+      promises.push(
+          chromeTabs.get(tabInfo.tabId).then((tab) => {
+            tabInfo.title = tab.title;
+            tabInfo.url = tab.url;
+            tabInfo.audio_played = !!tab.audible;
+            tabInfo.muted = tab.mutedInfo.muted;
+          })
+      );
+
+      promises.push(
+          chromeTabs.sendMessage(tabInfo.tabId, 'numberOfVideosPlaying')
+              .then((n) => {
+                if (typeof n === 'undefined') {
+                  tabInfo.videos_playing = 0;
+                  return;
+                } else {
+                  tabInfo.videos_playing = n;
+                }
+              })
+      );
+    }
+
+    Promise.all(promises).finally(() => {
+      this._records.push(data);
+
+      if (this._records.length >= 1) {
+        this._send();
+      }
+    });
+  }
+
+  _send() {
+    const formData = new FormData();
+    this._records.forEach(function(record, index) {
+      formData.append(index, JSON.stringify(record));
+    });
+    this._records.length = 0;
+
+    const url = 'http://localhost:8001/task-monitor';
+    const req = new XMLHttpRequest();
+    req.open('POST', url, true);
+    req.send(formData);
+  }
+}
+
+this.task_monitor = new TaskMonitor();
diff --git a/client/site_tests/power_LoadTest/extension/test.js b/client/site_tests/power_LoadTest/extension/test.js
index 600bc11..0014f3d 100644
--- a/client/site_tests/power_LoadTest/extension/test.js
+++ b/client/site_tests/power_LoadTest/extension/test.js
@@ -16,6 +16,8 @@
   //adding these listeners to track request failure codes
   chrome.webRequest.onCompleted.addListener(capture_completed_status,
                                             {urls: ["<all_urls>"]});
+  task_monitor.bind();
+
   chrome.windows.getAll(null, function(windows) {
     preexisting_windows = windows;
     for (var i = 0; i < tasks.length; i++) {
@@ -290,6 +292,7 @@
 
 function send_summary() {
   send_raw_page_time_info();
+  task_monitor.unbind();
   send_key_values();
   send_status();
   send_log_entries();
diff --git a/client/site_tests/power_LoadTest/power_LoadTest.py b/client/site_tests/power_LoadTest/power_LoadTest.py
index 61da3d4..0876085 100755
--- a/client/site_tests/power_LoadTest/power_LoadTest.py
+++ b/client/site_tests/power_LoadTest/power_LoadTest.py
@@ -241,6 +241,9 @@
             self._ah_charge_start = self._power_status.battery.charge_now
             self._wh_energy_start = self._power_status.battery.energy
 
+        self.task_monitor_file = open(os.path.join(self.resultsdir,
+                                      'task-monitor.json'), 'wt')
+
 
     def run_once(self):
         """Test main loop."""
@@ -353,6 +356,12 @@
                     _extension_key_values_handler(handler, forms,
                                                   loop_counter,
                                                   test_instance)))
+            self._testServer.add_url(url='/task-monitor')
+            self._testServer.add_url_handler(
+                url='/task-monitor',
+                handler_func=lambda handler, forms:
+                    self._extension_task_monitor_handler(handler, forms)
+            )
 
             # setup a handler to simulate waking up the base of a detachable
             # on user interaction. On scrolling, wake for 1s, on page
@@ -397,6 +406,7 @@
         self._tmp_keyvals['minutes_battery_life_tested'] = (t1 - t0) / 60
         self._tmp_keyvals.update(psr.get_keyvals())
 
+
     def postprocess_iteration(self):
         """Postprocess: write keyvals / log and send data to power dashboard."""
         def _log_stats(prefix, stats):
@@ -558,6 +568,9 @@
             self._services.restore_services()
         self._detachable_handler.restore()
 
+        if self.task_monitor_file:
+            self.task_monitor_file.close()
+
         # cleanup backchannel interface
         # Prevent wifi congestion in test lab by forcing machines to forget the
         # wifi AP we connected to at the start of the test.
@@ -620,6 +633,7 @@
 
         return low_battery
 
+
     def _set_backlight_level(self, loop=None):
         self._backlight.set_default()
         # record brightness level
@@ -743,6 +757,7 @@
         keyname = _loop_keyname(loop, 'percent_kbd_backlight')
         self._tmp_keyvals[keyname] = self._keyboard_backlight.get_percent()
 
+
     def _log_loop_checkpoint(self, loop, start, end):
         loop_str = _loop_prefix(loop)
         self._checkpoint_logger.checkpoint(loop_str, start, end)
@@ -783,6 +798,23 @@
             loop_section = '_' + loop_str + '_' + section
             self._checkpoint_logger.checkpoint(loop_section, s_start, s_end)
 
+
+    def _extension_task_monitor_handler(self, handler, form):
+        """
+        We use the httpd library to allow us to log chrome processes usage.
+        """
+        if form:
+            logging.debug("[task-monitor] got %d samples", len(form))
+            for idx in sorted(form.keys()):
+                json = form[idx].value
+                self.task_monitor_file.write(json)
+                self.task_monitor_file.write(",\n")
+                # we don't want to add url information to our keyvals.
+                # httpd adds them automatically so we remove them again
+                del handler.server._form_entries[idx]
+        handler.send_response(200)
+
+
 def alphanum_key(s):
     """ Turn a string into a list of string and numeric chunks. This enables a
         sort function to use this list as a key to sort alphanumeric strings
@@ -797,6 +829,7 @@
             pass
     return chunks
 
+
 def _extension_log_handler(handler, form, loop_number):
     """
     We use the httpd library to allow us to log whatever we
@@ -817,6 +850,7 @@
             # httpd adds them automatically so we remove them again
             del handler.server._form_entries[field]
 
+
 def _extension_page_time_info_handler(handler, form, loop_number,
                                       test_instance):
     page_timestamps = []
@@ -897,6 +931,7 @@
 
     logging.debug("%s\n", message)
 
+
 def _extension_key_values_handler(handler, form, loop_number,
                                       test_instance):
     if not form:
@@ -917,9 +952,11 @@
         # httpd adds them automatically so we remove them again
         del handler.server._form_entries[field]
 
+
 def _loop_prefix(loop):
     return "loop%02d" % loop
 
+
 def _loop_keyname(loop, keyname):
     if loop != None:
         return "%s_%s" % (_loop_prefix(loop), keyname)