| Index: build/android/pylib/android_commands.py
|
| diff --git a/build/android/pylib/android_commands.py b/build/android/pylib/android_commands.py
|
| index 9fefffab1d81b211b35c69af5b10e3a0c34642b4..7a32eaa4f25de1f16fcc7cc99caac78afe033a9f 100644
|
| --- a/build/android/pylib/android_commands.py
|
| +++ b/build/android/pylib/android_commands.py
|
| @@ -5,30 +5,29 @@
|
| """Provides an interface to communicate with the device via the adb command.
|
|
|
| Assumes adb binary is currently on system path.
|
| -
|
| -Usage:
|
| - python android_commands.py wait-for-pm
|
| """
|
|
|
| import collections
|
| import datetime
|
| +import io_stats_parser
|
| import logging
|
| import optparse
|
| import os
|
| import pexpect
|
| import re
|
| +import shlex
|
| import subprocess
|
| import sys
|
| import tempfile
|
| import time
|
|
|
| +
|
| # adb_interface.py is under ../../../third_party/android_testrunner/
|
| sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..',
|
| '..', '..', 'third_party', 'android_testrunner'))
|
| import adb_interface
|
| import cmd_helper
|
| -import errors # is under ../../third_party/android_testrunner/errors.py
|
| -from run_tests_helper import IsRunningAsBuildbot
|
| +import errors # is under ../../../third_party/android_testrunner/errors.py
|
|
|
|
|
| # Pattern to search for the next whole line of pexpect output and capture it
|
| @@ -51,14 +50,21 @@ LOCAL_PROPERTIES_PATH = '/data/local.prop'
|
| JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
|
|
|
| BOOT_COMPLETE_RE = re.compile(
|
| - re.escape('android.intent.action.MEDIA_MOUNTED path: /mnt/sdcard')
|
| - + '|' + re.escape('PowerManagerService: bootCompleted'))
|
| + 'android.intent.action.MEDIA_MOUNTED path: /\w+/sdcard\d?'
|
| + + '|' + 'PowerManagerService(\(\s+\d+\))?: bootCompleted')
|
| +
|
| +MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$')
|
| +NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*'
|
| + '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$')
|
|
|
| # Keycode "enum" suitable for passing to AndroidCommands.SendKey().
|
| +KEYCODE_HOME = 3
|
| +KEYCODE_BACK = 4
|
| +KEYCODE_DPAD_UP = 19
|
| +KEYCODE_DPAD_DOWN = 20
|
| KEYCODE_DPAD_RIGHT = 22
|
| KEYCODE_ENTER = 66
|
| KEYCODE_MENU = 82
|
| -KEYCODE_BACK = 4
|
|
|
|
|
| def GetEmulators():
|
| @@ -189,10 +195,11 @@ def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None):
|
| return files
|
|
|
|
|
| -def GetLogTimestamp(log_line):
|
| - """Returns the timestamp of the given |log_line|."""
|
| +def GetLogTimestamp(log_line, year):
|
| + """Returns the timestamp of the given |log_line| in the given year."""
|
| try:
|
| - return datetime.datetime.strptime(log_line[:18], '%m-%d %H:%M:%S.%f')
|
| + return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]),
|
| + '%Y-%m-%d %H:%M:%S.%f')
|
| except (ValueError, IndexError):
|
| logging.critical('Error reading timestamp from ' + log_line)
|
| return None
|
| @@ -204,23 +211,28 @@ class AndroidCommands(object):
|
| Args:
|
| device: If given, adb commands are only send to the device of this ID.
|
| Otherwise commands are sent to all attached devices.
|
| - wait_for_pm: If true, issues an adb wait-for-device command.
|
| """
|
|
|
| - def __init__(self, device=None, wait_for_pm=False):
|
| + def __init__(self, device=None):
|
| self._adb = adb_interface.AdbInterface()
|
| if device:
|
| self._adb.SetTargetSerial(device)
|
| - if wait_for_pm:
|
| - self.WaitForDevicePm()
|
| + # So many users require root that we just always do it. This could
|
| + # be made more fine grain if necessary.
|
| + self._adb.EnableAdbRoot()
|
| self._logcat = None
|
| self._original_governor = None
|
| self._pushed_files = []
|
| + self._device_utc_offset = self.RunShellCommand('date +%z')[0]
|
|
|
| def Adb(self):
|
| """Returns our AdbInterface to avoid us wrapping all its methods."""
|
| return self._adb
|
|
|
| + def GetDeviceYear(self):
|
| + """Returns the year information of the date on device"""
|
| + return self.RunShellCommand('date +%Y')[0]
|
| +
|
| def WaitForDevicePm(self):
|
| """Blocks until the device's package manager is available.
|
|
|
| @@ -269,33 +281,43 @@ class AndroidCommands(object):
|
| self.RestartShell()
|
| self.WaitForDevicePm()
|
| self.StartMonitoringLogcat(timeout=120)
|
| - self.WaitForLogMatch(BOOT_COMPLETE_RE)
|
| - self.UnlockDevice()
|
| + self.WaitForLogMatch(BOOT_COMPLETE_RE, None)
|
|
|
| def Uninstall(self, package):
|
| """Uninstalls the specified package from the device.
|
|
|
| Args:
|
| package: Name of the package to remove.
|
| +
|
| + Returns:
|
| + A status string returned by adb uninstall
|
| """
|
| uninstall_command = 'uninstall %s' % package
|
|
|
| logging.info('>>> $' + uninstall_command)
|
| - self._adb.SendCommand(uninstall_command, timeout_time=60)
|
| + return self._adb.SendCommand(uninstall_command, timeout_time=60)
|
|
|
| def Install(self, package_file_path):
|
| """Installs the specified package to the device.
|
|
|
| Args:
|
| package_file_path: Path to .apk file to install.
|
| - """
|
|
|
| + Returns:
|
| + A status string returned by adb install
|
| + """
|
| assert os.path.isfile(package_file_path)
|
|
|
| install_command = 'install %s' % package_file_path
|
|
|
| logging.info('>>> $' + install_command)
|
| - self._adb.SendCommand(install_command, timeout_time=2*60)
|
| + return self._adb.SendCommand(install_command, timeout_time=2*60)
|
| +
|
| + def MakeSystemFolderWritable(self):
|
| + """Remounts the /system folder rw. """
|
| + out = self._adb.SendCommand('remount')
|
| + if out.strip() != 'remount succeeded':
|
| + raise errors.MsgException('Remount failed: %s' % out)
|
|
|
| # It is tempting to turn this function into a generator, however this is not
|
| # possible without using a private (local) adb_shell instance (to ensure no
|
| @@ -337,44 +359,63 @@ class AndroidCommands(object):
|
| self.RunShellCommand('kill ' + ' '.join(pids))
|
| return len(pids)
|
|
|
| - def StartActivity(self, package, activity,
|
| - action='android.intent.action.VIEW', data=None,
|
| + def StartActivity(self, package, activity, wait_for_completion=False,
|
| + action='android.intent.action.VIEW',
|
| + category=None, data=None,
|
| extras=None, trace_file_name=None):
|
| """Starts |package|'s activity on the device.
|
|
|
| Args:
|
| - package: Name of package to start (e.g. 'com.android.chrome').
|
| - activity: Name of activity (e.g. '.Main' or 'com.android.chrome.Main').
|
| + package: Name of package to start (e.g. 'com.google.android.apps.chrome').
|
| + activity: Name of activity (e.g. '.Main' or
|
| + 'com.google.android.apps.chrome.Main').
|
| + wait_for_completion: wait for the activity to finish launching (-W flag).
|
| + action: string (e.g. "android.intent.action.MAIN"). Default is VIEW.
|
| + category: string (e.g. "android.intent.category.HOME")
|
| data: Data string to pass to activity (e.g. 'http://www.example.com/').
|
| - extras: Dict of extras to pass to activity.
|
| + extras: Dict of extras to pass to activity. Values are significant.
|
| trace_file_name: If used, turns on and saves the trace to this file name.
|
| """
|
| - cmd = 'am start -a %s -n %s/%s' % (action, package, activity)
|
| + cmd = 'am start -a %s' % action
|
| + if wait_for_completion:
|
| + cmd += ' -W'
|
| + if category:
|
| + cmd += ' -c %s' % category
|
| + if package and activity:
|
| + cmd += ' -n %s/%s' % (package, activity)
|
| if data:
|
| cmd += ' -d "%s"' % data
|
| if extras:
|
| - cmd += ' -e'
|
| for key in extras:
|
| - cmd += ' %s %s' % (key, extras[key])
|
| + value = extras[key]
|
| + if isinstance(value, str):
|
| + cmd += ' --es'
|
| + elif isinstance(value, bool):
|
| + cmd += ' --ez'
|
| + elif isinstance(value, int):
|
| + cmd += ' --ei'
|
| + else:
|
| + raise NotImplementedError(
|
| + 'Need to teach StartActivity how to pass %s extras' % type(value))
|
| + cmd += ' %s %s' % (key, value)
|
| if trace_file_name:
|
| - cmd += ' -S -P ' + trace_file_name
|
| + cmd += ' --start-profiler ' + trace_file_name
|
| self.RunShellCommand(cmd)
|
|
|
| - def EnableAdbRoot(self):
|
| - """Enable root on the device."""
|
| - self._adb.EnableAdbRoot()
|
|
|
| def CloseApplication(self, package):
|
| """Attempt to close down the application, using increasing violence.
|
|
|
| Args:
|
| - package: Name of the process to kill off, e.g. com.android.chrome
|
| + package: Name of the process to kill off, e.g.
|
| + com.google.android.apps.chrome
|
| """
|
| self.RunShellCommand('am force-stop ' + package)
|
|
|
| def ClearApplicationState(self, package):
|
| """Closes and clears all state for the given |package|."""
|
| self.CloseApplication(package)
|
| + self.RunShellCommand('rm -r /data/data/%s/app_*' % package)
|
| self.RunShellCommand('rm -r /data/data/%s/cache/*' % package)
|
| self.RunShellCommand('rm -r /data/data/%s/files/*' % package)
|
| self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package)
|
| @@ -423,15 +464,16 @@ class AndroidCommands(object):
|
| push_command = 'push %s %s' % (local_path, device_path)
|
| logging.info('>>> $' + push_command)
|
| output = self._adb.SendCommand(push_command, timeout_time=30*60)
|
| + assert output
|
| # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)"
|
| # Errors look like this: "failed to copy ... "
|
| - if not re.search('^[0-9]', output):
|
| + if not re.search('^[0-9]', output.splitlines()[-1]):
|
| logging.critical('PUSH FAILED: ' + output)
|
|
|
| - def GetFileContents(self, filename):
|
| + def GetFileContents(self, filename, log_result=True):
|
| """Gets contents from the file specified by |filename|."""
|
| return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' +
|
| - filename + '"; fi')
|
| + filename + '"; fi', log_result=log_result)
|
|
|
| def SetFileContents(self, filename, contents):
|
| """Writes |contents| to the file specified by |filename|."""
|
| @@ -466,13 +508,14 @@ class AndroidCommands(object):
|
| '(?P<filename>[^\s]+)$')
|
| return _GetFilesFromRecursiveLsOutput(
|
| path, self.RunShellCommand('ls -lR %s' % path), re_file,
|
| - self.RunShellCommand('date +%z')[0])
|
| + self._device_utc_offset)
|
|
|
| def SetupPerformanceTest(self):
|
| """Sets up performance tests."""
|
| # Disable CPU scaling to reduce noise in tests
|
| if not self._original_governor:
|
| - self._original_governor = self.RunShellCommand('cat ' + SCALING_GOVERNOR)
|
| + self._original_governor = self.GetFileContents(
|
| + SCALING_GOVERNOR, log_result=False)
|
| self.RunShellCommand('echo performance > ' + SCALING_GOVERNOR)
|
| self.DropRamCaches()
|
|
|
| @@ -535,7 +578,10 @@ class AndroidCommands(object):
|
| """
|
| if clear:
|
| self.RunShellCommand('logcat -c')
|
| - args = ['logcat', '-v', 'threadtime']
|
| + args = []
|
| + if self._adb._target_arg:
|
| + args += shlex.split(self._adb._target_arg)
|
| + args += ['logcat', '-v', 'threadtime']
|
| if filters:
|
| args.extend(filters)
|
| else:
|
| @@ -560,18 +606,26 @@ class AndroidCommands(object):
|
| self.StartMonitoringLogcat(clear=False)
|
| return self._logcat
|
|
|
| - def WaitForLogMatch(self, search_re):
|
| - """Blocks until a line containing |line_re| is logged or a timeout occurs.
|
| + def WaitForLogMatch(self, success_re, error_re, clear=False):
|
| + """Blocks until a matching line is logged or a timeout occurs.
|
|
|
| Args:
|
| - search_re: The compiled re to search each line for.
|
| + success_re: A compiled re to search each line for.
|
| + error_re: A compiled re which, if found, terminates the search for
|
| + |success_re|. If None is given, no error condition will be detected.
|
| + clear: If True the existing logcat output will be cleared, defaults to
|
| + false.
|
| +
|
| + Raises:
|
| + pexpect.TIMEOUT upon the timeout specified by StartMonitoringLogcat().
|
|
|
| Returns:
|
| - The re match object.
|
| + The re match object if |success_re| is matched first or None if |error_re|
|
| + is matched first.
|
| """
|
| if not self._logcat:
|
| - self.StartMonitoringLogcat(clear=False)
|
| - logging.info('<<< Waiting for logcat:' + str(search_re.pattern))
|
| + self.StartMonitoringLogcat(clear)
|
| + logging.info('<<< Waiting for logcat:' + str(success_re.pattern))
|
| t0 = time.time()
|
| try:
|
| while True:
|
| @@ -581,15 +635,19 @@ class AndroidCommands(object):
|
| if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat)
|
| self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining)
|
| line = self._logcat.match.group(1)
|
| - search_match = search_re.search(line)
|
| - if search_match:
|
| - return search_match
|
| + if error_re:
|
| + error_match = error_re.search(line)
|
| + if error_match:
|
| + return None
|
| + success_match = success_re.search(line)
|
| + if success_match:
|
| + return success_match
|
| logging.info('<<< Skipped Logcat Line:' + str(line))
|
| except pexpect.TIMEOUT:
|
| raise pexpect.TIMEOUT(
|
| 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv '
|
| 'to debug)' %
|
| - (self._logcat.timeout, search_re.pattern))
|
| + (self._logcat.timeout, success_re.pattern))
|
|
|
| def StartRecordingLogcat(self, clear=True, filters=['*:v']):
|
| """Starts recording logcat output to eventually be saved as a string.
|
| @@ -603,7 +661,8 @@ class AndroidCommands(object):
|
| """
|
| if clear:
|
| self._adb.SendCommand('logcat -c')
|
| - logcat_command = 'adb logcat -v threadtime %s' % ' '.join(filters)
|
| + logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg,
|
| + ' '.join(filters))
|
| self.logcat_process = subprocess.Popen(logcat_command, shell=True,
|
| stdout=subprocess.PIPE)
|
|
|
| @@ -672,13 +731,18 @@ class AndroidCommands(object):
|
|
|
| Returns:
|
| List of all the process ids (as strings) that match the given name.
|
| + If the name of a process exactly matches the given name, the pid of
|
| + that process will be inserted to the front of the pid list.
|
| """
|
| pids = []
|
| - for line in self.RunShellCommand('ps'):
|
| + for line in self.RunShellCommand('ps', log_result=False):
|
| data = line.split()
|
| try:
|
| if process_name in data[-1]: # name is in the last column
|
| - pids.append(data[1]) # PID is in the second column
|
| + if process_name == data[-1]:
|
| + pids.insert(0, data[1]) # PID is in the second column
|
| + else:
|
| + pids.append(data[1])
|
| except IndexError:
|
| pass
|
| return pids
|
| @@ -690,67 +754,88 @@ class AndroidCommands(object):
|
| Dict of {num_reads, num_writes, read_ms, write_ms} or None if there
|
| was an error.
|
| """
|
| - # Field definitions.
|
| - # http://www.kernel.org/doc/Documentation/iostats.txt
|
| - device = 2
|
| - num_reads_issued_idx = 3
|
| - num_reads_merged_idx = 4
|
| - num_sectors_read_idx = 5
|
| - ms_spent_reading_idx = 6
|
| - num_writes_completed_idx = 7
|
| - num_writes_merged_idx = 8
|
| - num_sectors_written_idx = 9
|
| - ms_spent_writing_idx = 10
|
| - num_ios_in_progress_idx = 11
|
| - ms_spent_doing_io_idx = 12
|
| - ms_spent_doing_io_weighted_idx = 13
|
| -
|
| - for line in self.RunShellCommand('cat /proc/diskstats'):
|
| - fields = line.split()
|
| - if fields[device] == 'mmcblk0':
|
| + for line in self.GetFileContents('/proc/diskstats', log_result=False):
|
| + stats = io_stats_parser.ParseIoStatsLine(line)
|
| + if stats.device == 'mmcblk0':
|
| return {
|
| - 'num_reads': int(fields[num_reads_issued_idx]),
|
| - 'num_writes': int(fields[num_writes_completed_idx]),
|
| - 'read_ms': int(fields[ms_spent_reading_idx]),
|
| - 'write_ms': int(fields[ms_spent_writing_idx]),
|
| + 'num_reads': stats.num_reads_issued,
|
| + 'num_writes': stats.num_writes_completed,
|
| + 'read_ms': stats.ms_spent_reading,
|
| + 'write_ms': stats.ms_spent_writing,
|
| }
|
| logging.warning('Could not find disk IO stats.')
|
| return None
|
|
|
| - def GetMemoryUsage(self, package):
|
| + def GetMemoryUsageForPid(self, pid):
|
| + """Returns the memory usage for given pid.
|
| +
|
| + Args:
|
| + pid: The pid number of the specific process running on device.
|
| +
|
| + Returns:
|
| + A tuple containg:
|
| + [0]: Dict of {metric:usage_kb}, for the process which has specified pid.
|
| + The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
|
| + Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
|
| + KernelPageSize, MMUPageSize, Nvidia (tablet only).
|
| + [1]: Detailed /proc/[PID]/smaps information.
|
| + """
|
| + usage_dict = collections.defaultdict(int)
|
| + smaps = collections.defaultdict(dict)
|
| + current_smap = ''
|
| + for line in self.GetFileContents('/proc/%s/smaps' % pid, log_result=False):
|
| + items = line.split()
|
| + # See man 5 proc for more details. The format is:
|
| + # address perms offset dev inode pathname
|
| + if len(items) > 5:
|
| + current_smap = ' '.join(items[5:])
|
| + elif len(items) > 3:
|
| + current_smap = ' '.join(items[3:])
|
| + match = re.match(MEMORY_INFO_RE, line)
|
| + if match:
|
| + key = match.group('key')
|
| + usage_kb = int(match.group('usage_kb'))
|
| + usage_dict[key] += usage_kb
|
| + if key not in smaps[current_smap]:
|
| + smaps[current_smap][key] = 0
|
| + smaps[current_smap][key] += usage_kb
|
| + if not usage_dict or not any(usage_dict.values()):
|
| + # Presumably the process died between ps and calling this method.
|
| + logging.warning('Could not find memory usage for pid ' + str(pid))
|
| +
|
| + for line in self.GetFileContents('/d/nvmap/generic-0/clients',
|
| + log_result=False):
|
| + match = re.match(NVIDIA_MEMORY_INFO_RE, line)
|
| + if match and match.group('pid') == pid:
|
| + usage_bytes = int(match.group('usage_bytes'))
|
| + usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0)) # kB
|
| + break
|
| +
|
| + return (usage_dict, smaps)
|
| +
|
| + def GetMemoryUsageForPackage(self, package):
|
| """Returns the memory usage for all processes whose name contains |pacakge|.
|
|
|
| Args:
|
| name: A string holding process name to lookup pid list for.
|
|
|
| Returns:
|
| - Dict of {metric:usage_kb}, summed over all pids associated with |name|.
|
| - The metric keys retruned are: Size, Rss, Pss, Shared_Clean, Shared_Dirty,
|
| - Private_Clean, Private_Dirty, Referenced, Swap, KernelPageSize,
|
| - MMUPageSize.
|
| + A tuple containg:
|
| + [0]: Dict of {metric:usage_kb}, summed over all pids associated with
|
| + |name|.
|
| + The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
|
| + Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
|
| + KernelPageSize, MMUPageSize, Nvidia (tablet only).
|
| + [1]: a list with detailed /proc/[PID]/smaps information.
|
| """
|
| usage_dict = collections.defaultdict(int)
|
| pid_list = self.ExtractPid(package)
|
| - # We used to use the showmap command, but it is currently broken on
|
| - # stingray so it's easier to just parse /proc/<pid>/smaps directly.
|
| - memory_stat_re = re.compile('^(?P<key>\w+):\s+(?P<value>\d+) kB$')
|
| + smaps = collections.defaultdict(dict)
|
| +
|
| for pid in pid_list:
|
| - for line in self.RunShellCommand('cat /proc/%s/smaps' % pid,
|
| - log_result=False):
|
| - match = re.match(memory_stat_re, line)
|
| - if match: usage_dict[match.group('key')] += int(match.group('value'))
|
| - if not usage_dict or not any(usage_dict.values()):
|
| - # Presumably the process died between ps and showmap.
|
| - logging.warning('Could not find memory usage for pid ' + str(pid))
|
| - return usage_dict
|
| -
|
| - def UnlockDevice(self):
|
| - """Unlocks the screen of the device."""
|
| - # Make sure a menu button event will actually unlock the screen.
|
| - if IsRunningAsBuildbot():
|
| - assert self.RunShellCommand('getprop ro.test_harness')[0].strip() == '1'
|
| - # The following keyevent unlocks the screen if locked.
|
| - self.SendKeyEvent(KEYCODE_MENU)
|
| - # If the screen wasn't locked the previous command will bring up the menu,
|
| - # which this will dismiss. Otherwise this shouldn't change anything.
|
| - self.SendKeyEvent(KEYCODE_BACK)
|
| + usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid)
|
| + smaps[pid] = smaps_per_pid
|
| + for (key, value) in usage_dict_per_pid.items():
|
| + usage_dict[key] += value
|
| +
|
| + return usage_dict, smaps
|
|
|