Add Pixel C knobs to skpbench

BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2369533002

Review-Url: https://codereview.chromium.org/2369533002
diff --git a/tools/skpbench/_benchresult.py b/tools/skpbench/_benchresult.py
index 3969b55..32d760c 100644
--- a/tools/skpbench/_benchresult.py
+++ b/tools/skpbench/_benchresult.py
@@ -3,7 +3,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-'''Parses an skpbench result from a line of output text.'''
+"""Parses an skpbench result from a line of output text."""
 
 from __future__ import print_function
 import re
diff --git a/tools/skpbench/_hardware.py b/tools/skpbench/_hardware.py
index 23cfc82..f1c8c26 100644
--- a/tools/skpbench/_hardware.py
+++ b/tools/skpbench/_hardware.py
@@ -5,12 +5,21 @@
 
 import time
 
-class HardwareException(Exception):
-  def __init__(self, message, sleeptime=60):
-    Exception.__init__(self, message)
-    self.sleeptime = sleeptime
-
 class Hardware:
+  """Locks down and monitors hardware for benchmarking.
+
+  This is a common base for classes that can control the specific hardware
+  we are running on. Its purpose is to lock the hardware into a constant
+  benchmarking mode for the duration of a 'with' block. e.g.:
+
+    with hardware:
+      run_benchmark()
+
+  While benchmarking, the caller must call sanity_check() frequently to verify
+  the hardware state has not changed.
+
+  """
+
   def __init__(self):
     self.kick_in_time = 0
 
@@ -21,9 +30,60 @@
     pass
 
   def sanity_check(self):
-    '''Raises a HardwareException if any hardware state is not as expected.'''
+    """Raises a HardwareException if any hardware state is not as expected."""
     pass
 
   def sleep(self, sleeptime):
-    '''Puts the hardware into a resting state for a fixed amount of time.'''
+    """Puts the hardware into a resting state for a fixed amount of time."""
     time.sleep(sleeptime)
+
+
+class HardwareException(Exception):
+  """Gets thrown when certain hardware state is not what we expect.
+
+  Generally this happens because of thermal conditions or other variables beyond
+  our control, and the appropriate course of action is to take a short nap
+  before resuming the benchmark.
+
+  """
+
+  def __init__(self, message, sleeptime=60):
+    Exception.__init__(self, message)
+    self.sleeptime = sleeptime
+
+
+class Expectation:
+  """Simple helper for checking the readings on hardware gauges."""
+  def __init__(self, value_type, min_value=None, max_value=None,
+               exact_value=None, name=None, sleeptime=60):
+    self.value_type = value_type
+    self.min_value = min_value
+    self.max_value = max_value
+    self.exact_value = exact_value
+    self.name = name
+    self.sleeptime = sleeptime
+
+  def check(self, stringvalue):
+    typedvalue = self.value_type(stringvalue)
+    if self.min_value is not None and typedvalue < self.min_value:
+       raise HardwareException("%s is too low (%s, min=%s)" %
+                               (self.name, stringvalue, str(self.min_value)),
+                               sleeptime=self.sleeptime)
+    if self.max_value is not None and typedvalue > self.max_value:
+       raise HardwareException("%s is too high (%s, max=%s)" %
+                               (self.name, stringvalue, str(self.max_value)),
+                               sleeptime=self.sleeptime)
+    if self.exact_value is not None and typedvalue != self.exact_value:
+       raise HardwareException("unexpected %s (%s, expected=%s)" %
+                               (self.name, stringvalue, str(self.exact_value)),
+                               sleeptime=self.sleeptime)
+
+  @staticmethod
+  def check_all(expectations, stringvalues):
+    if len(stringvalues) != len(expectations):
+      raise Exception("unexpected reading from hardware gauges "
+                      "(expected %i values):\n%s" %
+                      (len(expectations), '\n'.join(stringvalues)))
+
+    for value, expected in zip(stringvalues, expectations):
+      expected.check(value)
diff --git a/tools/skpbench/_hardware_pixel_c.py b/tools/skpbench/_hardware_pixel_c.py
new file mode 100644
index 0000000..3ea74c1
--- /dev/null
+++ b/tools/skpbench/_hardware_pixel_c.py
@@ -0,0 +1,94 @@
+# Copyright 2016 Google Inc.
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from _hardware import HardwareException, Expectation
+from _hardware_android import HardwareAndroid
+
+CPU_CLOCK_RATE = 1836000
+GPU_EMC_PROFILE = '0c: core 921 MHz emc 1600 MHz a A d D *'
+GPU_EMC_PROFILE_ID = '0c'
+
+class HardwarePixelC(HardwareAndroid):
+  def __init__(self, adb):
+    HardwareAndroid.__init__(self, adb)
+
+  def __enter__(self):
+    self._lock_clocks()
+    return HardwareAndroid.__enter__(self)
+
+  def __exit__(self, exception_type, exception_value, exception_traceback):
+    HardwareAndroid.__exit__(self, exception_type,
+                             exception_value, exception_traceback)
+    self._unlock_clocks()
+
+  def _lock_clocks(self):
+    if not self._is_root:
+      return
+
+    # lock cpu clocks.
+    self._adb.shell('''\
+      for N in $(seq 0 3); do
+        echo userspace > /sys/devices/system/cpu/cpu$N/cpufreq/scaling_governor
+        echo %i > /sys/devices/system/cpu/cpu$N/cpufreq/scaling_setspeed
+      done''' % CPU_CLOCK_RATE)
+
+    # lock gpu/emc clocks.
+    self._adb.shell('''\
+      chown root:root /sys/devices/57000000.gpu/pstate
+      echo %s > /sys/devices/57000000.gpu/pstate''' % GPU_EMC_PROFILE_ID)
+
+  def _unlock_clocks(self):
+    if not self._is_root:
+      return
+
+    # unlock gpu/emc clocks.
+    self._adb.shell('''\
+      echo auto > /sys/devices/57000000.gpu/pstate
+      chown system:system /sys/devices/57000000.gpu/pstate''')
+
+    # unlock cpu clocks.
+    self._adb.shell('''\
+      for N in $(seq 0 3); do
+        echo 0 > /sys/devices/system/cpu/cpu$N/cpufreq/scaling_setspeed
+        echo interactive > /sys/devices/system/cpu/cpu$N/cpufreq/scaling_governor
+      done''')
+
+  def sanity_check(self):
+    HardwareAndroid.sanity_check(self)
+
+    if not self._is_root:
+      return
+
+    # only issue one shell command in an attempt to minimize interference.
+    result = self._adb.check_lines('''\
+      cat /sys/class/power_supply/bq27742-0/capacity \
+          /sys/class/thermal/thermal_zone7/temp \
+          /sys/class/thermal/thermal_zone0/temp \
+          /sys/class/thermal/thermal_zone1/temp \
+          /sys/class/thermal/thermal_zone7/cdev1/cur_state \
+          /sys/class/thermal/thermal_zone7/cdev0/cur_state
+      for N in $(seq 0 3); do
+        cat /sys/devices/system/cpu/cpu$N/cpufreq/scaling_cur_freq
+      done
+      cat /sys/devices/57000000.gpu/pstate | grep \*$''')
+
+    expectations = \
+      [Expectation(int, min_value=30, name='battery', sleeptime=30*60),
+       Expectation(int, max_value=40000, name='skin temperature'),
+       Expectation(int, max_value=86000, name='cpu temperature'),
+       Expectation(int, max_value=87000, name='gpu temperature'),
+       Expectation(int, exact_value=0, name='cpu throttle'),
+       Expectation(int, exact_value=0, name='gpu throttle')] + \
+      [Expectation(int, exact_value=CPU_CLOCK_RATE,
+                   name='cpu_%i clock rate' % i, sleeptime=30)
+       for i in range(4)] + \
+      [Expectation(str, exact_value=GPU_EMC_PROFILE, name='gpu/emc profile')]
+
+    Expectation.check_all(expectations, result)
+
+  def sleep(self, sleeptime):
+    self._unlock_clocks()
+    HardwareAndroid.sleep(self, sleeptime)
+    self._lock_clocks()
diff --git a/tools/skpbench/parseskpbench.py b/tools/skpbench/parseskpbench.py
index 2481e1d..f903ec0 100755
--- a/tools/skpbench/parseskpbench.py
+++ b/tools/skpbench/parseskpbench.py
@@ -18,7 +18,7 @@
 import urlparse
 import webbrowser
 
-__argparse = ArgumentParser(description='''
+__argparse = ArgumentParser(description="""
 
 Parses output files from skpbench.py into csv.
 
@@ -31,7 +31,7 @@
 
 (3) Run parseskpbench.py with the --open flag.
 
-''')
+""")
 
 __argparse.add_argument('-r', '--result',
     choices=['median', 'accum', 'max', 'min'], default='median',
diff --git a/tools/skpbench/skpbench.py b/tools/skpbench/skpbench.py
index 6b226e0..320cdc1 100755
--- a/tools/skpbench/skpbench.py
+++ b/tools/skpbench/skpbench.py
@@ -20,14 +20,14 @@
 import sys
 import time
 
-__argparse = ArgumentParser(description='''
+__argparse = ArgumentParser(description="""
 
 Executes the skpbench binary with various configs and skps.
 
 Also monitors the output in order to filter out and re-run results that have an
 unacceptable stddev.
 
-''')
+""")
 
 __argparse.add_argument('--adb',
     action='store_true', help="execute skpbench over adb")
@@ -82,7 +82,7 @@
     Thread.__init__(self)
 
   def run(self):
-    '''Runs on the background thread.'''
+    """Runs on the background thread."""
     for line in iter(self._proc.stdout.readline, b''):
       self._queue.put(Message(Message.READLINE, line.decode('utf-8').rstrip()))
     self._queue.put(Message(Message.EXIT))
@@ -243,8 +243,9 @@
   if FLAGS.adb:
     adb = Adb(FLAGS.device_serial)
     model = adb.get_device_model()
-    if False:
-      pass # TODO: unique subclasses tailored to individual platforms.
+    if model == 'Pixel C':
+      from _hardware_pixel_c import HardwarePixelC
+      hardware = HardwarePixelC(adb)
     else:
       from _hardware_android import HardwareAndroid
       print("WARNING: %s: don't know how to monitor this hardware; results "