| # Copyright (C) 2012 Google Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import copy |
| import logging |
| import os |
| import re |
| import sys |
| import subprocess |
| import threading |
| import time |
| |
| from webkitpy.layout_tests.port import chromium |
| from webkitpy.layout_tests.port import driver |
| from webkitpy.layout_tests.port import factory |
| from webkitpy.layout_tests.port import server_process |
| from webkitpy.common.system.profiler import SingleFileOutputProfiler |
| |
| _log = logging.getLogger(__name__) |
| |
| # The root directory for test resources, which has the same structure as the |
| # source root directory of Chromium. |
| # This path is defined in Chromium's base/test/test_support_android.cc. |
| DEVICE_SOURCE_ROOT_DIR = '/data/local/tmp/' |
| COMMAND_LINE_FILE = DEVICE_SOURCE_ROOT_DIR + 'chrome-native-tests-command-line' |
| |
| # The directory to put tools and resources of DumpRenderTree. |
| # If change this, must also change Tools/DumpRenderTree/chromium/TestShellAndroid.cpp |
| # and Chromium's webkit/support/platform_support_android.cc. |
| DEVICE_DRT_DIR = DEVICE_SOURCE_ROOT_DIR + 'drt/' |
| DEVICE_FORWARDER_PATH = DEVICE_DRT_DIR + 'forwarder' |
| |
| # Path on the device where the test framework will create the fifo pipes. |
| DEVICE_FIFO_PATH = '/data/data/org.chromium.native_test/files/' |
| |
| DRT_APP_PACKAGE = 'org.chromium.native_test' |
| DRT_ACTIVITY_FULL_NAME = DRT_APP_PACKAGE + '/.ChromeNativeTestActivity' |
| DRT_APP_CACHE_DIR = DEVICE_DRT_DIR + 'cache/' |
| DRT_LIBRARY_NAME = 'libDumpRenderTree.so' |
| |
| SCALING_GOVERNORS_PATTERN = "/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor" |
| KPTR_RESTRICT_PATH = "/proc/sys/kernel/kptr_restrict" |
| |
| # All the test cases are still served to DumpRenderTree through file protocol, |
| # but we use a file-to-http feature to bridge the file request to host's http |
| # server to get the real test files and corresponding resources. |
| # See webkit/support/platform_support_android.cc for the other side of this bridge. |
| PERF_TEST_PATH_PREFIX = '/all-perf-tests' |
| LAYOUT_TEST_PATH_PREFIX = '/all-tests' |
| |
| # All ports the Android forwarder to forward. |
| # 8000, 8080 and 8443 are for http/https tests. |
| # 8880 and 9323 are for websocket tests |
| # (see http_server.py, apache_http_server.py and websocket_server.py). |
| FORWARD_PORTS = '8000 8080 8443 8880 9323' |
| |
| MS_TRUETYPE_FONTS_DIR = '/usr/share/fonts/truetype/msttcorefonts/' |
| MS_TRUETYPE_FONTS_PACKAGE = 'ttf-mscorefonts-installer' |
| |
| # Timeout in seconds to wait for start/stop of DumpRenderTree. |
| DRT_START_STOP_TIMEOUT_SECS = 10 |
| |
| # List of fonts that layout tests expect, copied from DumpRenderTree/chromium/TestShellX11.cpp. |
| HOST_FONT_FILES = [ |
| [[MS_TRUETYPE_FONTS_DIR], 'Arial.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Arial_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Arial_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Arial_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Comic_Sans_MS.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Comic_Sans_MS_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Courier_New.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Courier_New_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Courier_New_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Courier_New_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Georgia.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Georgia_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Georgia_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Georgia_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Impact.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Trebuchet_MS_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Times_New_Roman_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Verdana.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Verdana_Bold.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Verdana_Bold_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| [[MS_TRUETYPE_FONTS_DIR], 'Verdana_Italic.ttf', MS_TRUETYPE_FONTS_PACKAGE], |
| # The Microsoft font EULA |
| [['/usr/share/doc/ttf-mscorefonts-installer/'], 'READ_ME!.gz', MS_TRUETYPE_FONTS_PACKAGE], |
| # Other fonts: Arabic, CJK, Indic, Thai, etc. |
| [['/usr/share/fonts/truetype/ttf-dejavu/'], 'DejaVuSans.ttf', 'ttf-dejavu'], |
| [['/usr/share/fonts/truetype/kochi/'], 'kochi-mincho.ttf', 'ttf-kochi-mincho'], |
| [['/usr/share/fonts/truetype/ttf-indic-fonts-core/'], 'lohit_hi.ttf', 'ttf-indic-fonts-core'], |
| [['/usr/share/fonts/truetype/ttf-indic-fonts-core/'], 'lohit_ta.ttf', 'ttf-indic-fonts-core'], |
| [['/usr/share/fonts/truetype/ttf-indic-fonts-core/'], 'MuktiNarrow.ttf', 'ttf-indic-fonts-core'], |
| [['/usr/share/fonts/truetype/thai/', '/usr/share/fonts/truetype/tlwg/'], 'Garuda.ttf', 'fonts-tlwg-garuda'], |
| [['/usr/share/fonts/truetype/ttf-indic-fonts-core/', '/usr/share/fonts/truetype/ttf-punjabi-fonts/'], 'lohit_pa.ttf', 'ttf-indic-fonts-core'], |
| ] |
| |
| DEVICE_FONTS_DIR = DEVICE_DRT_DIR + 'fonts/' |
| |
| # The layout tests directory on device, which has two usages: |
| # 1. as a virtual path in file urls that will be bridged to HTTP. |
| # 2. pointing to some files that are pushed to the device for tests that |
| # don't work on file-over-http (e.g. blob protocol tests). |
| DEVICE_WEBKIT_BASE_DIR = DEVICE_SOURCE_ROOT_DIR + 'third_party/WebKit/' |
| DEVICE_LAYOUT_TESTS_DIR = DEVICE_WEBKIT_BASE_DIR + 'LayoutTests/' |
| |
| # Test resources that need to be accessed as files directly. |
| # Each item can be the relative path of a directory or a file. |
| TEST_RESOURCES_TO_PUSH = [ |
| # Blob tests need to access files directly. |
| 'editing/pasteboard/resources', |
| 'fast/files/resources', |
| 'http/tests/local/resources', |
| 'http/tests/local/formdata/resources', |
| # User style URLs are accessed as local files in webkit_support. |
| 'http/tests/security/resources/cssStyle.css', |
| # Media tests need to access audio/video as files. |
| 'media/content', |
| 'compositing/resources/video.mp4', |
| ] |
| |
| # List of test resources from webkit platform. These resources will be copied to the external storage. |
| WEBKIT_PLATFORM_RESOURCES_TO_PUSH = [ |
| 'third_party/hyphen/hyph_en_US.dic', |
| ] |
| |
| MD5SUM_DEVICE_FILE_NAME = 'md5sum_bin' |
| MD5SUM_DEVICE_PATH = '/data/local/tmp/' + MD5SUM_DEVICE_FILE_NAME |
| |
| class ChromiumAndroidPort(chromium.ChromiumPort): |
| port_name = 'chromium-android' |
| |
| # Avoid initializing the adb path [worker count]+1 times by storing it as a static member. |
| _adb_path = None |
| |
| FALLBACK_PATHS = [ |
| 'chromium-android', |
| 'chromium-linux', |
| 'chromium-win', |
| 'chromium', |
| ] |
| |
| def __init__(self, host, port_name, **kwargs): |
| super(ChromiumAndroidPort, self).__init__(host, port_name, **kwargs) |
| |
| self._operating_system = 'android' |
| self._version = 'icecreamsandwich' |
| |
| self._host_port = factory.PortFactory(host).get('chromium', **kwargs) |
| self._server_process_constructor = self._android_server_process_constructor |
| |
| if hasattr(self._options, 'adb_device'): |
| self._devices = self._options.adb_device |
| else: |
| self._devices = [] |
| |
| @staticmethod |
| def _android_server_process_constructor(port, server_name, cmd_line, env=None): |
| return server_process.ServerProcess(port, server_name, cmd_line, env, |
| universal_newlines=True, treat_no_data_as_crash=True) |
| |
| def additional_drt_flag(self): |
| # The Chromium port for Android always uses the hardware GPU path. |
| return ['--encode-binary', '--enable-hardware-gpu', |
| '--force-compositing-mode', |
| '--enable-accelerated-fixed-position'] |
| |
| def default_timeout_ms(self): |
| # Android platform has less computing power than desktop platforms. |
| # Using 10 seconds allows us to pass most slow tests which are not |
| # marked as slow tests on desktop platforms. |
| return 10 * 1000 |
| |
| def driver_stop_timeout(self): |
| # DRT doesn't respond to closing stdin, so we might as well stop the driver immediately. |
| return 0.0 |
| |
| def default_child_processes(self): |
| return len(self._get_devices()) |
| |
| def default_baseline_search_path(self): |
| return map(self._webkit_baseline_path, self.FALLBACK_PATHS) |
| |
| def check_wdiff(self, logging=True): |
| return self._host_port.check_wdiff(logging) |
| |
| def check_build(self, needs_http): |
| result = super(ChromiumAndroidPort, self).check_build(needs_http) |
| result = self._check_file_exists(self.path_to_md5sum(), 'md5sum utility') and result |
| result = self._check_file_exists(self.path_to_forwarder(), 'forwarder utility') and result |
| if not result: |
| _log.error('For complete Android build requirements, please see:') |
| _log.error('') |
| _log.error(' http://code.google.com/p/chromium/wiki/AndroidBuildInstructions') |
| |
| return result |
| |
| def check_sys_deps(self, needs_http): |
| for (font_dirs, font_file, package) in HOST_FONT_FILES: |
| exists = False |
| for font_dir in font_dirs: |
| font_path = font_dir + font_file |
| if self._check_file_exists(font_path, '', logging=False): |
| exists = True |
| break |
| if not exists: |
| _log.error('You are missing %s under %s. Try installing %s. See build instructions.' % (font_file, font_dirs, package)) |
| return False |
| return True |
| |
| def _port_specific_expectations_files(self): |
| # LayoutTests/platform/chromium-android/TestExpectations should contain only the rules to |
| # skip tests for the features not supported or not testable on Android. |
| # Other rules should be in LayoutTests/platform/chromium/TestExpectations. |
| android_expectations_file = self.path_from_webkit_base('LayoutTests', 'platform', 'chromium-android', 'TestExpectations') |
| return super(ChromiumAndroidPort, self)._port_specific_expectations_files() + [android_expectations_file] |
| |
| def requires_http_server(self): |
| """Chromium Android runs tests on devices, and uses the HTTP server to |
| serve the actual layout tests to DumpRenderTree.""" |
| return True |
| |
| def start_http_server(self, additional_dirs=None, number_of_servers=0): |
| if not additional_dirs: |
| additional_dirs = {} |
| additional_dirs[PERF_TEST_PATH_PREFIX] = self.perf_tests_dir() |
| additional_dirs[LAYOUT_TEST_PATH_PREFIX] = self.layout_tests_dir() |
| super(ChromiumAndroidPort, self).start_http_server(additional_dirs, number_of_servers) |
| |
| def create_driver(self, worker_number, no_timeout=False): |
| # We don't want the default DriverProxy which is not compatible with our driver. |
| # See comments in ChromiumAndroidDriver.start(). |
| return ChromiumAndroidDriver(self, worker_number, pixel_tests=self.get_option('pixel_tests'), |
| # Force no timeout to avoid DumpRenderTree timeouts before NRWT. |
| no_timeout=True) |
| |
| def driver_cmd_line(self): |
| # Override to return the actual DumpRenderTree command line. |
| return self.create_driver(0)._drt_cmd_line(self.get_option('pixel_tests'), []) |
| |
| def path_to_adb(self): |
| if ChromiumAndroidPort._adb_path: |
| return ChromiumAndroidPort._adb_path |
| |
| provided_adb_path = self.path_from_chromium_base('third_party', 'android_tools', 'sdk', 'platform-tools', 'adb') |
| |
| path_version = self._determine_adb_version('adb') |
| provided_version = self._determine_adb_version(provided_adb_path) |
| assert provided_version, 'The checked in Android SDK is missing. Are you sure you ran update-webkit --chromium-android?' |
| |
| if not path_version: |
| ChromiumAndroidPort._adb_path = provided_adb_path |
| elif provided_version > path_version: |
| _log.warning('The "adb" version in your path is older than the one checked in, consider updating your local Android SDK. Using the checked in one.') |
| ChromiumAndroidPort._adb_path = provided_adb_path |
| else: |
| ChromiumAndroidPort._adb_path = 'adb' |
| |
| return ChromiumAndroidPort._adb_path |
| |
| def path_to_forwarder(self): |
| return self._build_path('forwarder') |
| |
| def path_to_md5sum(self): |
| return self._build_path(MD5SUM_DEVICE_FILE_NAME) |
| |
| # Overridden private functions. |
| |
| def _build_path(self, *comps): |
| return self._host_port._build_path(*comps) |
| |
| def _build_path_with_configuration(self, configuration, *comps): |
| return self._host_port._build_path_with_configuration(configuration, *comps) |
| |
| def _path_to_apache(self): |
| return self._host_port._path_to_apache() |
| |
| def _path_to_apache_config_file(self): |
| return self._host_port._path_to_apache_config_file() |
| |
| def _path_to_driver(self, configuration=None): |
| return self._build_path_with_configuration(configuration, 'DumpRenderTree_apk/DumpRenderTree-debug.apk') |
| |
| def _path_to_helper(self): |
| return None |
| |
| def _path_to_image_diff(self): |
| return self._host_port._path_to_image_diff() |
| |
| def _path_to_lighttpd(self): |
| return self._host_port._path_to_lighttpd() |
| |
| def _path_to_lighttpd_modules(self): |
| return self._host_port._path_to_lighttpd_modules() |
| |
| def _path_to_lighttpd_php(self): |
| return self._host_port._path_to_lighttpd_php() |
| |
| def _path_to_wdiff(self): |
| return self._host_port._path_to_wdiff() |
| |
| def _shut_down_http_server(self, pid): |
| return self._host_port._shut_down_http_server(pid) |
| |
| def _driver_class(self): |
| return ChromiumAndroidDriver |
| |
| # Local private functions. |
| |
| def _determine_adb_version(self, adb_path): |
| re_version = re.compile('^.*version ([\d\.]+)$') |
| try: |
| output = self._executive.run_command([adb_path, 'version'], error_handler=self._executive.ignore_error) |
| except OSError: |
| return None |
| result = re_version.match(output) |
| if not output or not result: |
| return None |
| return [int(n) for n in result.group(1).split('.')] |
| |
| def _get_devices(self): |
| if not self._devices: |
| re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) |
| result = self._executive.run_command([self.path_to_adb(), 'devices'], error_handler=self._executive.ignore_error) |
| self._devices = re_device.findall(result) |
| if not self._devices: |
| raise AssertionError('No devices attached. Result of "adb devices": %s' % result) |
| return self._devices |
| |
| def _get_device_serial(self, worker_number): |
| devices = self._get_devices() |
| if worker_number >= len(devices): |
| raise AssertionError('Worker number exceeds available number of devices') |
| return devices[worker_number] |
| |
| |
| class AndroidPerf(SingleFileOutputProfiler): |
| _cached_perf_host_path = None |
| _have_searched_for_perf_host = False |
| |
| def __init__(self, host, executable_path, output_dir, adb_path, device_serial, symfs_path, kallsyms_path, identifier=None): |
| super(AndroidPerf, self).__init__(host, executable_path, output_dir, "data", identifier) |
| self._device_serial = device_serial |
| self._adb_command = [adb_path, '-s', self._device_serial] |
| self._perf_process = None |
| self._symfs_path = symfs_path |
| self._kallsyms_path = kallsyms_path |
| |
| def check_configuration(self): |
| # Check that perf is installed |
| if not self._file_exists_on_device('/system/bin/perf'): |
| print "Cannot find /system/bin/perf on device %s" % self._device_serial |
| return False |
| # Check that the device is a userdebug build (or at least has the necessary libraries). |
| if self._run_adb_command(['shell', 'getprop', 'ro.build.type']).strip() != 'userdebug': |
| print "Device %s is not flashed with a userdebug build of Android" % self._device_serial |
| return False |
| # FIXME: Check that the binary actually is perf-able (has stackframe pointers)? |
| # objdump -s a function and make sure it modifies the fp? |
| # Instruct users to rebuild after export GYP_DEFINES="profiling=1 $GYP_DEFINES" |
| return True |
| |
| def print_setup_instructions(self): |
| print """ |
| perf on android requires a 'userdebug' build of Android, see: |
| http://source.android.com/source/building-devices.html" |
| |
| The perf command can be built from: |
| https://android.googlesource.com/platform/external/linux-tools-perf/ |
| and requires libefl, libebl, libdw, and libdwfl available in: |
| https://android.googlesource.com/platform/external/elfutils/ |
| |
| DumpRenderTree must be built with profiling=1, make sure you've done: |
| export GYP_DEFINES="profiling=1 $GYP_DEFINES" |
| update-webkit --chromium-android |
| build-webkit --chromium-android |
| |
| Googlers should read: |
| http://goto.google.com/cr-android-perf-howto |
| """ |
| |
| def _file_exists_on_device(self, full_file_path): |
| assert full_file_path.startswith('/') |
| return self._run_adb_command(['shell', 'ls', full_file_path]).strip() == full_file_path |
| |
| def _run_adb_command(self, cmd): |
| return self._host.executive.run_command(self._adb_command + cmd) |
| |
| def attach_to_pid(self, pid): |
| assert(pid) |
| assert(self._perf_process == None) |
| # FIXME: This can't be a fixed timeout! |
| cmd = self._adb_command + ['shell', 'perf', 'record', '-g', '-p', pid, 'sleep', 30] |
| self._perf_process = self._host.executive.popen(cmd) |
| |
| def _perf_version_string(self, perf_path): |
| try: |
| return self._host.executive.run_command([perf_path, '--version']) |
| except: |
| return None |
| |
| def _find_perfhost_binary(self): |
| perfhost_version = self._perf_version_string('perfhost_linux') |
| if perfhost_version: |
| return 'perfhost_linux' |
| perf_version = self._perf_version_string('perf') |
| if perf_version: |
| return 'perf' |
| return None |
| |
| def _perfhost_path(self): |
| if self._have_searched_for_perf_host: |
| return self._cached_perf_host_path |
| self._have_searched_for_perf_host = True |
| self._cached_perf_host_path = self._find_perfhost_binary() |
| return self._cached_perf_host_path |
| |
| def _first_ten_lines_of_profile(self, perf_output): |
| match = re.search("^#[^\n]*\n((?: [^\n]*\n){1,10})", perf_output, re.MULTILINE) |
| return match.group(1) if match else None |
| |
| def profile_after_exit(self): |
| perf_exitcode = self._perf_process.wait() |
| if perf_exitcode != 0: |
| print "Perf failed (exit code: %i), can't process results." % perf_exitcode |
| return |
| self._run_adb_command(['pull', '/data/perf.data', self._output_path]) |
| |
| perfhost_path = self._perfhost_path() |
| perfhost_report_command = [ |
| 'report', |
| '--input', self._output_path, |
| '--symfs', self._symfs_path, |
| '--kallsyms', self._kallsyms_path, |
| ] |
| if perfhost_path: |
| perfhost_args = [perfhost_path] + perfhost_report_command + ['--call-graph', 'none'] |
| perf_output = self._host.executive.run_command(perfhost_args) |
| # We could save off the full -g report to a file if users found that useful. |
| print self._first_ten_lines_of_profile(perf_output) |
| else: |
| print """ |
| Failed to find perfhost_linux binary, can't process samples from the device. |
| |
| perfhost_linux can be built from: |
| https://android.googlesource.com/platform/external/linux-tools-perf/ |
| also, modern versions of perf (available from apt-get install goobuntu-kernel-tools-common) |
| may also be able to process the perf.data files from the device. |
| |
| Googlers should read: |
| http://goto.google.com/cr-android-perf-howto |
| for instructions on installing pre-built copies of perfhost_linux |
| http://crbug.com/165250 discusses making these pre-built binaries externally available. |
| """ |
| |
| perfhost_display_patch = perfhost_path if perfhost_path else 'perfhost_linux' |
| print "To view the full profile, run:" |
| print ' '.join([perfhost_display_patch] + perfhost_report_command) |
| |
| |
| class ChromiumAndroidDriver(driver.Driver): |
| def __init__(self, port, worker_number, pixel_tests, no_timeout=False): |
| super(ChromiumAndroidDriver, self).__init__(port, worker_number, pixel_tests, no_timeout) |
| self._cmd_line = None |
| self._in_fifo_path = DEVICE_FIFO_PATH + 'stdin.fifo' |
| self._out_fifo_path = DEVICE_FIFO_PATH + 'test.fifo' |
| self._err_fifo_path = DEVICE_FIFO_PATH + 'stderr.fifo' |
| self._read_stdout_process = None |
| self._read_stderr_process = None |
| self._forwarder_process = None |
| self._has_setup = False |
| self._original_governors = {} |
| self._original_kptr_restrict = None |
| self._device_serial = port._get_device_serial(worker_number) |
| self._adb_command_base = None |
| |
| # FIXME: If we taught ProfileFactory about "target" devices we could |
| # just use the logic in Driver instead of duplicating it here. |
| if self._port.get_option("profile"): |
| # FIXME: This should be done once, instead of per-driver! |
| symfs_path = self._find_or_create_symfs() |
| kallsyms_path = self._update_kallsyms_cache(symfs_path) |
| # FIXME: We should pass this some sort of "Bridge" object abstraction around ADB instead of a path/device pair. |
| self._profiler = AndroidPerf(self._port.host, self._port._path_to_driver(), self._port.results_directory(), |
| self._port.path_to_adb(), self._device_serial, symfs_path, kallsyms_path) |
| # FIXME: This is a layering violation and should be moved to Port.check_sys_deps |
| # once we have an abstraction around an adb_path/device_serial pair to make it |
| # easy to make these class methods on AndroidPerf. |
| if not self._profiler.check_configuration(): |
| self._profiler.print_setup_instructions() |
| sys.exit(1) |
| else: |
| self._profiler = None |
| |
| def __del__(self): |
| self._teardown_performance() |
| super(ChromiumAndroidDriver, self).__del__() |
| |
| def _update_kallsyms_cache(self, output_dir): |
| kallsyms_name = "%s-kallsyms" % self._device_serial |
| kallsyms_cache_path = self._port.host.filesystem.join(output_dir, kallsyms_name) |
| |
| self._restart_adb_as_root() |
| |
| saved_kptr_restrict = self._run_adb_command(['shell', 'cat', KPTR_RESTRICT_PATH]).strip() |
| self._run_adb_command(['shell', 'echo', '0', '>', KPTR_RESTRICT_PATH]) |
| |
| print "Updating kallsyms file (%s) from device" % kallsyms_cache_path |
| self._pull_from_device("/proc/kallsyms", kallsyms_cache_path) |
| |
| self._run_adb_command(['shell', 'echo', saved_kptr_restrict, '>', KPTR_RESTRICT_PATH]) |
| |
| return kallsyms_cache_path |
| |
| def _find_or_create_symfs(self): |
| environment = self._port.host.copy_current_environment() |
| env = environment.to_dictionary() |
| fs = self._port.host.filesystem |
| |
| if 'ANDROID_SYMFS' in env: |
| symfs_path = env['ANDROID_SYMFS'] |
| else: |
| symfs_path = fs.join(self._port.results_directory(), 'symfs') |
| print "ANDROID_SYMFS not set, using %s" % symfs_path |
| |
| # find the installed path, and the path of the symboled built library |
| # FIXME: We should get the install path from the device! |
| symfs_library_path = fs.join(symfs_path, "data/app-lib/%s-1/%s" % (DRT_APP_PACKAGE, DRT_LIBRARY_NAME)) |
| built_library_path = self._port._build_path('lib', DRT_LIBRARY_NAME) |
| assert(fs.exists(built_library_path)) |
| |
| # FIXME: Ideally we'd check the sha1's first and make a soft-link instead of copying (since we probably never care about windows). |
| print "Updating symfs libary (%s) from built copy (%s)" % (symfs_library_path, built_library_path) |
| fs.maybe_make_directory(fs.dirname(symfs_library_path)) |
| fs.copyfile(built_library_path, symfs_library_path) |
| |
| return symfs_path |
| |
| def _setup_md5sum_and_push_data_if_needed(self): |
| self._md5sum_path = self._port.path_to_md5sum() |
| if not self._file_exists_on_device(MD5SUM_DEVICE_PATH): |
| if not self._push_to_device(self._md5sum_path, MD5SUM_DEVICE_PATH): |
| raise AssertionError('Could not push md5sum to device') |
| |
| self._push_executable() |
| self._push_fonts() |
| self._push_test_resources() |
| self._push_platform_resources() |
| |
| def _setup_test(self): |
| if self._has_setup: |
| return |
| |
| self._restart_adb_as_root() |
| self._setup_md5sum_and_push_data_if_needed() |
| self._has_setup = True |
| self._setup_performance() |
| |
| # Required by webkit_support::GetWebKitRootDirFilePath(). |
| # Other directories will be created automatically by adb push. |
| self._run_adb_command(['shell', 'mkdir', '-p', DEVICE_SOURCE_ROOT_DIR + 'chrome']) |
| |
| # Allow the DumpRenderTree app to fully access the directory. |
| # The native code needs the permission to write temporary files and create pipes here. |
| self._run_adb_command(['shell', 'mkdir', '-p', DEVICE_DRT_DIR]) |
| self._run_adb_command(['shell', 'chmod', '777', DEVICE_DRT_DIR]) |
| |
| # Delete the disk cache if any to ensure a clean test run. |
| # This is like what's done in ChromiumPort.setup_test_run but on the device. |
| self._run_adb_command(['shell', 'rm', '-r', DRT_APP_CACHE_DIR]) |
| |
| def _log_error(self, message): |
| _log.error('[%s] %s' % (self._device_serial, message)) |
| |
| def _log_debug(self, message): |
| _log.debug('[%s] %s' % (self._device_serial, message)) |
| |
| def _abort(self, message): |
| raise AssertionError('[%s] %s' % (self._device_serial, message)) |
| |
| @staticmethod |
| def _extract_hashes_from_md5sum_output(md5sum_output): |
| assert md5sum_output |
| return [line.split(' ')[0] for line in md5sum_output] |
| |
| def _push_file_if_needed(self, host_file, device_file): |
| assert os.path.exists(host_file) |
| device_hashes = self._extract_hashes_from_md5sum_output( |
| self._port.host.executive.popen(self._adb_command() + ['shell', MD5SUM_DEVICE_PATH, device_file], |
| stdout=subprocess.PIPE).stdout) |
| host_hashes = self._extract_hashes_from_md5sum_output( |
| self._port.host.executive.popen(args=['%s_host' % self._md5sum_path, host_file], |
| stdout=subprocess.PIPE).stdout) |
| if host_hashes and device_hashes == host_hashes: |
| return |
| self._push_to_device(host_file, device_file) |
| |
| def _push_executable(self): |
| self._push_file_if_needed(self._port.path_to_forwarder(), DEVICE_FORWARDER_PATH) |
| self._push_file_if_needed(self._port._build_path('DumpRenderTree.pak'), DEVICE_DRT_DIR + 'DumpRenderTree.pak') |
| self._push_file_if_needed(self._port._build_path('DumpRenderTree_resources'), DEVICE_DRT_DIR + 'DumpRenderTree_resources') |
| self._push_file_if_needed(self._port._build_path('android_main_fonts.xml'), DEVICE_DRT_DIR + 'android_main_fonts.xml') |
| self._push_file_if_needed(self._port._build_path('android_fallback_fonts.xml'), DEVICE_DRT_DIR + 'android_fallback_fonts.xml') |
| self._run_adb_command(['uninstall', DRT_APP_PACKAGE]) |
| drt_host_path = self._port._path_to_driver() |
| install_result = self._run_adb_command(['install', drt_host_path]) |
| if install_result.find('Success') == -1: |
| self._abort('Failed to install %s onto device: %s' % (drt_host_path, install_result)) |
| |
| def _push_fonts(self): |
| self._log_debug('Pushing fonts') |
| path_to_ahem_font = self._port._build_path('AHEM____.TTF') |
| self._push_file_if_needed(path_to_ahem_font, DEVICE_FONTS_DIR + 'AHEM____.TTF') |
| for (host_dirs, font_file, package) in HOST_FONT_FILES: |
| for host_dir in host_dirs: |
| host_font_path = host_dir + font_file |
| if self._port._check_file_exists(host_font_path, '', logging=False): |
| self._push_file_if_needed(host_font_path, DEVICE_FONTS_DIR + font_file) |
| |
| def _push_test_resources(self): |
| self._log_debug('Pushing test resources') |
| for resource in TEST_RESOURCES_TO_PUSH: |
| self._push_file_if_needed(self._port.layout_tests_dir() + '/' + resource, DEVICE_LAYOUT_TESTS_DIR + resource) |
| |
| def _push_platform_resources(self): |
| self._log_debug('Pushing platform resources') |
| external_storage = self._port._filesystem.join(self._run_adb_command(['shell', 'echo $EXTERNAL_STORAGE']).strip(), 'Source', 'WebKit', 'chromium') |
| for resource in WEBKIT_PLATFORM_RESOURCES_TO_PUSH: |
| self._push_file_if_needed(self._port._chromium_base_dir(self._port._filesystem) + '/' + resource, external_storage + '/' + resource) |
| |
| def _restart_adb_as_root(self): |
| output = self._run_adb_command(['root']) |
| if 'adbd is already running as root' in output: |
| return |
| elif not 'restarting adbd as root' in output: |
| self._log_error('Unrecognized output from adb root: %s' % output) |
| |
| # Regardless the output, give the device a moment to come back online. |
| self._run_adb_command(['wait-for-device']) |
| |
| def _run_adb_command(self, cmd, ignore_error=False): |
| self._log_debug('Run adb command: ' + str(cmd)) |
| if ignore_error: |
| error_handler = self._port._executive.ignore_error |
| else: |
| error_handler = None |
| result = self._port._executive.run_command(self._adb_command() + cmd, error_handler=error_handler) |
| # Limit the length to avoid too verbose output of commands like 'adb logcat' and 'cat /data/tombstones/tombstone01' |
| # whose outputs are normally printed in later logs. |
| self._log_debug('Run adb result: ' + result[:80]) |
| return result |
| |
| def _link_device_file(self, from_file, to_file, ignore_error=False): |
| # rm to_file first to make sure that ln succeeds. |
| self._run_adb_command(['shell', 'rm', to_file], ignore_error) |
| return self._run_adb_command(['shell', 'ln', '-s', from_file, to_file], ignore_error) |
| |
| def _push_to_device(self, host_path, device_path, ignore_error=False): |
| return self._run_adb_command(['push', host_path, device_path], ignore_error) |
| |
| def _pull_from_device(self, device_path, host_path, ignore_error=False): |
| return self._run_adb_command(['pull', device_path, host_path], ignore_error) |
| |
| def _get_last_stacktrace(self): |
| tombstones = self._run_adb_command(['shell', 'ls', '-n', '/data/tombstones']) |
| if not tombstones or tombstones.startswith('/data/tombstones: No such file or directory'): |
| self._log_error('DRT crashed, but no tombstone found!') |
| return '' |
| tombstones = tombstones.rstrip().split('\n') |
| last_tombstone = tombstones[0].split() |
| for tombstone in tombstones[1:]: |
| # Format of fields: |
| # 0 1 2 3 4 5 6 |
| # permission uid gid size date time filename |
| # -rw------- 1000 1000 45859 2011-04-13 06:00 tombstone_00 |
| fields = tombstone.split() |
| if (fields[4] + fields[5] >= last_tombstone[4] + last_tombstone[5]): |
| last_tombstone = fields |
| else: |
| break |
| |
| # Use Android tool vendor/google/tools/stack to convert the raw |
| # stack trace into a human readable format, if needed. |
| # It takes a long time, so don't do it here. |
| return '%s\n%s' % (' '.join(last_tombstone), |
| self._run_adb_command(['shell', 'cat', '/data/tombstones/' + last_tombstone[6]])) |
| |
| def _get_logcat(self): |
| return self._run_adb_command(['logcat', '-d', '-v', 'threadtime']) |
| |
| def _setup_performance(self): |
| # Disable CPU scaling and drop ram cache to reduce noise in tests |
| if not self._original_governors: |
| governor_files = self._run_adb_command(['shell', 'ls', SCALING_GOVERNORS_PATTERN]) |
| if governor_files.find('No such file or directory') == -1: |
| for file in governor_files.split(): |
| self._original_governors[file] = self._run_adb_command(['shell', 'cat', file]).strip() |
| self._run_adb_command(['shell', 'echo', 'performance', '>', file]) |
| |
| def _teardown_performance(self): |
| for file, original_content in self._original_governors.items(): |
| self._run_adb_command(['shell', 'echo', original_content, '>', file]) |
| self._original_governors = {} |
| |
| def _get_crash_log(self, stdout, stderr, newer_than): |
| if not stdout: |
| stdout = '' |
| stdout += '********* [%s] Logcat:\n%s' % (self._device_serial, self._get_logcat()) |
| if not stderr: |
| stderr = '' |
| stderr += '********* [%s] Tombstone file:\n%s' % (self._device_serial, self._get_last_stacktrace()) |
| return super(ChromiumAndroidDriver, self)._get_crash_log(stdout, stderr, newer_than) |
| |
| def cmd_line(self, pixel_tests, per_test_args): |
| # The returned command line is used to start _server_process. In our case, it's an interactive 'adb shell'. |
| # The command line passed to the DRT process is returned by _drt_cmd_line() instead. |
| return self._adb_command() + ['shell'] |
| |
| def _file_exists_on_device(self, full_file_path): |
| assert full_file_path.startswith('/') |
| return self._run_adb_command(['shell', 'ls', full_file_path]).strip() == full_file_path |
| |
| def _drt_cmd_line(self, pixel_tests, per_test_args): |
| return driver.Driver.cmd_line(self, pixel_tests, per_test_args) + ['--create-stdin-fifo', '--separate-stderr-fifo'] |
| |
| @staticmethod |
| def _loop_with_timeout(condition, timeout_secs): |
| deadline = time.time() + timeout_secs |
| while time.time() < deadline: |
| if condition(): |
| return True |
| return False |
| |
| def _all_pipes_created(self): |
| return (self._file_exists_on_device(self._in_fifo_path) and |
| self._file_exists_on_device(self._out_fifo_path) and |
| self._file_exists_on_device(self._err_fifo_path)) |
| |
| def _remove_all_pipes(self): |
| for file in [self._in_fifo_path, self._out_fifo_path, self._err_fifo_path]: |
| self._run_adb_command(['shell', 'rm', file]) |
| |
| return (not self._file_exists_on_device(self._in_fifo_path) and |
| not self._file_exists_on_device(self._out_fifo_path) and |
| not self._file_exists_on_device(self._err_fifo_path)) |
| |
| def run_test(self, driver_input, stop_when_done): |
| base = self._port.lookup_virtual_test_base(driver_input.test_name) |
| if base: |
| driver_input = copy.copy(driver_input) |
| driver_input.args = self._port.lookup_virtual_test_args(driver_input.test_name) |
| driver_input.test_name = base |
| return super(ChromiumAndroidDriver, self).run_test(driver_input, stop_when_done) |
| |
| def start(self, pixel_tests, per_test_args): |
| # Only one driver instance is allowed because of the nature of Android activity. |
| # The single driver needs to restart DumpRenderTree when the command line changes. |
| cmd_line = self._drt_cmd_line(pixel_tests, per_test_args) |
| if cmd_line != self._cmd_line: |
| self.stop() |
| self._cmd_line = cmd_line |
| super(ChromiumAndroidDriver, self).start(pixel_tests, per_test_args) |
| |
| def _start(self, pixel_tests, per_test_args): |
| self._setup_test() |
| |
| for retries in range(3): |
| if self._start_once(pixel_tests, per_test_args): |
| return |
| self._log_error('Failed to start DumpRenderTree application. Retries=%d. Log:%s' % (retries, self._get_logcat())) |
| self.stop() |
| time.sleep(2) |
| self._abort('Failed to start DumpRenderTree application multiple times. Give up.') |
| |
| def _start_once(self, pixel_tests, per_test_args): |
| super(ChromiumAndroidDriver, self)._start(pixel_tests, per_test_args) |
| |
| self._log_debug('Starting forwarder') |
| self._forwarder_process = self._port._server_process_constructor( |
| self._port, 'Forwarder', self._adb_command() + ['shell', '%s -D %s' % (DEVICE_FORWARDER_PATH, FORWARD_PORTS)]) |
| self._forwarder_process.start() |
| |
| self._run_adb_command(['logcat', '-c']) |
| self._run_adb_command(['shell', 'echo'] + self._cmd_line + ['>', COMMAND_LINE_FILE]) |
| start_result = self._run_adb_command(['shell', 'am', 'start', '-e', 'RunInSubThread', '-n', DRT_ACTIVITY_FULL_NAME]) |
| if start_result.find('Exception') != -1: |
| self._log_error('Failed to start DumpRenderTree application. Exception:\n' + start_result) |
| return False |
| |
| if not ChromiumAndroidDriver._loop_with_timeout(self._all_pipes_created, DRT_START_STOP_TIMEOUT_SECS): |
| return False |
| |
| # Read back the shell prompt to ensure adb shell ready. |
| deadline = time.time() + DRT_START_STOP_TIMEOUT_SECS |
| self._server_process.start() |
| self._read_prompt(deadline) |
| self._log_debug('Interactive shell started') |
| |
| # Start a process to read from the stdout fifo of the DumpRenderTree app and print to stdout. |
| self._log_debug('Redirecting stdout to ' + self._out_fifo_path) |
| self._read_stdout_process = self._port._server_process_constructor( |
| self._port, 'ReadStdout', self._adb_command() + ['shell', 'cat', self._out_fifo_path]) |
| self._read_stdout_process.start() |
| |
| # Start a process to read from the stderr fifo of the DumpRenderTree app and print to stdout. |
| self._log_debug('Redirecting stderr to ' + self._err_fifo_path) |
| self._read_stderr_process = self._port._server_process_constructor( |
| self._port, 'ReadStderr', self._adb_command() + ['shell', 'cat', self._err_fifo_path]) |
| self._read_stderr_process.start() |
| |
| self._log_debug('Redirecting stdin to ' + self._in_fifo_path) |
| self._server_process.write('cat >%s\n' % self._in_fifo_path) |
| |
| # Combine the stdout and stderr pipes into self._server_process. |
| self._server_process.replace_outputs(self._read_stdout_process._proc.stdout, self._read_stderr_process._proc.stdout) |
| |
| def deadlock_detector(processes, normal_startup_event): |
| if not ChromiumAndroidDriver._loop_with_timeout(lambda: normal_startup_event.is_set(), DRT_START_STOP_TIMEOUT_SECS): |
| # If normal_startup_event is not set in time, the main thread must be blocked at |
| # reading/writing the fifo. Kill the fifo reading/writing processes to let the |
| # main thread escape from the deadlocked state. After that, the main thread will |
| # treat this as a crash. |
| self._log_error('Deadlock detected. Processes killed.') |
| for i in processes: |
| i.kill() |
| |
| # Start a thread to kill the pipe reading/writing processes on deadlock of the fifos during startup. |
| normal_startup_event = threading.Event() |
| threading.Thread(name='DeadlockDetector', target=deadlock_detector, |
| args=([self._server_process, self._read_stdout_process, self._read_stderr_process], normal_startup_event)).start() |
| |
| output = '' |
| line = self._server_process.read_stdout_line(deadline) |
| while not self._server_process.timed_out and not self.has_crashed() and line.rstrip() != '#READY': |
| output += line |
| line = self._server_process.read_stdout_line(deadline) |
| |
| if self._server_process.timed_out and not self.has_crashed(): |
| # DumpRenderTree crashes during startup, or when the deadlock detector detected |
| # deadlock and killed the fifo reading/writing processes. |
| _log.error('Failed to start DumpRenderTree: \n%s' % output) |
| return False |
| |
| # Inform the deadlock detector that the startup is successful without deadlock. |
| normal_startup_event.set() |
| return True |
| |
| def _pid_from_android_ps_output(self, ps_output, package_name): |
| # ps output seems to be fixed width, we only care about the name and the pid |
| # u0_a72 21630 125 947920 59364 ffffffff 400beee4 S org.chromium.native_test |
| for line in ps_output.split('\n'): |
| if line.find(DRT_APP_PACKAGE) != -1: |
| match = re.match(r'\S+\s+(\d+)', line) |
| return int(match.group(1)) |
| |
| def _pid_on_target(self): |
| # FIXME: There must be a better way to do this than grepping ps output! |
| ps_output = self._run_adb_command(['shell', 'ps']) |
| return self._pid_from_android_ps_output(ps_output, DRT_APP_PACKAGE) |
| |
| def stop(self): |
| self._run_adb_command(['shell', 'am', 'force-stop', DRT_APP_PACKAGE]) |
| |
| if self._read_stdout_process: |
| self._read_stdout_process.kill() |
| self._read_stdout_process = None |
| |
| if self._read_stderr_process: |
| self._read_stderr_process.kill() |
| self._read_stderr_process = None |
| |
| super(ChromiumAndroidDriver, self).stop() |
| |
| if self._forwarder_process: |
| self._forwarder_process.kill() |
| self._forwarder_process = None |
| |
| if self._has_setup: |
| if not ChromiumAndroidDriver._loop_with_timeout(self._remove_all_pipes, DRT_START_STOP_TIMEOUT_SECS): |
| raise AssertionError('Failed to remove fifo files. May be locked.') |
| |
| def _command_from_driver_input(self, driver_input): |
| command = super(ChromiumAndroidDriver, self)._command_from_driver_input(driver_input) |
| if command.startswith('/'): |
| fs = self._port._filesystem |
| # FIXME: what happens if command lies outside of the layout_tests_dir on the host? |
| relative_test_filename = fs.relpath(command, fs.dirname(self._port.layout_tests_dir())) |
| command = DEVICE_WEBKIT_BASE_DIR + relative_test_filename |
| return command |
| |
| def _read_prompt(self, deadline): |
| last_char = '' |
| while True: |
| current_char = self._server_process.read_stdout(deadline, 1) |
| if current_char == ' ': |
| if last_char in ('#', '$'): |
| return |
| last_char = current_char |
| |
| def _adb_command(self): |
| if not self._adb_command_base: |
| self._adb_command_base = [self._port.path_to_adb(), '-s', self._device_serial] |
| return self._adb_command_base |