OLD | NEW |
(Empty) | |
| 1 # Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import motopho_thread as mt |
| 6 import robot_arm as ra |
| 7 |
| 8 import json |
| 9 import glob |
| 10 import logging |
| 11 import numpy |
| 12 import os |
| 13 import re |
| 14 import subprocess |
| 15 import sys |
| 16 import time |
| 17 |
| 18 |
| 19 MOTOPHO_THREAD_TIMEOUT = 30 |
| 20 |
| 21 |
| 22 def GetTtyDevices(tty_pattern, vendor_ids): |
| 23 """Finds all devices connected to tty that match a pattern and device id. |
| 24 |
| 25 If a serial device is connected to the computer via USB, this function |
| 26 will check all tty devices that match tty_pattern, and return the ones |
| 27 that have vendor identification number in the list vendor_ids. |
| 28 |
| 29 Args: |
| 30 tty_pattern: The search pattern, such as r'ttyACM\d+'. |
| 31 vendor_ids: The list of 16-bit USB vendor ids, such as [0x2a03]. |
| 32 |
| 33 Returns: |
| 34 A list of strings of tty devices, for example ['ttyACM0']. |
| 35 """ |
| 36 product_string = 'PRODUCT=' |
| 37 sys_class_dir = '/sys/class/tty/' |
| 38 |
| 39 tty_devices = glob.glob(sys_class_dir + '*') |
| 40 |
| 41 matcher = re.compile('.*' + tty_pattern) |
| 42 tty_matches = [x for x in tty_devices if matcher.search(x)] |
| 43 tty_matches = [x[len(sys_class_dir):] for x in tty_matches] |
| 44 |
| 45 found_devices = [] |
| 46 for match in tty_matches: |
| 47 class_filename = sys_class_dir + match + '/device/uevent' |
| 48 with open(class_filename, 'r') as uevent_file: |
| 49 # Look for the desired product id in the uevent text. |
| 50 for line in uevent_file: |
| 51 if product_string in line: |
| 52 ids = line[len(product_string):].split('/') |
| 53 ids = [int(x, 16) for x in ids] |
| 54 |
| 55 for desired_id in vendor_ids: |
| 56 if desired_id in ids: |
| 57 found_devices.append(match) |
| 58 |
| 59 return found_devices |
| 60 |
| 61 |
| 62 class WebVrLatencyTest(object): |
| 63 """Base class for all WebVR latency tests. |
| 64 |
| 65 This is meant to be subclassed for each platform the test is run on. While |
| 66 the latency test itself is cross-platform, the setup and teardown for |
| 67 tests is platform-dependent. |
| 68 """ |
| 69 def __init__(self, args): |
| 70 self.args = args |
| 71 self._num_samples = args.num_samples |
| 72 self._flicker_app_url = args.url |
| 73 assert (self._num_samples > 0),'Number of samples must be greater than 0' |
| 74 self._device_name = 'generic_device' |
| 75 |
| 76 # Connect to the Arduino that drives the servos |
| 77 devices = GetTtyDevices(r'ttyACM\d+', [0x2a03, 0x2341]) |
| 78 assert (len(devices) == 1),'Found %d devices, expected 1' % len(devices) |
| 79 self.robot_arm = ra.RobotArm(devices[0]) |
| 80 |
| 81 def RunTest(self): |
| 82 """Runs the steps to start Chrome, measure/save latency, and clean up.""" |
| 83 self._Setup() |
| 84 self._Run() |
| 85 self._Teardown() |
| 86 |
| 87 def _Setup(self): |
| 88 """Perform any platform-specific setup.""" |
| 89 raise NotImplementedError( |
| 90 'Platform-specific setup must be implemented in subclass') |
| 91 |
| 92 def _Run(self): |
| 93 """Run the latency test. |
| 94 |
| 95 Handles the actual latency measurement, which is identical across |
| 96 different platforms, as well as result saving. |
| 97 """ |
| 98 # Motopho scripts use relative paths, so switch to the Motopho directory |
| 99 os.chdir(self.args.motopho_path) |
| 100 |
| 101 # Set up the thread that runs the Motopho script |
| 102 motopho_thread = mt.MotophoThread(self._num_samples) |
| 103 motopho_thread.start() |
| 104 |
| 105 # Run multiple times so we can get an average and standard deviation |
| 106 for _ in xrange(self._num_samples): |
| 107 self.robot_arm.ResetPosition() |
| 108 # Start the Motopho script |
| 109 motopho_thread.StartIteration() |
| 110 # Let the Motopho be stationary so the script can calculate the bias |
| 111 time.sleep(3) |
| 112 motopho_thread.BlockNextIteration() |
| 113 # Move so we can measure latency |
| 114 self.robot_arm.StartMotophoMovement() |
| 115 if not motopho_thread.WaitForIterationEnd(MOTOPHO_THREAD_TIMEOUT): |
| 116 # TODO(bsheedy): Look into ways to prevent Motopho from not sending any |
| 117 # data until unplugged and replugged into the machine after a reboot. |
| 118 logging.error('Motopho thread timeout, ' |
| 119 'Motopho may need to be replugged.') |
| 120 self.robot_arm.StopAllMovement() |
| 121 time.sleep(1) |
| 122 self._SaveResults(motopho_thread.latencies, motopho_thread.correlations) |
| 123 |
| 124 def _Teardown(self): |
| 125 """Performs any platform-specific teardown.""" |
| 126 raise NotImplementedError( |
| 127 'Platform-specific setup must be implemented in subclass') |
| 128 |
| 129 def _RunCommand(self, cmd): |
| 130 """Runs the given cmd list and returns its output. |
| 131 |
| 132 Prints the command's output and exits if any error occurs. |
| 133 |
| 134 Returns: |
| 135 A string containing the stdout and stderr of the command. |
| 136 """ |
| 137 try: |
| 138 return subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
| 139 except subprocess.CalledProcessError as e: |
| 140 logging.error('Failed command output: %s', e.output) |
| 141 raise e |
| 142 |
| 143 def _SetChromeCommandLineFlags(self, flags): |
| 144 raise NotImplementedError( |
| 145 'Command-line flag setting must be implemented in subclass') |
| 146 |
| 147 def _SaveResults(self, latencies, correlations): |
| 148 """Saves the results to a JSON file. |
| 149 |
| 150 Saved JSON object is compatible with Chrome perf dashboard if |
| 151 put in as the 'chart_data' value. Also logs the raw data and its |
| 152 average/standard deviation. |
| 153 """ |
| 154 avg_latency = sum(latencies) / len(latencies) |
| 155 std_latency = numpy.std(latencies) |
| 156 avg_correlation = sum(correlations) / len(correlations) |
| 157 std_correlation = numpy.std(correlations) |
| 158 logging.info('Raw latencies: %s\nRaw correlations: %s\n' |
| 159 'Avg latency: %f +/- %f\nAvg correlation: %f +/- %f', |
| 160 str(latencies), str(correlations), avg_latency, std_latency, |
| 161 avg_correlation, std_correlation) |
| 162 |
| 163 if not (self.args.output_dir and os.path.isdir(self.args.output_dir)): |
| 164 logging.warning('No output directory set, not saving results to file') |
| 165 return |
| 166 |
| 167 results = { |
| 168 'format_version': '1.0', |
| 169 'benchmark_name': 'webvr_latency', |
| 170 'benchmark_description': 'Measures the motion-to-photon latency of WebVR', |
| 171 'charts': { |
| 172 'correlation': { |
| 173 'summary': { |
| 174 'improvement_direction': 'up', |
| 175 'name': 'correlation', |
| 176 'std': std_correlation, |
| 177 'type': 'list_of_scalar_values', |
| 178 'units': '', |
| 179 'values': correlations, |
| 180 }, |
| 181 }, |
| 182 'latency': { |
| 183 'summary': { |
| 184 'improvement_direction': 'down', |
| 185 'name': 'latency', |
| 186 'std': std_latency, |
| 187 'type': 'list_of_scalar_values', |
| 188 'units': 'ms', |
| 189 'values': latencies, |
| 190 }, |
| 191 } |
| 192 } |
| 193 } |
| 194 |
| 195 with file(os.path.join(self.args.output_dir, |
| 196 self.args.results_file), 'w') as outfile: |
| 197 json.dump(results, outfile) |
OLD | NEW |