Skip to content

Commit

Permalink
Make VR latency results compatible with perf dashboard, refactor into…
Browse files Browse the repository at this point in the history
… 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
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 229 deletions.
2 changes: 1 addition & 1 deletion BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ group("gn_all") {
if (is_android) {
deps += [
"//build/android/gyp/test:hello_world",
"//chrome/test/vr/perf/latency:motopho_latency_test",
"//chrome/test/vr/perf:motopho_latency_test",
]
}

Expand Down
20 changes: 20 additions & 0 deletions chrome/test/vr/perf/BUILD.gn
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",
]
}
13 changes: 0 additions & 13 deletions chrome/test/vr/perf/latency/BUILD.gn

This file was deleted.

Empty file.
135 changes: 135 additions & 0 deletions chrome/test/vr/perf/latency/android_webvr_latency_test.py
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)
83 changes: 83 additions & 0 deletions chrome/test/vr/perf/latency/motopho_thread.py
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
46 changes: 46 additions & 0 deletions chrome/test/vr/perf/latency/robot_arm.py
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')
Loading

0 comments on commit a158481

Please sign in to comment.