Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(118)

Side by Side Diff: chrome/test/vr/perf/latency/run_latency_test.py

Issue 2799783002: Add automated VR latency tester (Closed)
Patch Set: Address thakis@ comments Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/test/vr/perf/latency/BUILD.gn ('k') | testing/buildbot/gn_isolate_map.pyl » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 """Script for automatically measuring motion-to-photon latency for VR.
6
7 Doing so requires two specialized pieces of hardware. The first is a Motopho,
8 which when used with a VR flicker app, finds the delay between movement and
9 the test device's screen updating in response to the movement. The second is
10 a set of servos, which physically moves the test device and Motopho during the
11 latency test.
12 """
13
14 import argparse
15 import glob
16 import httplib
17 import logging
18 import os
19 import re
20 import serial
21 import subprocess
22 import sys
23 import threading
24 import time
25
26 # RobotArm connection constants
27 BAUD_RATE = 115200
28 CONNECTION_TIMEOUT = 3.0
29 NUM_TRIES = 5
30 # Motopho constants
31 DEFAULT_ADB_PATH = os.path.join(os.path.expanduser('~'),
32 'tools/android/android-sdk-linux',
33 'platform-tools/adb')
34 # TODO(bsheedy): See about adding tool via DEPS instead of relying on it
35 # existing on the bot already
36 DEFAULT_MOTOPHO_PATH = os.path.join(os.path.expanduser('~'), 'motopho/Motopho')
37 MOTOPHO_THREAD_TIMEOUT = 30
38
39 class MotophoThread(threading.Thread):
40 """Handles the running of the Motopho script and extracting results."""
41 def __init__(self):
42 threading.Thread.__init__(self)
43 self._latency = None
44 self._max_correlation = None
45
46 def run(self):
Lei Lei 2017/04/12 22:10:53 s/run/Run to keep consistent within file.
bsheedy 2017/04/12 22:51:58 It's overriding threading.Thread's run() function,
47 motopho_output = ""
48 try:
49 motopho_output = subprocess.check_output(["./motophopro_nograph"],
50 stderr=subprocess.STDOUT)
51 except subprocess.CalledProcessError as e:
52 logging.error('Failed to run Motopho script: %s', e.output)
53 raise e
54
55 if "FAIL" in motopho_output:
56 logging.error('Failed to get latency, logging raw output: %s',
57 motopho_output)
Lei Lei 2017/04/12 22:10:53 How about raising exception here instead of return
bsheedy 2017/04/12 22:51:58 Done.
58 return 1
59
60 self._latency = None
61 self._max_correlation = None
62 for line in motopho_output.split("\n"):
63 if 'Motion-to-photon latency:' in line:
64 self._latency = float(line.split(" ")[-2])
65 if 'Max correlation is' in line:
66 self._max_correlation = float(line.split(' ')[-1])
67 if self._latency and self._max_correlation:
68 break;
69
70 @property
71 def latency(self):
72 return self._latency
73
74 @property
75 def max_correlation(self):
76 return self._max_correlation
77
78
79 class RobotArm():
80 """Handles the serial communication with the servos/arm used for movement."""
81 def __init__(self, device_name, num_tries, baud, timeout):
82 self._connection = None
83 connected = False
84 for _ in xrange(num_tries):
85 try:
86 self._connection = serial.Serial('/dev/' + device_name,
87 baud,
88 timeout=timeout)
89 except serial.SerialException as e:
90 pass
91 if self._connection and 'Enter parameters' in self._connection.read(1024):
92 connected = True
93 break
94 if not connected:
95 raise serial.SerialException('Failed to connect to the robot arm.')
96
97 def StartMotophoMovement(self):
98 if not self._connection:
99 return
100 self._connection.write('9\n')
101
102 def StopAllMovement(self):
103 if not self._connection:
104 return
105 self._connection.write('0\n')
106
107
108 def GetParsedArgs():
109 """Parses the command line arguments passed to the script.
110
111 Fails if any unknown arguments are present.
112 """
113 parser = argparse.ArgumentParser()
114 parser.add_argument('--adb-path',
115 type=os.path.realpath,
116 help='The absolute path to adb',
117 default=DEFAULT_ADB_PATH)
118 parser.add_argument('--motopho-path',
119 type=os.path.realpath,
120 help='The absolute path to the directory with Motopho '
121 'scripts',
122 default=DEFAULT_MOTOPHO_PATH)
123 parser.add_argument('--output-dir',
124 type=os.path.realpath,
125 help='The directory where the script\'s output files '
126 'will be saved')
127 parser.add_argument('-v', '--verbose',
128 dest='verbose_count', default=0, action='count',
129 help='Verbose level (multiple times for more)')
130 (args, unknown_args) = parser.parse_known_args()
131 SetLogLevel(args.verbose_count)
132 if unknown_args:
133 parser.error('Received unknown arguments: %s' % ' '.join(unknown_args))
134 return args
135
136
137 def SetLogLevel(verbose_count):
138 """Sets the log level based on the command line arguments."""
139 log_level = logging.WARNING
140 if verbose_count == 1:
141 log_level = logging.INFO
142 elif verbose_count >= 2:
143 log_level = logging.DEBUG
144 logger = logging.getLogger()
145 logger.setLevel(log_level)
146
147
148 def GetTtyDevices(tty_pattern, vendor_ids):
149 """Find all devices connected to tty that match a pattern and device id.
150
151 If a serial device is connected to the computer via USB, this function
152 will check all tty devices that match tty_pattern, and return the ones
153 that have vendor identification number in the list vendor_ids.
154
155 Args:
156 tty_pattern: The search pattern, such as r'ttyACM\d+'.
157 vendor_ids: The list of 16-bit USB vendor ids, such as [0x2a03].
158
159 Returns:
160 A list of strings of tty devices, for example ['ttyACM0'].
161 """
162 product_string = 'PRODUCT='
163 sys_class_dir = '/sys/class/tty/'
164
165 tty_devices = glob.glob(sys_class_dir + '*')
166
167 matcher = re.compile('.*' + tty_pattern)
168 tty_matches = [x for x in tty_devices if matcher.search(x)]
169 tty_matches = [x[len(sys_class_dir):] for x in tty_matches]
170
171 found_devices = []
172 for match in tty_matches:
173 class_filename = sys_class_dir + match + '/device/uevent'
174 with open(class_filename, 'r') as uevent_file:
175 # Look for the desired product id in the uevent text.
176 for line in uevent_file:
177 if product_string in line:
178 ids = line[len(product_string):].split('/')
179 ids = [int(x, 16) for x in ids]
180
181 for desired_id in vendor_ids:
182 if desired_id in ids:
183 found_devices.append(match)
184
185 return found_devices
186
187
188 def RunCommand(cmd):
189 """Runs the given cmd list.
190
191 Prints the command's output and exits if any error occurs.
192 """
193 try:
194 subprocess.check_output(cmd, stderr=subprocess.STDOUT)
195 except subprocess.CalledProcessError as e:
196 logging.error('Failed command output: %s', e.output)
197 raise e
198
199
200 def SetChromeCommandLineFlags(adb_path, flags):
201 """Sets the given Chrome command line flags."""
202 RunCommand([adb_path,
203 'shell', "echo 'chrome " + ' '.join(flags) + "' > "
204 + '/data/local/tmp/chrome-command-line'])
205
206
207 def main():
208 args = GetParsedArgs()
209
210 RunCommand([args.adb_path, 'root'])
211 RunCommand([args.adb_path, 'install', '-r', 'apks/ChromePublic.apk'])
212 # Force WebVR support and don't have first run experience
213 SetChromeCommandLineFlags(args.adb_path, ['--enable-webvr', '--disable-fre'])
214
215 # Motopho scripts use relative paths, so switch to the Motopho directory
216 os.chdir(args.motopho_path)
217
218 # Connect to the Arduino that drives the servos
219 devices = GetTtyDevices(r'ttyACM\d+', [0x2a03, 0x2341])
220 if len(devices) != 1:
221 logging.error('Found %d devices, expected 1', len(devices))
222 return 1
223 robot_arm = RobotArm(devices[0], NUM_TRIES, BAUD_RATE, CONNECTION_TIMEOUT)
224
225 # Wake the device
226 RunCommand([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_WAKEUP'])
227 # Sleep a bit, otherwise WebGL can crash when Canary starts
228 time.sleep(1)
229
230 # Start Chrome and go to the flicker app
231 # TODO(bsheedy): See about having versioned copies of the flicker app instead
232 # of using personal github.
233 RunCommand([args.adb_path, 'shell', 'am', 'start',
234 '-a', 'android.intent.action.MAIN',
235 '-n', 'org.chromium.chrome/com.google.android.apps.chrome.Main',
236 'https://weableandbob.github.io/Motopho/flicker_apps/webvr/webvr- flicker-app-klaus.html?polyfill=0\&canvasClickPresents=1'])
237 time.sleep(10)
238
239 # Tap the screen to start presenting
240 RunCommand([args.adb_path, 'shell', 'input', 'touchscreen',
241 'tap', '800', '800'])
Lei Lei 2017/04/12 22:10:53 indent here.
bsheedy 2017/04/12 22:51:58 Done.
242 # Wait for VR to fully start up
243 time.sleep(5)
244
245 # Start the Motopho script
246 motopho_thread = MotophoThread()
247 motopho_thread.start()
248 # Let the Motopho be stationary so the script can calculate its bias
249 time.sleep(3)
250
251 # Move so we can measure latency
252 robot_arm.StartMotophoMovement()
253 motopho_thread.join(MOTOPHO_THREAD_TIMEOUT)
254 if motopho_thread.isAlive():
255 # TODO(bsheedy): Look into ways to prevent Motopho from not sending any
256 # data until unplugged and replugged into the machine after a reboot.
257 logging.error('Motopho thread timeout, Motopho may need to be replugged.')
258 robot_arm.StopAllMovement()
259
260 logging.info('Latency: %s', motopho_thread.latency)
261 logging.info('Max correlation: %s', motopho_thread.max_correlation)
262
263 # TODO(bsheedy): Change this to output JSON compatible with the performance
264 # dashboard.
265 if args.output_dir and os.path.isdir(args.output_dir):
266 with file(os.path.join(args.output_dir, 'output.txt'), 'w') as outfile:
267 outfile.write('Latency: %s\nMax correlation: %s\n' %
268 (motopho_thread.latency, motopho_thread.max_correlation))
269
270 # Exit VR and Close Chrome
271 # TODO(bsheedy): See about closing current tab before exiting so they don't
272 # pile up over time.
273 RunCommand([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_BACK'])
274 RunCommand([args.adb_path, 'shell', 'am', 'force-stop',
275 'org.chromium.chrome'])
276
277 # Turn off the screen
278 RunCommand([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_POWER'])
279
280 return 0
281
282 if __name__ == '__main__':
283 sys.exit(main())
OLDNEW
« no previous file with comments | « chrome/test/vr/perf/latency/BUILD.gn ('k') | testing/buildbot/gn_isolate_map.pyl » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698