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

Side by Side Diff: chrome/test/vr/automated_motopho_latency/run_latency_test.py

Issue 2799783002: Add automated VR latency tester (Closed)
Patch Set: Working version that must be manually triggered 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
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 ShakerBot, which physically moves the test device and Motopho during the
Lei Lei 2017/04/07 21:36:35 I am not sure if it is ok to mention shakerbot pub
bsheedy 2017/04/07 23:24:52 Removed from the comment, and renamed the class to
11 latency test.
12 """
13
14 import argparse
15 import glob
16 import httplib
17 import os
18 import re
19 import serial
20 import subprocess
21 import sys
22 import threading
23 import time
24
25 # ShakerBot connection constants
26 BAUD_RATE = 115200
27 CONNECTION_TIMEOUT = 3.0
28 NUM_TRIES = 5
29 # Motopho constants
30 DEFAULT_ADB_PATH = ('/home/gtvchrome/tools/android/android-sdk-linux/'
31 'platform-tools/adb')
32 DEFAULT_MOTOPHO_PATH = '/home/gtvchrome/motopho/Motopho'
33 MOTOPHO_THREAD_TIMEOUT = 30
34
35 class MotophoThread(threading.Thread):
36 """Handles the running of the Motopho script and extracting results."""
37 def __init__(self):
38 threading.Thread.__init__(self)
39 self._latency = None
40 self._max_correlation = None
41
42 def run(self):
Lei Lei 2017/04/07 21:36:35 s/run/Run to keep consistent about naming within f
bsheedy 2017/04/07 23:24:52 It's overriding threading.Thread's run() function.
43 motopho_output = ""
44 try:
45 motopho_output = subprocess.check_output(["./motophopro_nograph"],
46 stderr=subprocess.STDOUT)
47 except subprocess.CalledProcessError as e:
48 print e.output
Lei Lei 2017/04/07 21:36:35 It is better to use logging module for logs instea
bsheedy 2017/04/07 23:24:52 Done.
49 return 1
50
51 if "FAIL" in motopho_output:
52 print 'Failed to get latency, printing raw output'
53 print motopho_output
54 return 1
55
56 self._latency = None
57 self._max_correlation = None
58 for line in motopho_output.split("\n"):
59 if 'Motion-to-photon latency:' in line:
60 self._latency = float(line.split(" ")[-2])
61 if 'Max correlation is' in line:
62 self._max_correlation = float(line.split(' ')[-1])
63 if self._latency and self._max_correlation:
64 break;
65
66 @property
67 def latency(self):
68 return self._latency
69
70 @property
71 def max_correlation(self):
72 return self._max_correlation
73
74
75 class ShakerBot():
76 """Handles the serial communication with the ShakerBot."""
77 def __init__(self, device_name, num_tries, baud, timeout):
78 self._connection = None
79 connected = False
80 for attempt in xrange(num_tries):
Lei Lei 2017/04/07 21:36:35 nit: attempt is unused, you can add unused prefix
bsheedy 2017/04/07 23:24:52 Replaced with the more standard "_" for unused var
81 try:
82 self._connection = serial.Serial('/dev/' + device_name,
83 baud,
84 timeout=timeout)
85 except serial.SerialException as e:
86 pass
87 if self._connection and 'Enter parameters' in self._connection.read(1024):
88 connected = True
89 break
90 if not connected:
91 print 'Failed to connect to ShakerBot'
92 sys.exit(1)
Lei Lei 2017/04/07 21:36:35 It is better to throw exception here instead of ex
bsheedy 2017/04/07 23:24:52 Done.
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 (args, unknown_args) = parser.parse_known_args()
121 if unknown_args:
122 print 'Received unknown args: ' + ' '.join(unknown_args)
123 sys.exit(1)
Lei Lei 2017/04/07 21:36:35 Considering to use parser.error('unrecognized argu
bsheedy 2017/04/07 23:24:52 Done.
124 return args
125
126
127 # Taken from Daydream's perf test suite
128 def GetTtyDevices(tty_pattern, vendor_ids):
129 """Find all devices connected to tty that match a pattern and device id.
130
131 If a serial device is connected to the computer via USB, this function
132 will check all tty devices that match tty_pattern, and return the ones
133 that have vendor identification number in the list vendor_ids.
134
135 Args:
136 tty_pattern: The search pattern, such as r'ttyACM\d+'.
137 vendor_ids: The list of 16-bit USB vendor ids, such as [0x2a03].
138
139 Returns:
140 A list of strings of tty devices, for example ['ttyACM0'].
141 """
142 product_string = 'PRODUCT='
143 sys_class_dir = '/sys/class/tty/'
144
145 tty_devices = glob.glob(sys_class_dir + '*')
146
147 matcher = re.compile('.*' + tty_pattern)
148 tty_matches = [x for x in tty_devices if matcher.search(x)]
149 tty_matches = [x[len(sys_class_dir):] for x in tty_matches]
150
151 found_devices = []
152 for match in tty_matches:
153 class_filename = sys_class_dir + match + '/device/uevent'
154 with open(class_filename, 'r') as uevent_file:
155 # Look for the desired product id in the uevent text.
156 for line in uevent_file:
157 if product_string in line:
158 ids = line[len(product_string):].split('/')
159 ids = [int(x, 16) for x in ids]
160
161 for desired_id in vendor_ids:
162 if desired_id in ids:
163 found_devices.append(match)
164
165 return found_devices
166
167
168 def RunCmdOrFail(cmd):
Lei Lei 2017/04/07 21:36:35 Consider to rename it to RunCommand, no need for f
bsheedy 2017/04/07 23:24:52 Done.
169 """Runs the given cmd list.
170
171 Prints the command's output and exits if any error occurs.
172 """
173 try:
174 subprocess.check_output(cmd, stderr=subprocess.STDOUT)
175 except subprocess.CalledProcessError as e:
176 print e.output
177 sys.exit(1)
Lei Lei 2017/04/07 21:36:35 It is better to throw the original exception here.
bsheedy 2017/04/07 23:24:52 Done.
178
179
180 def SetChromeCommandLineFlags(adb_path, flags):
181 """Sets the given Chrome command line flags.
182
183 Puts them in both valid file locations.
184 """
185 flag_string = "echo 'chrome " + ' '.join(flags) + "' > "
186 RunCmdOrFail([adb_path,
187 'shell', flag_string + '/data/local/chrome-command-line'])
188 RunCmdOrFail([adb_path,
189 'shell', flag_string + '/data/local/tmp/chrome-command-line'])
Lei Lei 2017/04/07 21:36:35 Why does it need to set the flags in two files? Ac
bsheedy 2017/04/07 23:24:52 You're right, /data/local/chrome-command-line was
190
191
192 def main():
193 args = GetParsedArgs()
194
195 RunCmdOrFail([args.adb_path, 'root'])
196 RunCmdOrFail([args.adb_path, 'install', '-r', 'apks/ChromePublic.apk'])
197 # Force WebVR support and don't have first run experience
198 SetChromeCommandLineFlags(args.adb_path, ['--enable-webvr', '--disable-fre'])
199
200 # Motopho scripts use relative paths, so switch to the Motopho directory
201 os.chdir(args.motopho_path)
202
203 # Connect to the ShakerBot
204 devices = GetTtyDevices(r'ttyACM\d+', [0x2a03, 0x2341])
205 if len(devices) != 1:
206 print 'Found ' + str(len(devices)) + ' devices, expected 1'
207 return 1
208 shaker_bot = ShakerBot(devices[0], NUM_TRIES, BAUD_RATE, CONNECTION_TIMEOUT)
209
210 # Wake the device
211 RunCmdOrFail([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_WAKEUP'])
212 # Sleep a bit, otherwise WebGL can crash when Canary starts
213 time.sleep(1)
214
215 # Start Chrome and go to the flicker app
216 # TODO(bsheedy): See about having versioned copies of the flicker app instead
217 # of using personal github.
218 RunCmdOrFail([args.adb_path, 'shell', 'am', 'start',
219 '-a', 'android.intent.action.MAIN',
220 '-n', 'org.chromium.chrome/com.google.android.apps.chrome.Main',
221 'https://weableandbob.github.io/Motopho/flicker_apps/webvr/webvr- flicker-app-klaus.html?polyfill=0\&canvasClickPresents=1'])
222 time.sleep(10)
223
224 # Tap the screen to start presenting
225 RunCmdOrFail([args.adb_path, 'shell', 'input', 'touchscreen',
226 'tap', '800', '800'])
227 # Wait for VR to fully start up
228 time.sleep(5)
229
230 # Start the Motopho script
231 motopho_thread = MotophoThread()
232 motopho_thread.start()
233 # Let the Motopho be stationary so the script can calculate its bias
234 time.sleep(3)
235
236 # Move so we can measure latency
237 shaker_bot.StartMotophoMovement()
238 motopho_thread.join(MOTOPHO_THREAD_TIMEOUT)
239 if motopho_thread.isAlive():
240 # TODO(bsheedy): Look into ways to prevent Motopho from not sending any
241 # data until unplugged and replugged into the machine after a reboot.
242 print "Motopho thread join timed out - Motopho might need to be replugged."
243 shaker_bot.StopAllMovement()
244
245 print "Latency: " + str(motopho_thread.latency)
246 print "Max correlation: " + str(motopho_thread.max_correlation)
Lei Lei 2017/04/07 21:36:35 The latency and max correlation are also needed to
bsheedy 2017/04/07 23:24:52 Done.
247
248 # Exit VR and Close Chrome
249 # TODO(bsheedy): See about closing current tab before exiting so they don't
250 # pile up over time.
251 RunCmdOrFail([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_BACK'])
252 RunCmdOrFail([args.adb_path, 'shell', 'am', 'force-stop',
253 'org.chromium.chrome'])
254
255 # Turn off the screen
256 RunCmdOrFail([args.adb_path, 'shell', 'input', 'keyevent', 'KEYCODE_POWER'])
257
258 return 0
259
260 if __name__ == '__main__':
261 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698