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

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

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