Index: tools/android/meminfo.py |
diff --git a/tools/android/meminfo.py b/tools/android/meminfo.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..4958d5b450ab0f15a9f553e64a3c52b7bf19059e |
--- /dev/null |
+++ b/tools/android/meminfo.py |
@@ -0,0 +1,717 @@ |
+#!/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 polling for Chrome on Android |
+ |
+import argparse |
+import curses |
+import os |
+import re |
+import sys |
+import time |
+ |
+from operator import sub |
+ |
+sys.path.append(os.path.join(os.path.dirname(__file__), |
+ os.pardir, |
+ os.pardir, |
+ 'build', |
+ 'android')) |
+from pylib import android_commands |
+from pylib.device import adb_wrapper |
+from pylib.device import device_errors |
+ |
+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 GetDeviceModel(adb): |
+ """Returns the model of the device with the |adb| connection.""" |
+ return adb.Shell(' '.join(['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 = android_commands.GetAttachedDevices() |
+ 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 pids 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.""" |
+ pids = [] |
+ try: |
+ cmd = ['ps'] |
+ if default_pid: |
+ cmd.extend(['|', 'grep', '-F', str(default_pid)]) |
+ if process_filter: |
+ cmd.extend(['|', 'grep', '-F', process_filter]) |
+ pid_str = adb.Shell(' '.join(cmd)) |
+ for line in pid_str.splitlines(): |
+ data = re.split('\s+', line.strip()) |
+ pid = data[1] |
+ name = data[-1] |
+ |
+ # Confirm that the pid and name match. Using a regular grep isn't |
+ # reliable when doing it on the whole 'ps' input line. |
+ pid_matches = not default_pid or pid == str(default_pid) |
+ name_matches = not process_filter or name.find(process_filter) != -1 |
+ if pid_matches and name_matches: |
+ pids.append((pid, name)) |
+ except device_errors.AdbShellCommandFailedError: |
+ pass |
+ return pids |
+ |
+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] |
+ |
+ memstr = adb.Shell(' '.join(['dumpsys', 'meminfo', pid])) |
+ for line in memstr.splitlines(): |
+ 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: |
+ memstr = adb.Shell(' '.join(['showmap', '-t', pid])) |
+ for line in memstr.splitlines(): |
+ 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: |
+ exists = adb.shell(' '.join(['ls', nv_file])) |
+ if exists == nv_file.split('/')[-1]: |
+ 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: |
+ memstr = adb.Shell(' '.join(['cat', nv_file])) |
+ for line in memstr.splitlines(): |
+ 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 MemorySnapshot(object): |
+ """A class holding a snapshot of memory for various pids that are being |
+ tracked. |
+ |
+ Attributes: |
+ pids: A list of tuples (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 ]. |
+ timestamp: The amount of time (in seconds) between when this program started |
+ and this snapshot was taken. |
+ """ |
+ |
+ def __init__(self, adb, pids): |
+ """Creates an instances of a MemorySnapshot with an |adb| device connection |
+ and a list of (pid, process name) tuples.""" |
+ super(MemorySnapshot, self).__init__() |
+ |
+ self.pids = pids |
+ self.memory = {} |
+ self.timestamp = Timer.GetTimestamp() |
+ |
+ for (pid, name) in pids: |
+ self.memory[pid] = self.__QueryMemoryForPid(adb, pid) |
+ |
+ @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[1] for tuple in self.pids] |
+ |
+ def HasResults(self): |
+ """Whether or not this snapshot was tracking any processes.""" |
+ return self.pids |
+ |
+ def GetPidAndNames(self): |
+ """Returns a list of (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 (pid, name) in self.pids: |
+ if pid == search_pid: |
+ return name |
+ return None |
+ |
+ def GetResults(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 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'] |
+ |
+ __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 (pid, name) in snapshot.GetPidAndNames(): |
+ pids.add((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' |
+ |
+ 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 __PrintBasicStatsHeader(self): |
+ """Returns a common header for the memory usage stats.""" |
+ titles = '' |
+ for title in self.__MEMORY_COLUMN_TITLES: |
+ titles += self.__PadString(title, 8, True) + ' ' |
+ titles += self.__PadString('', 8, True) |
+ return self.__ColorString(titles, 'BOLD') |
+ |
+ def __PrintLabeledStatsHeader(self, snapshot): |
+ """Returns a header for the memory usage stats that includes sections for |
+ the pid and the process name. The available room given to the process name |
+ is based on the length of the longest process name tracked by |snapshot|. |
+ This header also puts the timestamp of the snapshot on the right.""" |
+ if not snapshot or not snapshot.HasResults(): |
+ return |
+ |
+ name_length = max(8, snapshot.GetLongestNameLength()) |
+ |
+ titles = self.__PadString('Pid', 8, True) + ' ' |
+ titles += self.__PadString('Name', name_length, False) + ' ' |
+ titles += self.__PrintBasicStatsHeader() |
+ titles += '(' + str(round(snapshot.GetTimestamp(), 2)) + 's)' |
+ titles = self.__ColorString(titles, 'BOLD') |
+ return titles |
+ |
+ def __PrintTimestampedBasicStatsHeader(self): |
+ """Returns a header for the memory usage stats that includes a the |
+ timestamp of the snapshot.""" |
+ titles = self.__PadString('Timestamp', 8, False) + ' ' |
+ titles = self.__ColorString(titles, 'BOLD') |
+ titles += self.__PrintBasicStatsHeader() |
+ return titles |
+ |
+ def __PrintBasicSnapshotStats(self, pid, snapshot, prev_snapshot): |
+ """Returns a string that contains the basic snapshot memory statistics. |
+ This string should line up with the header returned by |
+ |self.__PrintBasicStatsHeader|.""" |
+ if not snapshot or not snapshot.HasResults(): |
+ return |
+ |
+ results = snapshot.GetResults(pid) |
+ if not results: |
+ return |
+ |
+ old_results = prev_snapshot.GetResults(pid) if prev_snapshot else None |
+ |
+ # Build Delta List |
+ deltas = [ 0, 0, 0, 0 ] |
+ if old_results: |
+ deltas = map(sub, results, old_results) |
+ assert len(deltas) == len(results) |
+ |
+ output = '' |
+ for idx, mem in enumerate(results): |
+ output += self.__PadString(mem, 8, True) + ' ' |
+ output += self.__PadAndColor('(' + str(round(deltas[idx], 2)) + ')', |
+ 8, False, self.__GetDiffColor(deltas[idx])) |
+ |
+ return output |
+ |
+ def __PrintLabeledSnapshotStats(self, pid, snapshot, prev_snapshot): |
+ """Returns a string that contains memory usage stats along with the pid and |
+ process name. This string should line up with the header returned by |
+ |self.__PrintLabeledStatsHeader|.""" |
+ 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) + ' ' |
+ output += self.__PrintBasicSnapshotStats(pid, snapshot, prev_snapshot) |
+ return output |
+ |
+ def __PrintTimestampedBasicSnapshotStats(self, pid, snapshot, prev_snapshot): |
+ """Returns a string that contains memory usage stats along with the |
+ timestamp of the snapshot. This string should line up with the header |
+ returned by |self.__PrintTimestampedBasicStatsHeader|.""" |
+ 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) + ' ' |
+ output += self.__PrintBasicSnapshotStats(pid, snapshot, prev_snapshot) |
+ return output |
+ |
+ def PrettyPrint(self, snapshot, prev_snapshot): |
+ """Prints |snapshot| to the console. This will show memory deltas between |
+ |snapshot| and |prev_snapshot|. This will also either color or overwrite |
+ the previous entries based on |self.can_color| and |self.overwrite|.""" |
+ self.__ClearScreen() |
+ |
+ if not snapshot or not snapshot.HasResults(): |
+ self.__OutputLine("No results...") |
+ return |
+ |
+ self.__OutputLine(self.__PrintLabeledStatsHeader(snapshot)) |
+ |
+ for (pid, name) in snapshot.GetPidAndNames(): |
+ self.__OutputLine(self.__PrintLabeledSnapshotStats(pid, |
+ snapshot, |
+ prev_snapshot)) |
+ |
+ def PrettyFile(self, file_path, snapshots, diff_against_start): |
+ """Writes |snapshots| (a list of MemorySnapshots) 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 not file_path or not snapshots: |
+ return |
+ |
+ 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 (pid, name) in pids: |
+ out.write(name + ' (' + str(pid) + '):\n') |
+ out.write(self.__PrintTimestampedBasicStatsHeader()) |
+ out.write('\n') |
+ |
+ prev_snapshot = None |
+ for snapshot in snapshots: |
+ if not snapshot.GetResults(pid): |
+ continue |
+ out.write(self.__PrintTimestampedBasicSnapshotStats(pid, |
+ snapshot, |
+ prev_snapshot)) |
+ 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 MemorySnapshots) at |
+ |file_path|.""" |
+ # 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 (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.GetResults(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('-n', |
+ '--no-overwrite', |
+ 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.') |
+ |
+ 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' |
+ |
+ 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) |
+ old_snapshot = None |
+ snapshots = [] |
+ try: |
+ adb.Root() |
+ except device_errors.AdbCommandFailedError: |
+ 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 = MemorySnapshot(adb, pids) if pids else None |
+ |
+ if snapshot and snapshot.HasResults(): |
+ snapshots.append(snapshot) |
+ |
+ printer.PrettyPrint(snapshot, old_snapshot) |
+ |
+ # 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) |
+ |
+if __name__ == '__main__': |
+ sys.exit(main(sys.argv)) |
+ |