| Index: chrome/test/vr/perf/latency/webvr_latency_test.py
|
| diff --git a/chrome/test/vr/perf/latency/webvr_latency_test.py b/chrome/test/vr/perf/latency/webvr_latency_test.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0a289a070b25e61536647716280b198f2f5248ee
|
| --- /dev/null
|
| +++ b/chrome/test/vr/perf/latency/webvr_latency_test.py
|
| @@ -0,0 +1,197 @@
|
| +# 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 motopho_thread as mt
|
| +import robot_arm as ra
|
| +
|
| +import json
|
| +import glob
|
| +import logging
|
| +import numpy
|
| +import os
|
| +import re
|
| +import subprocess
|
| +import sys
|
| +import time
|
| +
|
| +
|
| +MOTOPHO_THREAD_TIMEOUT = 30
|
| +
|
| +
|
| +def GetTtyDevices(tty_pattern, vendor_ids):
|
| + """Finds all devices connected to tty that match a pattern and device id.
|
| +
|
| + If a serial device is connected to the computer via USB, this function
|
| + will check all tty devices that match tty_pattern, and return the ones
|
| + that have vendor identification number in the list vendor_ids.
|
| +
|
| + Args:
|
| + tty_pattern: The search pattern, such as r'ttyACM\d+'.
|
| + vendor_ids: The list of 16-bit USB vendor ids, such as [0x2a03].
|
| +
|
| + Returns:
|
| + A list of strings of tty devices, for example ['ttyACM0'].
|
| + """
|
| + product_string = 'PRODUCT='
|
| + sys_class_dir = '/sys/class/tty/'
|
| +
|
| + tty_devices = glob.glob(sys_class_dir + '*')
|
| +
|
| + matcher = re.compile('.*' + tty_pattern)
|
| + tty_matches = [x for x in tty_devices if matcher.search(x)]
|
| + tty_matches = [x[len(sys_class_dir):] for x in tty_matches]
|
| +
|
| + found_devices = []
|
| + for match in tty_matches:
|
| + class_filename = sys_class_dir + match + '/device/uevent'
|
| + with open(class_filename, 'r') as uevent_file:
|
| + # Look for the desired product id in the uevent text.
|
| + for line in uevent_file:
|
| + if product_string in line:
|
| + ids = line[len(product_string):].split('/')
|
| + ids = [int(x, 16) for x in ids]
|
| +
|
| + for desired_id in vendor_ids:
|
| + if desired_id in ids:
|
| + found_devices.append(match)
|
| +
|
| + return found_devices
|
| +
|
| +
|
| +class WebVrLatencyTest(object):
|
| + """Base class for all WebVR latency tests.
|
| +
|
| + This is meant to be subclassed for each platform the test is run on. While
|
| + the latency test itself is cross-platform, the setup and teardown for
|
| + tests is platform-dependent.
|
| + """
|
| + def __init__(self, args):
|
| + self.args = args
|
| + self._num_samples = args.num_samples
|
| + self._flicker_app_url = args.url
|
| + assert (self._num_samples > 0),'Number of samples must be greater than 0'
|
| + self._device_name = 'generic_device'
|
| +
|
| + # Connect to the Arduino that drives the servos
|
| + devices = GetTtyDevices(r'ttyACM\d+', [0x2a03, 0x2341])
|
| + assert (len(devices) == 1),'Found %d devices, expected 1' % len(devices)
|
| + self.robot_arm = ra.RobotArm(devices[0])
|
| +
|
| + def RunTest(self):
|
| + """Runs the steps to start Chrome, measure/save latency, and clean up."""
|
| + self._Setup()
|
| + self._Run()
|
| + self._Teardown()
|
| +
|
| + def _Setup(self):
|
| + """Perform any platform-specific setup."""
|
| + raise NotImplementedError(
|
| + 'Platform-specific setup must be implemented in subclass')
|
| +
|
| + def _Run(self):
|
| + """Run the latency test.
|
| +
|
| + Handles the actual latency measurement, which is identical across
|
| + different platforms, as well as result saving.
|
| + """
|
| + # Motopho scripts use relative paths, so switch to the Motopho directory
|
| + os.chdir(self.args.motopho_path)
|
| +
|
| + # Set up the thread that runs the Motopho script
|
| + motopho_thread = mt.MotophoThread(self._num_samples)
|
| + motopho_thread.start()
|
| +
|
| + # Run multiple times so we can get an average and standard deviation
|
| + for _ in xrange(self._num_samples):
|
| + self.robot_arm.ResetPosition()
|
| + # Start the Motopho script
|
| + motopho_thread.StartIteration()
|
| + # Let the Motopho be stationary so the script can calculate the bias
|
| + time.sleep(3)
|
| + motopho_thread.BlockNextIteration()
|
| + # Move so we can measure latency
|
| + self.robot_arm.StartMotophoMovement()
|
| + if not motopho_thread.WaitForIterationEnd(MOTOPHO_THREAD_TIMEOUT):
|
| + # TODO(bsheedy): Look into ways to prevent Motopho from not sending any
|
| + # data until unplugged and replugged into the machine after a reboot.
|
| + logging.error('Motopho thread timeout, '
|
| + 'Motopho may need to be replugged.')
|
| + self.robot_arm.StopAllMovement()
|
| + time.sleep(1)
|
| + self._SaveResults(motopho_thread.latencies, motopho_thread.correlations)
|
| +
|
| + def _Teardown(self):
|
| + """Performs any platform-specific teardown."""
|
| + raise NotImplementedError(
|
| + 'Platform-specific setup must be implemented in subclass')
|
| +
|
| + def _RunCommand(self, cmd):
|
| + """Runs the given cmd list and returns its output.
|
| +
|
| + Prints the command's output and exits if any error occurs.
|
| +
|
| + Returns:
|
| + A string containing the stdout and stderr of the command.
|
| + """
|
| + try:
|
| + return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
| + except subprocess.CalledProcessError as e:
|
| + logging.error('Failed command output: %s', e.output)
|
| + raise e
|
| +
|
| + def _SetChromeCommandLineFlags(self, flags):
|
| + raise NotImplementedError(
|
| + 'Command-line flag setting must be implemented in subclass')
|
| +
|
| + def _SaveResults(self, latencies, correlations):
|
| + """Saves the results to a JSON file.
|
| +
|
| + Saved JSON object is compatible with Chrome perf dashboard if
|
| + put in as the 'chart_data' value. Also logs the raw data and its
|
| + average/standard deviation.
|
| + """
|
| + avg_latency = sum(latencies) / len(latencies)
|
| + std_latency = numpy.std(latencies)
|
| + avg_correlation = sum(correlations) / len(correlations)
|
| + std_correlation = numpy.std(correlations)
|
| + logging.info('Raw latencies: %s\nRaw correlations: %s\n'
|
| + 'Avg latency: %f +/- %f\nAvg correlation: %f +/- %f',
|
| + str(latencies), str(correlations), avg_latency, std_latency,
|
| + avg_correlation, std_correlation)
|
| +
|
| + if not (self.args.output_dir and os.path.isdir(self.args.output_dir)):
|
| + logging.warning('No output directory set, not saving results to file')
|
| + return
|
| +
|
| + results = {
|
| + 'format_version': '1.0',
|
| + 'benchmark_name': 'webvr_latency',
|
| + 'benchmark_description': 'Measures the motion-to-photon latency of WebVR',
|
| + 'charts': {
|
| + 'correlation': {
|
| + 'summary': {
|
| + 'improvement_direction': 'up',
|
| + 'name': 'correlation',
|
| + 'std': std_correlation,
|
| + 'type': 'list_of_scalar_values',
|
| + 'units': '',
|
| + 'values': correlations,
|
| + },
|
| + },
|
| + 'latency': {
|
| + 'summary': {
|
| + 'improvement_direction': 'down',
|
| + 'name': 'latency',
|
| + 'std': std_latency,
|
| + 'type': 'list_of_scalar_values',
|
| + 'units': 'ms',
|
| + 'values': latencies,
|
| + },
|
| + }
|
| + }
|
| + }
|
| +
|
| + with file(os.path.join(self.args.output_dir,
|
| + self.args.results_file), 'w') as outfile:
|
| + json.dump(results, outfile)
|
|
|