blob: 66774423c58030b387eb4fef5fe05b3074d9c9a0 [file] [log] [blame]
# Copyright 2014 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 glob
import hashlib
import logging
import os
import platform
import re
import shutil
import subprocess
from telemetry.internal.util import binary_manager
from telemetry.core import platform as telemetry_platform
from telemetry.core import util
from telemetry import decorators
from telemetry.internal.platform.profiler import android_prebuilt_profiler_helper
from devil.android import md5sum # pylint: disable=import-error
try:
import sqlite3
except ImportError:
sqlite3 = None
_TEXT_SECTION = '.text'
def _ElfMachineId(elf_file):
headers = subprocess.check_output(['readelf', '-h', elf_file])
return re.match(r'.*Machine:\s+(\w+)', headers, re.DOTALL).group(1)
def _ElfSectionAsString(elf_file, section):
return subprocess.check_output(['readelf', '-p', section, elf_file])
def _ElfSectionMd5Sum(elf_file, section):
result = subprocess.check_output(
'readelf -p%s "%s" | md5sum' % (section, elf_file), shell=True)
return result.split(' ', 1)[0]
def _FindMatchingUnstrippedLibraryOnHost(device, lib):
lib_base = os.path.basename(lib)
device_md5 = device.RunShellCommand('md5 "%s"' % lib, as_root=True)[0]
device_md5 = device_md5.split(' ', 1)[0]
def FindMatchingStrippedLibrary(out_path):
# First find a matching stripped library on the host. This avoids the need
# to pull the stripped library from the device, which can take tens of
# seconds.
host_lib_pattern = os.path.join(out_path, '*_apk', 'libs', '*', lib_base)
for stripped_host_lib in glob.glob(host_lib_pattern):
with open(stripped_host_lib) as f:
host_md5 = hashlib.md5(f.read()).hexdigest()
if host_md5 == device_md5:
return stripped_host_lib
return None
out_path = None
stripped_host_lib = None
for out_path in util.GetBuildDirectories():
stripped_host_lib = FindMatchingStrippedLibrary(out_path)
if stripped_host_lib:
break
if not stripped_host_lib:
return None
# The corresponding unstripped library will be under out/Release/lib.
unstripped_host_lib = os.path.join(out_path, 'lib', lib_base)
# Make sure the unstripped library matches the stripped one. We do this
# by comparing the hashes of text sections in both libraries. This isn't an
# exact guarantee, but should still give reasonable confidence that the
# libraries are compatible.
# TODO(skyostil): Check .note.gnu.build-id instead once we're using
# --build-id=sha1.
# pylint: disable=undefined-loop-variable
if (_ElfSectionMd5Sum(unstripped_host_lib, _TEXT_SECTION) !=
_ElfSectionMd5Sum(stripped_host_lib, _TEXT_SECTION)):
return None
return unstripped_host_lib
@decorators.Cache
def GetPerfhostName():
return 'perfhost_' + telemetry_platform.GetHostPlatform().GetOSVersionName()
# Ignored directories for libraries that aren't useful for symbolization.
_IGNORED_LIB_PATHS = [
'/data/dalvik-cache',
'/tmp'
]
def GetRequiredLibrariesForPerfProfile(profile_file):
"""Returns the set of libraries necessary to symbolize a given perf profile.
Args:
profile_file: Path to perf profile to analyse.
Returns:
A set of required library file names.
"""
with open(os.devnull, 'w') as dev_null:
perfhost_path = binary_manager.FetchPath(
GetPerfhostName(), 'x86_64', 'linux')
perf = subprocess.Popen([perfhost_path, 'script', '-i', profile_file],
stdout=dev_null, stderr=subprocess.PIPE)
_, output = perf.communicate()
missing_lib_re = re.compile(
('^Failed to open (.*), continuing without symbols|'
'^(.*[.]so).*not found, continuing without symbols'))
libs = set()
for line in output.split('\n'):
lib = missing_lib_re.match(line)
if lib:
lib = lib.group(1) or lib.group(2)
path = os.path.dirname(lib)
if (any(path.startswith(ignored_path)
for ignored_path in _IGNORED_LIB_PATHS)
or path == '/' or not path):
continue
libs.add(lib)
return libs
def GetRequiredLibrariesForVTuneProfile(profile_file):
"""Returns the set of libraries necessary to symbolize a given VTune profile.
Args:
profile_file: Path to VTune profile to analyse.
Returns:
A set of required library file names.
"""
db_file = os.path.join(profile_file, 'sqlite-db', 'dicer.db')
conn = sqlite3.connect(db_file)
try:
# The 'dd_module_file' table lists all libraries on the device. Only the
# ones with 'bin_located_path' are needed for the profile.
query = 'SELECT bin_path, bin_located_path FROM dd_module_file'
return set(row[0] for row in conn.cursor().execute(query) if row[1])
finally:
conn.close()
def _FileMetadataMatches(filea, fileb):
"""Check if the metadata of two files matches."""
assert os.path.exists(filea)
if not os.path.exists(fileb):
return False
fields_to_compare = [
'st_ctime', 'st_gid', 'st_mode', 'st_mtime', 'st_size', 'st_uid']
filea_stat = os.stat(filea)
fileb_stat = os.stat(fileb)
for field in fields_to_compare:
# shutil.copy2 doesn't get ctime/mtime identical when the file system
# provides sub-second accuracy.
if int(getattr(filea_stat, field)) != int(getattr(fileb_stat, field)):
return False
return True
def CreateSymFs(device, symfs_dir, libraries, use_symlinks=True):
"""Creates a symfs directory to be used for symbolizing profiles.
Prepares a set of files ("symfs") to be used with profilers such as perf for
converting binary addresses into human readable function names.
Args:
device: DeviceUtils instance identifying the target device.
symfs_dir: Path where the symfs should be created.
libraries: Set of library file names that should be included in the symfs.
use_symlinks: If True, link instead of copy unstripped libraries into the
symfs. This will speed up the operation, but the resulting symfs will no
longer be valid if the linked files are modified, e.g., by rebuilding.
Returns:
The absolute path to the kernel symbols within the created symfs.
"""
logging.info('Building symfs into %s.' % symfs_dir)
for lib in libraries:
device_dir = os.path.dirname(lib)
output_dir = os.path.join(symfs_dir, device_dir[1:])
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_lib = os.path.join(output_dir, os.path.basename(lib))
if lib.startswith('/data/app'):
# If this is our own library instead of a system one, look for a matching
# unstripped library under the out directory.
unstripped_host_lib = _FindMatchingUnstrippedLibraryOnHost(device, lib)
if not unstripped_host_lib:
logging.warning('Could not find symbols for %s.' % lib)
logging.warning('Is the correct output directory selected '
'(CHROMIUM_OUT_DIR)? Did you install the APK after '
'building?')
continue
if use_symlinks:
if os.path.lexists(output_lib):
os.remove(output_lib)
os.symlink(os.path.abspath(unstripped_host_lib), output_lib)
# Copy the unstripped library only if it has been changed to avoid the
# delay.
elif not _FileMetadataMatches(unstripped_host_lib, output_lib):
logging.info('Copying %s to %s' % (unstripped_host_lib, output_lib))
shutil.copy2(unstripped_host_lib, output_lib)
else:
# Otherwise save a copy of the stripped system library under the symfs so
# the profiler can at least use the public symbols of that library. To
# speed things up, only pull files that don't match copies we already
# have in the symfs.
if not os.path.exists(output_lib):
pull = True
else:
host_md5sums = md5sum.CalculateHostMd5Sums([output_lib])
try:
device_md5sums = md5sum.CalculateDeviceMd5Sums([lib], device)
except:
logging.exception('New exception caused by DeviceUtils conversion')
raise
pull = True
if host_md5sums and device_md5sums and output_lib in host_md5sums \
and lib in device_md5sums:
pull = host_md5sums[output_lib] != device_md5sums[lib]
if pull:
logging.info('Pulling %s to %s', lib, output_lib)
device.PullFile(lib, output_lib)
# Also pull a copy of the kernel symbols.
output_kallsyms = os.path.join(symfs_dir, 'kallsyms')
if not os.path.exists(output_kallsyms):
device.PullFile('/proc/kallsyms', output_kallsyms)
return output_kallsyms
def PrepareDeviceForPerf(device):
"""Set up a device for running perf.
Args:
device: DeviceUtils instance identifying the target device.
Returns:
The path to the installed perf binary on the device.
"""
android_prebuilt_profiler_helper.InstallOnDevice(device, 'perf')
# Make sure kernel pointers are not hidden.
device.WriteFile('/proc/sys/kernel/kptr_restrict', '0', as_root=True)
return android_prebuilt_profiler_helper.GetDevicePath('perf')
def GetToolchainBinaryPath(library_file, binary_name):
"""Return the path to an Android toolchain binary on the host.
Args:
library_file: ELF library which is used to identify the used ABI,
architecture and toolchain.
binary_name: Binary to search for, e.g., 'objdump'
Returns:
Full path to binary or None if the binary was not found.
"""
# Mapping from ELF machine identifiers to GNU toolchain names.
toolchain_configs = {
'x86': 'i686-linux-android',
'MIPS': 'mipsel-linux-android',
'ARM': 'arm-linux-androideabi',
'x86-64': 'x86_64-linux-android',
'AArch64': 'aarch64-linux-android',
}
toolchain_config = toolchain_configs[_ElfMachineId(library_file)]
host_os = platform.uname()[0].lower()
host_machine = platform.uname()[4]
elf_comment = _ElfSectionAsString(library_file, '.comment')
toolchain_version = re.match(r'.*GCC: \(GNU\) ([\w.]+)',
elf_comment, re.DOTALL)
if not toolchain_version:
return None
toolchain_version = toolchain_version.group(1)
toolchain_version = toolchain_version.replace('.x', '')
toolchain_path = os.path.abspath(os.path.join(
util.GetChromiumSrcDir(), 'third_party', 'android_tools', 'ndk',
'toolchains', '%s-%s' % (toolchain_config, toolchain_version)))
if not os.path.exists(toolchain_path):
logging.warning(
'Unable to find toolchain binary %s: toolchain not found at %s',
binary_name, toolchain_path)
return None
path = os.path.join(
toolchain_path, 'prebuilt', '%s-%s' % (host_os, host_machine), 'bin',
'%s-%s' % (toolchain_config, binary_name))
if not os.path.exists(path):
logging.warning(
'Unable to find toolchain binary %s: binary not found at %s',
binary_name, path)
return None
return path