| #!/usr/bin/python |
| # Copyright 2015 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. |
| |
| # 'top'-like memory/network polling for Android apps. |
| |
| import argparse |
| import curses |
| import logging |
| import os |
| import re |
| import sys |
| import time |
| |
| from operator import sub |
| |
| _SRC_PATH = os.path.abspath(os.path.join( |
| os.path.dirname(__file__), '..', '..')) |
| |
| sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'catapult', 'devil')) |
| from devil.android import device_errors |
| from devil.android import device_utils |
| |
| sys.path.append(os.path.join(_SRC_PATH, 'build', 'android')) |
| import devil_chromium |
| |
| class Utils(object): |
| """A helper class to hold various utility methods.""" |
| |
| @staticmethod |
| def FindLines(haystack, needle): |
| """A helper method to find lines in |haystack| that contain the string |
| |needle|.""" |
| return [ hay for hay in haystack if needle in hay ] |
| |
| class Validator(object): |
| """A helper class with validation methods for argparse.""" |
| |
| @staticmethod |
| def ValidatePath(path): |
| """An argparse validation method to make sure a file path is writable.""" |
| if os.path.exists(path): |
| return path |
| elif os.access(os.path.dirname(path), os.W_OK): |
| return path |
| raise argparse.ArgumentTypeError("%s is an invalid file path" % path) |
| |
| @staticmethod |
| def ValidatePdfPath(path): |
| """An argparse validation method to make sure a pdf file path is writable. |
| Validates a file path to make sure it is writable and also appends '.pdf' if |
| necessary.""" |
| if os.path.splitext(path)[-1].lower() != 'pdf': |
| path = path + '.pdf' |
| return Validator.ValidatePath(path) |
| |
| @staticmethod |
| def ValidateNonNegativeNumber(val): |
| """An argparse validation method to make sure a number is not negative.""" |
| ival = int(val) |
| if ival < 0: |
| raise argparse.ArgumentTypeError("%s is a negative integer" % val) |
| return ival |
| |
| class Timer(object): |
| """A helper class to track timestamps based on when this program was |
| started""" |
| starting_time = time.time() |
| |
| @staticmethod |
| def GetTimestamp(): |
| """A helper method to return the time (in seconds) since this program was |
| started.""" |
| return time.time() - Timer.starting_time |
| |
| class DeviceHelper(object): |
| """A helper class with various generic device interaction methods.""" |
| |
| @staticmethod |
| def __GetUserIdForProcessName(adb, process_name): |
| """Returns the userId of the application associated by |pid| or None if |
| not found.""" |
| try: |
| process_name = process_name.split(':')[0] |
| cmd = ['dumpsys', 'package', process_name] |
| user_id_lines = adb.RunShellCommand( |
| cmd, large_output=True, check_return=True) |
| user_id_lines = Utils.FindLines(user_id_lines, 'userId=') |
| |
| if not user_id_lines: |
| return None |
| |
| columns = re.split('\s+|=', user_id_lines[0].strip()) |
| |
| if len(columns) >= 2: |
| return columns[1] |
| except device_errors.AdbShellCommandFailedError: |
| pass |
| return None |
| |
| @staticmethod |
| def GetDeviceModel(adb): |
| """Returns the model of the device with the |adb| connection.""" |
| return adb.GetProp('ro.product.model').strip() |
| |
| @staticmethod |
| def GetDeviceToTrack(preset=None): |
| """Returns a device serial to connect to. If |preset| is specified it will |
| return |preset| if it is connected and |None| otherwise. If |preset| is not |
| specified it will return the first connected device.""" |
| devices = [d.adb.GetDeviceSerial() |
| for d in device_utils.DeviceUtils.HealthyDevices()] |
| if not devices: |
| return None |
| |
| if preset: |
| return preset if preset in devices else None |
| |
| return devices[0] |
| |
| @staticmethod |
| def GetPidsToTrack(adb, default_pid=None, process_filter=None): |
| """Returns a list of tuples of (userid, pids, process name) based on the |
| input arguments. If |default_pid| is specified it will return that pid if |
| it exists. If |process_filter| is specified it will return the pids of |
| processes with that string in the name. If both are specified it will |
| intersect the two. The returned result is sorted based on userid.""" |
| pids = [] |
| try: |
| for process in adb.ListProcesses(process_filter): |
| if default_pid and process.pid != default_pid: |
| continue |
| userid = DeviceHelper.__GetUserIdForProcessName(adb, process.name) |
| pids.append((userid, process.pid, process.name)) |
| except device_errors.AdbShellCommandFailedError as exc: |
| logging.warning('Error getting PIDs to track: %s', exc) |
| return sorted(pids, key=lambda tup: tup[0]) |
| |
| class NetworkHelper(object): |
| """A helper class to query basic network usage of an application.""" |
| @staticmethod |
| def QueryNetwork(adb, userid): |
| """Queries the device for network information about the application with a |
| user id of |userid|. It will return a list of values: |
| [ Download Background, Upload Background, Download Foreground, Upload |
| Foreground ]. If the application is not found it will return |
| [ 0, 0, 0, 0 ].""" |
| results = [0, 0, 0, 0] |
| |
| if not userid: |
| return results |
| |
| try: |
| # Parsing indices for scanning a row from /proc/net/xt_qtaguid/stats. |
| # The application id |
| userid_idx = 3 |
| |
| # Whether or not the transmission happened with the application in the |
| # background (0) or foreground (1). |
| bg_or_fg_idx = 4 |
| |
| # The number of bytes received. |
| rx_idx = 5 |
| |
| # The number of bytes sent. |
| tx_idx = 7 |
| |
| net_lines = adb.ReadFile('/proc/net/xt_qtaguid/stats').splitlines() |
| net_lines = Utils.FindLines(net_lines, userid) |
| for line in net_lines: |
| data = re.split('\s+', line.strip()) |
| if data[userid_idx] != userid: |
| continue |
| |
| dst_idx_offset = None |
| if data[bg_or_fg_idx] == '0': |
| dst_idx_offset = 0 |
| elif data[bg_or_fg_idx] == '1': |
| dst_idx_offset = 2 |
| |
| if dst_idx_offset is None: |
| continue |
| |
| results[dst_idx_offset] = round(float(data[rx_idx]) / 1000.0, 2) |
| results[dst_idx_offset + 1] = round(float(data[tx_idx]) / 1000.0, 2) |
| except device_errors.AdbShellCommandFailedError: |
| pass |
| return results |
| |
| class MemoryHelper(object): |
| """A helper class to query basic memory usage of a process.""" |
| |
| @staticmethod |
| def QueryMemory(adb, pid): |
| """Queries the device for memory information about the process with a pid of |
| |pid|. It will query Native, Dalvik, and Pss memory of the process. It |
| returns a list of values: [ Native, Pss, Dalvik ]. If the process is not |
| found it will return [ 0, 0, 0 ].""" |
| results = [0, 0, 0] |
| |
| mem_lines = adb.RunShellCommand( |
| ['dumpsys', 'meminfo', pid], check_return=True) |
| for line in mem_lines: |
| match = re.split('\s+', line.strip()) |
| |
| # Skip data after the 'App Summary' line. This is to fix builds where |
| # they have more entries that might match the other conditions. |
| if len(match) >= 2 and match[0] == 'App' and match[1] == 'Summary': |
| break |
| |
| result_idx = None |
| query_idx = None |
| if match[0] == 'Native' and match[1] == 'Heap': |
| result_idx = 0 |
| query_idx = -2 |
| elif match[0] == 'Dalvik' and match[1] == 'Heap': |
| result_idx = 2 |
| query_idx = -2 |
| elif match[0] == 'TOTAL': |
| result_idx = 1 |
| query_idx = 1 |
| |
| # If we already have a result, skip it and don't overwrite the data. |
| if result_idx is not None and results[result_idx] != 0: |
| continue |
| |
| if result_idx is not None and query_idx is not None: |
| results[result_idx] = round(float(match[query_idx]) / 1000.0, 2) |
| return results |
| |
| class GraphicsHelper(object): |
| """A helper class to query basic graphics memory usage of a process.""" |
| |
| # TODO(dtrainor): Find a generic way to query/fall back for other devices. |
| # Is showmap consistently reliable? |
| __NV_MAP_MODELS = ['Xoom'] |
| __NV_MAP_FILE_LOCATIONS = ['/d/nvmap/generic-0/clients', |
| '/d/nvmap/iovmm/clients'] |
| |
| __SHOWMAP_MODELS = ['Nexus S', |
| 'Nexus S 4G', |
| 'Galaxy Nexus', |
| 'Nexus 4', |
| 'Nexus 5', |
| 'Nexus 7'] |
| __SHOWMAP_KEY_MATCHES = ['/dev/pvrsrvkm', |
| '/dev/kgsl-3d0'] |
| |
| @staticmethod |
| def __QueryShowmap(adb, pid): |
| """Attempts to query graphics memory via the 'showmap' command. It will |
| look for |self.__SHOWMAP_KEY_MATCHES| entries to try to find one that |
| represents the graphics memory usage. Will return this as a single entry |
| array of [ Graphics ]. If not found, will return [ 0 ].""" |
| try: |
| mem_lines = adb.RunShellCommand(['showmap', '-t', pid], check_return=True) |
| for line in mem_lines: |
| match = re.split('[ ]+', line.strip()) |
| if match[-1] in GraphicsHelper.__SHOWMAP_KEY_MATCHES: |
| return [ round(float(match[2]) / 1000.0, 2) ] |
| except device_errors.AdbShellCommandFailedError: |
| pass |
| return [ 0 ] |
| |
| @staticmethod |
| def __NvMapPath(adb): |
| """Attempts to find a valid NV Map file on the device. It will look for a |
| file in |self.__NV_MAP_FILE_LOCATIONS| and see if one exists. If so, it |
| will return it.""" |
| for nv_file in GraphicsHelper.__NV_MAP_FILE_LOCATIONS: |
| if adb.PathExists(nv_file): |
| return nv_file |
| return None |
| |
| @staticmethod |
| def __QueryNvMap(adb, pid): |
| """Attempts to query graphics memory via the NV file map method. It will |
| find a possible NV Map file from |self.__NvMapPath| and try to parse the |
| graphics memory from it. Will return this as a single entry array of |
| [ Graphics ]. If not found, will return [ 0 ].""" |
| nv_file = GraphicsHelper.__NvMapPath(adb) |
| if nv_file: |
| mem_lines = adb.ReadFile(nv_file).splitlines() |
| for line in mem_lines: |
| match = re.split(' +', line.strip()) |
| if match[2] == pid: |
| return [ round(float(match[3]) / 1000000.0, 2) ] |
| return [ 0 ] |
| |
| @staticmethod |
| def QueryVideoMemory(adb, pid): |
| """Queries the device for graphics memory information about the process with |
| a pid of |pid|. Not all devices are currently supported. If possible, this |
| will return a single entry array of [ Graphics ]. Otherwise it will return |
| [ 0 ]. |
| |
| Please see |self.__NV_MAP_MODELS| and |self.__SHOWMAP_MODELS| |
| to see if the device is supported. For new devices, see if they can be |
| supported by existing methods and add their entry appropriately. Also, |
| please add any new way of querying graphics memory as they become |
| available.""" |
| model = DeviceHelper.GetDeviceModel(adb) |
| if model in GraphicsHelper.__NV_MAP_MODELS: |
| return GraphicsHelper.__QueryNvMap(adb, pid) |
| elif model in GraphicsHelper.__SHOWMAP_MODELS: |
| return GraphicsHelper.__QueryShowmap(adb, pid) |
| return [ 0 ] |
| |
| class DeviceSnapshot(object): |
| """A class holding a snapshot of memory and network usage for various pids |
| that are being tracked. If |show_mem| is True, this will track memory usage. |
| If |show_net| is True, this will track network usage. |
| |
| Attributes: |
| pids: A list of tuples (userid, pid, process name) that should be |
| tracked. |
| memory: A map of entries of pid => memory consumption array. Right now |
| the indices are [ Native, Pss, Dalvik, Graphics ]. |
| network: A map of entries of userid => network consumption array. Right |
| now the indices are [ Download Background, Upload Background, |
| Download Foreground, Upload Foreground ]. |
| timestamp: The amount of time (in seconds) between when this program started |
| and this snapshot was taken. |
| """ |
| |
| def __init__(self, adb, pids, show_mem, show_net): |
| """Creates an instances of a DeviceSnapshot with an |adb| device connection |
| and a list of (pid, process name) tuples.""" |
| super(DeviceSnapshot, self).__init__() |
| |
| self.pids = pids |
| self.memory = {} |
| self.network = {} |
| self.timestamp = Timer.GetTimestamp() |
| |
| for (userid, pid, name) in pids: |
| if show_mem: |
| self.memory[pid] = self.__QueryMemoryForPid(adb, pid) |
| |
| if show_net and userid not in self.network: |
| self.network[userid] = NetworkHelper.QueryNetwork(adb, userid) |
| |
| @staticmethod |
| def __QueryMemoryForPid(adb, pid): |
| """Queries the |adb| device for memory information about |pid|. This will |
| return a list of memory values that map to [ Native, Pss, Dalvik, |
| Graphics ].""" |
| results = MemoryHelper.QueryMemory(adb, pid) |
| results.extend(GraphicsHelper.QueryVideoMemory(adb, pid)) |
| return results |
| |
| def __GetProcessNames(self): |
| """Returns a list of all of the process names tracked by this snapshot.""" |
| return [tuple[2] for tuple in self.pids] |
| |
| def HasResults(self): |
| """Whether or not this snapshot was tracking any processes.""" |
| return self.pids |
| |
| def GetPidInfo(self): |
| """Returns a list of (userid, pid, process name) tuples that are being |
| tracked in this snapshot.""" |
| return self.pids |
| |
| def GetNameForPid(self, search_pid): |
| """Returns the process name of a tracked |search_pid|. This only works if |
| |search_pid| is tracked by this snapshot.""" |
| for (userid, pid, name) in self.pids: |
| if pid == search_pid: |
| return name |
| return None |
| |
| def GetUserIdForPid(self, search_pid): |
| """Returns the application userId for an associated |pid|. This only works |
| if |search_pid| is tracked by this snapshot and the application userId is |
| queryable.""" |
| for (userid, pid, name) in self.pids: |
| if pid == search_pid: |
| return userid |
| return None |
| |
| def IsFirstPidForUserId(self, search_pid): |
| """Returns whether or not |search_pid| is the first pid in the |pids| with |
| the associated application userId. This is used to determine if network |
| statistics should be shown for this pid or if they have already been shown |
| for a pid associated with this application.""" |
| prev_userid = None |
| for idx, (userid, pid, name) in enumerate(self.pids): |
| if pid == search_pid: |
| return prev_userid != userid |
| prev_userid = userid |
| return False |
| |
| def GetMemoryResults(self, pid): |
| """Returns a list of entries about the memory usage of the process specified |
| by |pid|. This will be of the format [ Native, Pss, Dalvik, Graphics ].""" |
| if pid in self.memory: |
| return self.memory[pid] |
| return None |
| |
| def GetNetworkResults(self, userid): |
| """Returns a list of entries about the network usage of the application |
| specified by |userid|. This will be of the format [ Download Background, |
| Upload Background, Download Foreground, Upload Foreground ].""" |
| if userid in self.network: |
| return self.network[userid] |
| return None |
| |
| def GetLongestNameLength(self): |
| """Returns the length of the longest process name tracked by this |
| snapshot.""" |
| return len(max(self.__GetProcessNames(), key=len)) |
| |
| def GetTimestamp(self): |
| """Returns the time since program start that this snapshot was taken.""" |
| return self.timestamp |
| |
| class OutputBeautifier(object): |
| """A helper class to beautify the memory output to various destinations. |
| |
| Attributes: |
| can_color: Whether or not the output should include ASCII color codes to |
| make it look nicer. Default is |True|. This is disabled when |
| writing to a file or a graph. |
| overwrite: Whether or not the output should overwrite the previous output. |
| Default is |True|. This is disabled when writing to a file or a |
| graph. |
| """ |
| |
| __MEMORY_COLUMN_TITLES = ['Native', |
| 'Pss', |
| 'Dalvik', |
| 'Graphics'] |
| |
| __NETWORK_COLUMN_TITLES = ['Bg Rx', |
| 'Bg Tx', |
| 'Fg Rx', |
| 'Fg Tx'] |
| |
| __TERMINAL_COLORS = {'ENDC': 0, |
| 'BOLD': 1, |
| 'GREY30': 90, |
| 'RED': 91, |
| 'DARK_YELLOW': 33, |
| 'GREEN': 92} |
| |
| def __init__(self, can_color=True, overwrite=True): |
| """Creates an instance of an OutputBeautifier.""" |
| super(OutputBeautifier, self).__init__() |
| self.can_color = can_color |
| self.overwrite = overwrite |
| |
| self.lines_printed = 0 |
| self.printed_header = False |
| |
| @staticmethod |
| def __FindPidsForSnapshotList(snapshots): |
| """Find the set of unique pids across all every snapshot in |snapshots|.""" |
| pids = set() |
| for snapshot in snapshots: |
| for (userid, pid, name) in snapshot.GetPidInfo(): |
| pids.add((userid, pid, name)) |
| return pids |
| |
| @staticmethod |
| def __TermCode(num): |
| """Escapes a terminal code. See |self.__TERMINAL_COLORS| for a list of some |
| terminal codes that are used by this program.""" |
| return '\033[%sm' % num |
| |
| @staticmethod |
| def __PadString(string, length, left_align): |
| """Pads |string| to at least |length| with spaces. Depending on |
| |left_align| the padding will appear at either the left or the right of the |
| original string.""" |
| return (('%' if left_align else '%-') + str(length) + 's') % string |
| |
| @staticmethod |
| def __GetDiffColor(delta): |
| """Returns a color based on |delta|. Used to color the deltas between |
| different snapshots.""" |
| if not delta or delta == 0.0: |
| return 'GREY30' |
| elif delta < 0: |
| return 'GREEN' |
| elif delta > 0: |
| return 'RED' |
| |
| @staticmethod |
| def __CleanRound(val, precision): |
| """Round |val| to |precision|. If |precision| is 0, completely remove the |
| decimal point.""" |
| return int(val) if precision == 0 else round(float(val), precision) |
| |
| def __ColorString(self, string, color): |
| """Colors |string| based on |color|. |color| must be in |
| |self.__TERMINAL_COLORS|. Returns the colored string or the original |
| string if |self.can_color| is |False| or the |color| is invalid.""" |
| if not self.can_color or not color or not self.__TERMINAL_COLORS[color]: |
| return string |
| |
| return '%s%s%s' % ( |
| self.__TermCode(self.__TERMINAL_COLORS[color]), |
| string, |
| self.__TermCode(self.__TERMINAL_COLORS['ENDC'])) |
| |
| def __PadAndColor(self, string, length, left_align, color): |
| """A helper method to both pad and color the string. See |
| |self.__ColorString| and |self.__PadString|.""" |
| return self.__ColorString( |
| self.__PadString(string, length, left_align), color) |
| |
| def __OutputLine(self, line): |
| """Writes a line to the screen. This also tracks how many times this method |
| was called so that the screen can be cleared properly if |self.overwrite| is |
| |True|.""" |
| sys.stdout.write(line + '\n') |
| if self.overwrite: |
| self.lines_printed += 1 |
| |
| def __ClearScreen(self): |
| """Clears the screen based on the number of times |self.__OutputLine| was |
| called.""" |
| if self.lines_printed == 0 or not self.overwrite: |
| return |
| |
| key_term_up = curses.tparm(curses.tigetstr('cuu1')) |
| key_term_clear_eol = curses.tparm(curses.tigetstr('el')) |
| key_term_go_to_bol = curses.tparm(curses.tigetstr('cr')) |
| |
| sys.stdout.write(key_term_go_to_bol) |
| sys.stdout.write(key_term_clear_eol) |
| |
| for i in range(self.lines_printed): |
| sys.stdout.write(key_term_up) |
| sys.stdout.write(key_term_clear_eol) |
| self.lines_printed = 0 |
| |
| def __PrintPidLabelHeader(self, snapshot): |
| """Returns a header string with columns Pid and Name.""" |
| if not snapshot or not snapshot.HasResults(): |
| return |
| |
| name_length = max(8, snapshot.GetLongestNameLength()) |
| |
| header = self.__PadString('Pid', 8, True) + ' ' |
| header += self.__PadString('Name', name_length, False) |
| header = self.__ColorString(header, 'BOLD') |
| return header |
| |
| def __PrintTimestampHeader(self): |
| """Returns a header string with a Timestamp column.""" |
| header = self.__PadString('Timestamp', 8, False) |
| header = self.__ColorString(header, 'BOLD') |
| return header |
| |
| def __PrintMemoryStatsHeader(self): |
| """Returns a header string for memory usage statistics.""" |
| headers = '' |
| for header in self.__MEMORY_COLUMN_TITLES: |
| headers += self.__PadString(header, 8, True) + ' ' |
| headers += self.__PadString('(mB)', 8, False) |
| return self.__ColorString(headers, 'BOLD') |
| |
| def __PrintNetworkStatsHeader(self): |
| """Returns a header string for network usage statistics.""" |
| headers = '' |
| for header in self.__NETWORK_COLUMN_TITLES: |
| headers += self.__PadString(header, 8, True) + ' ' |
| headers += self.__PadString('(kB)', 8, False) |
| return self.__ColorString(headers, 'BOLD') |
| |
| def __PrintTrailingHeader(self, snapshot): |
| """Returns a header string for the header trailer (includes timestamp).""" |
| if not snapshot or not snapshot.HasResults(): |
| return |
| |
| header = '(' + str(round(snapshot.GetTimestamp(), 2)) + 's)' |
| return self.__ColorString(header, 'BOLD') |
| |
| def __PrintArrayWithDeltas(self, results, old_results, precision=2): |
| """Helper method to return a string of statistics with their deltas. This |
| takes two arrays and prints out "current (current - old)" for all entries in |
| the arrays.""" |
| if not results: |
| return |
| deltas = [0] * len(results) |
| if old_results: |
| assert len(old_results) == len(results) |
| deltas = map(sub, results, old_results) |
| output = '' |
| for idx, val in enumerate(results): |
| round_val = self.__CleanRound(val, precision) |
| round_delta = self.__CleanRound(deltas[idx], precision) |
| output += self.__PadString(str(round_val), 8, True) + ' ' |
| output += self.__PadAndColor('(' + str(round_delta) + ')', 8, False, |
| self.__GetDiffColor(deltas[idx])) |
| |
| return output |
| |
| def __PrintPidLabelStats(self, pid, snapshot): |
| """Returns a string that includes the columns pid and process name for |
| the specified |pid|. This lines up with the associated header.""" |
| if not snapshot or not snapshot.HasResults(): |
| return |
| |
| name_length = max(8, snapshot.GetLongestNameLength()) |
| name = snapshot.GetNameForPid(pid) |
| |
| output = self.__PadAndColor(pid, 8, True, 'DARK_YELLOW') + ' ' |
| output += self.__PadAndColor(name, name_length, False, None) |
| return output |
| |
| def __PrintTimestampStats(self, snapshot): |
| """Returns a string that includes the timestamp of the |snapshot|. This |
| lines up with the associated header.""" |
| if not snapshot or not snapshot.HasResults(): |
| return |
| |
| timestamp_length = max(8, len("Timestamp")) |
| timestamp = round(snapshot.GetTimestamp(), 2) |
| |
| output = self.__PadString(str(timestamp), timestamp_length, True) |
| return output |
| |
| def __PrintMemoryStats(self, pid, snapshot, prev_snapshot): |
| """Returns a string that includes memory statistics of the |snapshot|. This |
| lines up with the associated header.""" |
| if not snapshot or not snapshot.HasResults(): |
| return |
| |
| results = snapshot.GetMemoryResults(pid) |
| if not results: |
| return |
| |
| old_results = prev_snapshot.GetMemoryResults(pid) if prev_snapshot else None |
| return self.__PrintArrayWithDeltas(results, old_results, 2) |
| |
| def __PrintNetworkStats(self, userid, snapshot, prev_snapshot): |
| """Returns a string that includes network statistics of the |snapshot|. This |
| lines up with the associated header.""" |
| if not snapshot or not snapshot.HasResults(): |
| return |
| |
| results = snapshot.GetNetworkResults(userid) |
| if not results: |
| return |
| |
| old_results = None |
| if prev_snapshot: |
| old_results = prev_snapshot.GetNetworkResults(userid) |
| return self.__PrintArrayWithDeltas(results, old_results, 0) |
| |
| def __PrintNulledNetworkStats(self): |
| """Returns a string that includes empty network statistics. This lines up |
| with the associated header. This is used when showing statistics for pids |
| that share the same application userId. Network statistics should only be |
| shown once for each application userId.""" |
| stats = '' |
| for title in self.__NETWORK_COLUMN_TITLES: |
| stats += self.__PadString('-', 8, True) + ' ' |
| stats += self.__PadString('', 8, True) |
| return stats |
| |
| def __PrintHeaderHelper(self, |
| snapshot, |
| show_labels, |
| show_timestamp, |
| show_mem, |
| show_net, |
| show_trailer): |
| """Helper method to concat various header entries together into one header. |
| This will line up with a entry built by __PrintStatsHelper if the same |
| values are passed to it.""" |
| titles = [] |
| if show_labels: |
| titles.append(self.__PrintPidLabelHeader(snapshot)) |
| |
| if show_timestamp: |
| titles.append(self.__PrintTimestampHeader()) |
| |
| if show_mem: |
| titles.append(self.__PrintMemoryStatsHeader()) |
| |
| if show_net: |
| titles.append(self.__PrintNetworkStatsHeader()) |
| |
| if show_trailer: |
| titles.append(self.__PrintTrailingHeader(snapshot)) |
| |
| return ' '.join(titles) |
| |
| def __PrintStatsHelper(self, |
| pid, |
| snapshot, |
| prev_snapshot, |
| show_labels, |
| show_timestamp, |
| show_mem, |
| show_net): |
| """Helper method to concat various stats entries together into one line. |
| This will line up with a header built by __PrintHeaderHelper if the same |
| values are passed to it.""" |
| stats = [] |
| if show_labels: |
| stats.append(self.__PrintPidLabelStats(pid, snapshot)) |
| |
| if show_timestamp: |
| stats.append(self.__PrintTimestampStats(snapshot)) |
| |
| if show_mem: |
| stats.append(self.__PrintMemoryStats(pid, snapshot, prev_snapshot)) |
| |
| if show_net: |
| userid = snapshot.GetUserIdForPid(pid) |
| show_userid = snapshot.IsFirstPidForUserId(pid) |
| if userid and show_userid: |
| stats.append(self.__PrintNetworkStats(userid, snapshot, prev_snapshot)) |
| else: |
| stats.append(self.__PrintNulledNetworkStats()) |
| |
| return ' '.join(stats) |
| |
| def PrettyPrint(self, snapshot, prev_snapshot, show_mem=True, show_net=True): |
| """Prints |snapshot| to the console. This will show memory and/or network |
| deltas between |snapshot| and |prev_snapshot|. This will also either color |
| or overwrite the previous entries based on |self.can_color| and |
| |self.overwrite|. If |show_mem| is True, this will attempt to show memory |
| statistics. If |show_net| is True, this will attempt to show network |
| statistics.""" |
| self.__ClearScreen() |
| |
| if not snapshot or not snapshot.HasResults(): |
| self.__OutputLine("No results...") |
| return |
| |
| # Output Format |
| show_label = True |
| show_timestamp = False |
| show_trailer = True |
| |
| self.__OutputLine(self.__PrintHeaderHelper(snapshot, |
| show_label, |
| show_timestamp, |
| show_mem, |
| show_net, |
| show_trailer)) |
| |
| for (userid, pid, name) in snapshot.GetPidInfo(): |
| self.__OutputLine(self.__PrintStatsHelper(pid, |
| snapshot, |
| prev_snapshot, |
| show_label, |
| show_timestamp, |
| show_mem, |
| show_net)) |
| |
| def PrettyFile(self, |
| file_path, |
| snapshots, |
| diff_against_start, |
| show_mem=True, |
| show_net=True): |
| """Writes |snapshots| (a list of DeviceSnapshots) to |file_path|. |
| |diff_against_start| determines whether or not the snapshot deltas are |
| between the first entry and all entries or each previous entry. This output |
| will not follow |self.can_color| or |self.overwrite|. If |show_mem| is |
| True, this will attempt to show memory statistics. If |show_net| is True, |
| this will attempt to show network statistics.""" |
| if not file_path or not snapshots: |
| return |
| |
| # Output Format |
| show_label = False |
| show_timestamp = True |
| show_trailer = False |
| |
| pids = self.__FindPidsForSnapshotList(snapshots) |
| |
| # Disable special output formatting for file writing. |
| can_color = self.can_color |
| self.can_color = False |
| |
| with open(file_path, 'w') as out: |
| for (userid, pid, name) in pids: |
| out.write(name + ' (' + str(pid) + '):\n') |
| out.write(self.__PrintHeaderHelper(None, |
| show_label, |
| show_timestamp, |
| show_mem, |
| show_net, |
| show_trailer)) |
| out.write('\n') |
| |
| prev_snapshot = None |
| for snapshot in snapshots: |
| has_mem = show_mem and snapshot.GetMemoryResults(pid) is not None |
| has_net = show_net and snapshot.GetNetworkResults(userid) is not None |
| if not has_mem and not has_net: |
| continue |
| out.write(self.__PrintStatsHelper(pid, |
| snapshot, |
| prev_snapshot, |
| show_label, |
| show_timestamp, |
| show_mem, |
| show_net)) |
| out.write('\n') |
| if not prev_snapshot or not diff_against_start: |
| prev_snapshot = snapshot |
| out.write('\n\n') |
| |
| # Restore special output formatting. |
| self.can_color = can_color |
| |
| def PrettyGraph(self, file_path, snapshots): |
| """Creates a pdf graph of |snapshots| (a list of DeviceSnapshots) at |
| |file_path|. This currently only shows memory stats and no network |
| stats.""" |
| # Import these here so the rest of the functionality doesn't rely on |
| # matplotlib |
| from matplotlib import pyplot |
| from matplotlib.backends.backend_pdf import PdfPages |
| |
| if not file_path or not snapshots: |
| return |
| |
| pids = self.__FindPidsForSnapshotList(snapshots) |
| |
| pp = PdfPages(file_path) |
| for (userid, pid, name) in pids: |
| figure = pyplot.figure() |
| ax = figure.add_subplot(1, 1, 1) |
| ax.set_xlabel('Time (s)') |
| ax.set_ylabel('MB') |
| ax.set_title(name + ' (' + pid + ')') |
| |
| mem_list = [[] for x in range(len(self.__MEMORY_COLUMN_TITLES))] |
| timestamps = [] |
| |
| for snapshot in snapshots: |
| results = snapshot.GetMemoryResults(pid) |
| if not results: |
| continue |
| |
| timestamps.append(round(snapshot.GetTimestamp(), 2)) |
| |
| assert len(results) == len(self.__MEMORY_COLUMN_TITLES) |
| for idx, result in enumerate(results): |
| mem_list[idx].append(result) |
| |
| colors = [] |
| for data in mem_list: |
| colors.append(ax.plot(timestamps, data)[0]) |
| for i in xrange(len(timestamps)): |
| ax.annotate(data[i], xy=(timestamps[i], data[i])) |
| figure.legend(colors, self.__MEMORY_COLUMN_TITLES) |
| pp.savefig() |
| pp.close() |
| |
| def main(argv): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--process', |
| dest='procname', |
| help="A (sub)string to match against process names.") |
| parser.add_argument('-p', |
| '--pid', |
| dest='pid', |
| type=Validator.ValidateNonNegativeNumber, |
| help='Which pid to scan for.') |
| parser.add_argument('-d', |
| '--device', |
| dest='device', |
| help='Device serial to scan.') |
| parser.add_argument('-t', |
| '--timelimit', |
| dest='timelimit', |
| type=Validator.ValidateNonNegativeNumber, |
| help='How long to track memory in seconds.') |
| parser.add_argument('-f', |
| '--frequency', |
| dest='frequency', |
| default=0, |
| type=Validator.ValidateNonNegativeNumber, |
| help='How often to poll in seconds.') |
| parser.add_argument('-s', |
| '--diff-against-start', |
| dest='diff_against_start', |
| action='store_true', |
| help='Whether or not to always compare against the' |
| ' original memory values for deltas.') |
| parser.add_argument('-b', |
| '--boring-output', |
| dest='dull_output', |
| action='store_true', |
| help='Whether or not to dull down the output.') |
| parser.add_argument('-k', |
| '--keep-results', |
| dest='no_overwrite', |
| action='store_true', |
| help='Keeps printing the results in a list instead of' |
| ' overwriting the previous values.') |
| parser.add_argument('-g', |
| '--graph-file', |
| dest='graph_file', |
| type=Validator.ValidatePdfPath, |
| help='PDF file to save graph of memory stats to.') |
| parser.add_argument('-o', |
| '--text-file', |
| dest='text_file', |
| type=Validator.ValidatePath, |
| help='File to save memory tracking stats to.') |
| parser.add_argument('-m', |
| '--memory', |
| dest='show_mem', |
| action='store_true', |
| help='Whether or not to show memory stats. True by' |
| ' default unless --n is specified.') |
| parser.add_argument('-n', |
| '--net', |
| dest='show_net', |
| action='store_true', |
| help='Whether or not to show network stats. False by' |
| ' default.') |
| |
| args = parser.parse_args() |
| |
| # Add a basic filter to make sure we search for something. |
| if not args.procname and not args.pid: |
| args.procname = 'chrome' |
| |
| # Make sure we show memory stats if nothing was specifically requested. |
| if not args.show_net and not args.show_mem: |
| args.show_mem = True |
| |
| devil_chromium.Initialize() |
| |
| curses.setupterm() |
| |
| printer = OutputBeautifier(not args.dull_output, not args.no_overwrite) |
| |
| sys.stdout.write("Running... Hold CTRL-C to stop (or specify timeout).\n") |
| try: |
| last_time = time.time() |
| |
| adb = None |
| old_snapshot = None |
| snapshots = [] |
| while not args.timelimit or Timer.GetTimestamp() < float(args.timelimit): |
| # Check if we need to track another device |
| device = DeviceHelper.GetDeviceToTrack(args.device) |
| if not device: |
| adb = None |
| elif not adb or device != str(adb): |
| #adb = adb_wrapper.AdbWrapper(device) |
| adb = device_utils.DeviceUtils(device) |
| old_snapshot = None |
| snapshots = [] |
| try: |
| adb.EnableRoot() |
| except device_errors.CommandFailedError: |
| sys.stderr.write('Unable to run adb as root.\n') |
| sys.exit(1) |
| |
| # Grab a snapshot if we have a device |
| snapshot = None |
| if adb: |
| pids = DeviceHelper.GetPidsToTrack(adb, args.pid, args.procname) |
| snapshot = None |
| if pids: |
| snapshot = DeviceSnapshot(adb, pids, args.show_mem, args.show_net) |
| |
| if snapshot and snapshot.HasResults(): |
| snapshots.append(snapshot) |
| |
| printer.PrettyPrint(snapshot, old_snapshot, args.show_mem, args.show_net) |
| |
| # Transfer state for the next iteration and sleep |
| delay = max(1, args.frequency) |
| if snapshot: |
| delay = max(0, args.frequency - (time.time() - last_time)) |
| time.sleep(delay) |
| |
| last_time = time.time() |
| if not old_snapshot or not args.diff_against_start: |
| old_snapshot = snapshot |
| except KeyboardInterrupt: |
| pass |
| |
| if args.graph_file: |
| printer.PrettyGraph(args.graph_file, snapshots) |
| |
| if args.text_file: |
| printer.PrettyFile(args.text_file, |
| snapshots, |
| args.diff_against_start, |
| args.show_mem, |
| args.show_net) |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv)) |