forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make VR latency results compatible with perf dashboard, refactor into…
… classes Makes the following changes to the automated VR latency testing script: - Adds the functionality to save latency and correlation results in a format compatible with the Chrome performance dashboard - Refactors the code into separate classes and files for improved readability and to better support future Windows latency testing - Runs the test multiple times (default 10) to get an average and standard deviation BUG=708747 Review-Url: https://codereview.chromium.org/2823883003 Cr-Commit-Position: refs/heads/master@{#466147}
- Loading branch information
bsheedy
authored and
Commit bot
committed
Apr 20, 2017
1 parent
f6cbd1a
commit a158481
Showing
10 changed files
with
524 additions
and
229 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Copyright 2017 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
group("motopho_latency_test") { | ||
testonly = true | ||
data = [ | ||
"./latency/__init__.py", | ||
"./latency/android_webvr_latency_test.py", | ||
"./latency/motopho_thread.py", | ||
"./latency/robot_arm.py", | ||
"./latency/run_latency_test.py", | ||
"./latency/webvr_latency_test.py", | ||
"//third_party/android_tools/sdk/platform-tools/adb", | ||
"//third_party/gvr-android-sdk/test-apks/vr_services/vr_services_current.apk", | ||
] | ||
data_deps = [ | ||
"//chrome/android:chrome_public_apk", | ||
] | ||
} |
This file was deleted.
Oops, something went wrong.
Empty file.
135 changes: 135 additions & 0 deletions
135
chrome/test/vr/perf/latency/android_webvr_latency_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# Copyright 2017 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
import webvr_latency_test | ||
|
||
import logging | ||
import os | ||
import time | ||
|
||
|
||
DEFAULT_SCREEN_WIDTH = 720 | ||
DEFAULT_SCREEN_HEIGHT = 1280 | ||
NUM_VR_ENTRY_ATTEMPTS = 5 | ||
|
||
|
||
class AndroidWebVrLatencyTest(webvr_latency_test.WebVrLatencyTest): | ||
"""Android implementation of the WebVR latency test.""" | ||
def __init__(self, args): | ||
super(AndroidWebVrLatencyTest, self).__init__(args) | ||
self._device_name = self._Adb(['shell', 'getprop', | ||
'ro.product.name']).strip() | ||
|
||
def _Setup(self): | ||
self._Adb(['root']) | ||
|
||
# Install the latest VrCore and Chrome APKs | ||
self._Adb(['install', '-r', '-d', | ||
'../../third_party/gvr-android-sdk/test-apks/vr_services' | ||
'/vr_services_current.apk']) | ||
self._SaveInstalledVrCoreVersion() | ||
# TODO(bsheedy): Make APK path configurable so usable with other channels | ||
self._Adb(['install', '-r', 'apks/ChromePublic.apk']) | ||
|
||
# Force WebVR support, remove open tabs, and don't have first run | ||
# experience. | ||
self._SetChromeCommandLineFlags(['--enable-webvr', '--no-restore-state', | ||
'--disable-fre']) | ||
# Wake up the device and sleep, otherwise WebGL can crash on startup. | ||
self._Adb(['shell', 'input', 'keyevent', 'KEYCODE_WAKEUP']) | ||
time.sleep(1) | ||
|
||
# Start Chrome | ||
self._Adb(['shell', 'am', 'start', | ||
'-a', 'android.intent.action.MAIN', | ||
'-n', 'org.chromium.chrome/com.google.android.apps.chrome.Main', | ||
self._flicker_app_url]) | ||
time.sleep(10) | ||
|
||
# Tap the center of the screen to start presenting. | ||
# It's technically possible that the screen tap won't enter VR on the first | ||
# time, so try several times by checking for the logcat output from | ||
# entering VR | ||
(width, height) = self._GetScreenResolution() | ||
entered_vr = False | ||
for _ in xrange(NUM_VR_ENTRY_ATTEMPTS): | ||
self._Adb(['logcat', '-c']) | ||
self._Adb(['shell', 'input', 'touchscreen', 'tap', str(width/2), | ||
str(height/2)]) | ||
time.sleep(5) | ||
output = self._Adb(['logcat', '-d']) | ||
if 'Initialized GVR version' in output: | ||
entered_vr = True | ||
break | ||
logging.warning('Failed to enter VR, retrying') | ||
if not entered_vr: | ||
raise RuntimeError('Failed to enter VR after %d attempts' | ||
% NUM_VR_ENTRY_ATTEMPTS) | ||
|
||
def _Teardown(self): | ||
# Exit VR and close Chrome | ||
self._Adb(['shell', 'input', 'keyevent', 'KEYCODE_BACK']) | ||
self._Adb(['shell', 'am', 'force-stop', 'org.chromium.chrome']) | ||
# Turn off the screen | ||
self._Adb(['shell', 'input', 'keyevent', 'KEYCODE_POWER']) | ||
|
||
def _Adb(self, cmd): | ||
"""Runs the given command via adb. | ||
Returns: | ||
A string containing the stdout and stderr of the adb command. | ||
""" | ||
# TODO(bsheedy): Maybe migrate to use Devil (overkill?) | ||
return self._RunCommand([self.args.adb_path] + cmd) | ||
|
||
def _SaveInstalledVrCoreVersion(self): | ||
"""Retrieves the VrCore version and saves it for dashboard uploading.""" | ||
output = self._Adb(['shell', 'dumpsys', 'package', 'com.google.vr.vrcore']) | ||
version = None | ||
for line in output.split('\n'): | ||
if 'versionName' in line: | ||
version = line.split('=')[1] | ||
break | ||
if version: | ||
logging.info('VrCore version is %s', version) | ||
else: | ||
logging.info('VrCore version not retrieved') | ||
version = '0' | ||
if not (self.args.output_dir and os.path.isdir(self.args.output_dir)): | ||
logging.warning('No output directory, not saving VrCore version') | ||
return | ||
with file(os.path.join(self.args.output_dir, | ||
self.args.vrcore_version_file), 'w') as outfile: | ||
outfile.write(version) | ||
|
||
def _SetChromeCommandLineFlags(self, flags): | ||
"""Sets the Chrome command line flags to the given list.""" | ||
self._Adb(['shell', "echo 'chrome " + ' '.join(flags) + "' > " | ||
+ '/data/local/tmp/chrome-command-line']) | ||
|
||
def _GetScreenResolution(self): | ||
"""Retrieves the device's screen resolution, or a default if not found. | ||
Returns: | ||
A tuple (width, height). | ||
""" | ||
output = self._Adb(['shell', 'dumpsys', 'display']) | ||
width = None | ||
height = None | ||
for line in output.split('\n'): | ||
if 'mDisplayWidth' in line: | ||
width = int(line.split('=')[1]) | ||
elif 'mDisplayHeight' in line: | ||
height = int(line.split('=')[1]) | ||
if width and height: | ||
break | ||
if not width: | ||
logging.warning('Could not get actual screen width, defaulting to %d', | ||
DEFAULT_SCREEN_WIDTH) | ||
width = DEFAULT_SCREEN_WIDTH | ||
if not height: | ||
logging.warning('Could not get actual screen height, defaulting to %d', | ||
DEFAULT_SCREEN_HEIGHT) | ||
height = DEFAULT_SCREEN_HEIGHT | ||
return (width, height) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# Copyright 2017 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
import logging | ||
import subprocess | ||
import threading | ||
|
||
|
||
class MotophoThread(threading.Thread): | ||
"""Handles the running of the Motopho script and extracting results.""" | ||
def __init__(self, num_samples): | ||
threading.Thread.__init__(self) | ||
self._num_samples = num_samples | ||
self._latencies = [] | ||
self._correlations = [] | ||
# Threads can't be restarted, so in order to gather multiple samples, we | ||
# need to either re-create the thread for every iteration or use a loop | ||
# and locks in a single thread -> use the latter solution | ||
self._start_lock = threading.Event() | ||
self._finish_lock = threading.Event() | ||
self.BlockNextIteration() | ||
|
||
def run(self): | ||
for _ in xrange(self._num_samples): | ||
self._WaitForIterationStart() | ||
self._ResetEndLock() | ||
motopho_output = "" | ||
try: | ||
motopho_output = subprocess.check_output(["./motophopro_nograph"], | ||
stderr=subprocess.STDOUT) | ||
except subprocess.CalledProcessError as e: | ||
logging.error('Failed to run Motopho script: %s', e.output) | ||
raise e | ||
|
||
if "FAIL" in motopho_output: | ||
logging.error('Failed to get latency, logging raw output: %s', | ||
motopho_output) | ||
raise RuntimeError('Failed to get latency - correlation likely too low') | ||
|
||
current_num_samples = len(self._latencies) | ||
for line in motopho_output.split("\n"): | ||
if 'Motion-to-photon latency:' in line: | ||
self._latencies.append(float(line.split(" ")[-2])) | ||
if 'Max correlation is' in line: | ||
self._correlations.append(float(line.split(' ')[-1])) | ||
if (len(self._latencies) > current_num_samples and | ||
len(self._correlations) > current_num_samples): | ||
break; | ||
self._EndIteration() | ||
|
||
def _WaitForIterationStart(self): | ||
self._start_lock.wait() | ||
|
||
def StartIteration(self): | ||
"""Tells the thread to start its next test iteration.""" | ||
self._start_lock.set() | ||
|
||
def BlockNextIteration(self): | ||
"""Blocks the thread from starting the next iteration without a signal.""" | ||
self._start_lock.clear() | ||
|
||
def _EndIteration(self): | ||
self._finish_lock.set() | ||
|
||
def _ResetEndLock(self): | ||
self._finish_lock.clear() | ||
|
||
def WaitForIterationEnd(self, timeout): | ||
"""Waits until the thread says it's finished or times out. | ||
Returns: | ||
Whether the iteration ended within the given timeout | ||
""" | ||
return self._finish_lock.wait(timeout) | ||
|
||
@property | ||
def latencies(self): | ||
return self._latencies | ||
|
||
@property | ||
def correlations(self): | ||
return self._correlations |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Copyright 2017 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
import serial | ||
import time | ||
|
||
|
||
class RobotArm(): | ||
"""Handles the serial communication with the servos/arm used for movement.""" | ||
def __init__(self, device_name, num_tries=5, baud=115200, timeout=3.0): | ||
self._connection = None | ||
connected = False | ||
for _ in xrange(num_tries): | ||
try: | ||
self._connection = serial.Serial('/dev/' + device_name, | ||
baud, | ||
timeout=timeout) | ||
except serial.SerialException as e: | ||
pass | ||
if self._connection and 'Enter parameters' in self._connection.read(1024): | ||
connected = True | ||
break | ||
if not connected: | ||
raise serial.SerialException('Failed to connect to the robot arm.') | ||
|
||
def ResetPosition(self): | ||
if not self._connection: | ||
return | ||
# If the servo stopped very close to the desired position, it can just | ||
# vibrate instead of moving, so move away before going to the reset | ||
# position | ||
self._connection.write('5 300 0 5\n') | ||
time.sleep(0.5) | ||
self._connection.write('5 250 0 5\n') | ||
time.sleep(0.5) | ||
|
||
def StartMotophoMovement(self): | ||
if not self._connection: | ||
return | ||
self._connection.write('9\n') | ||
|
||
def StopAllMovement(self): | ||
if not self._connection: | ||
return | ||
self._connection.write('0\n') |
Oops, something went wrong.