Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/python | |
| 2 # Copyright 2015 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 # 'top'-like memory polling for Chrome on Android | |
| 7 | |
| 8 import argparse | |
| 9 import commands | |
|
nyquist
2015/07/17 21:42:11
Nit: Is this import unused?
David Trainor- moved to gerrit
2015/07/21 18:19:57
Done.
| |
| 10 import copy | |
|
nyquist
2015/07/17 21:42:10
Nit: Is this import unused?
David Trainor- moved to gerrit
2015/07/21 18:19:57
Done.
| |
| 11 import curses | |
| 12 import os | |
| 13 import re | |
| 14 import sys | |
| 15 import time | |
| 16 | |
| 17 from operator import sub | |
| 18 | |
| 19 sys.path.append(os.path.join(os.path.dirname(__file__), | |
| 20 os.pardir, | |
| 21 os.pardir, | |
| 22 'build', | |
| 23 'android')) | |
| 24 from pylib import android_commands | |
| 25 from pylib.device import adb_wrapper | |
| 26 from pylib.device import device_errors | |
| 27 | |
| 28 class Validator(object): | |
| 29 """A helper class with validation methods for argparse.""" | |
| 30 | |
| 31 @staticmethod | |
| 32 def ValidatePath(path): | |
| 33 """An argparse validation method to make sure a file path is writable.""" | |
| 34 if os.path.exists(path): | |
| 35 return path | |
| 36 elif os.access(os.path.dirname(path), os.W_OK): | |
| 37 return path | |
| 38 raise argparse.ArgumentTypeError("%s is an invalid file path" % path) | |
| 39 | |
| 40 @staticmethod | |
| 41 def ValidatePdfPath(path): | |
| 42 """An argparse validation method to make sure a pdf file path is writable. | |
| 43 Validates a file path to make sure it is writable and also appends '.pdf' if | |
| 44 necessary.""" | |
| 45 if os.path.splitext(path)[-1].lower() != 'pdf': | |
| 46 path = path + '.pdf' | |
| 47 return Validator.ValidatePath(path) | |
| 48 | |
| 49 @staticmethod | |
| 50 def ValidatePositiveNumber(val): | |
|
nyquist
2015/07/17 21:42:11
This method allows 0, which is not positive. How a
David Trainor- moved to gerrit
2015/07/21 18:19:57
Done.
| |
| 51 """An argparse validation method to make sure a number is positive.""" | |
| 52 ival = int(val) | |
| 53 if ival < 0: | |
| 54 raise argparse.ArgumentTypeError("%s is not a positive integer" % val) | |
| 55 return ival | |
| 56 | |
| 57 class Timer(object): | |
| 58 """A helper class to track timestamps based on when this program was | |
| 59 started""" | |
| 60 starting_time = time.time() | |
| 61 | |
| 62 @staticmethod | |
| 63 def GetTimestamp(): | |
| 64 """A helper method to return the time (in seconds) since this program was | |
| 65 started.""" | |
| 66 return time.time() - Timer.starting_time | |
| 67 | |
| 68 class DeviceHelper(object): | |
| 69 """A helper class with various generic device interaction methods.""" | |
| 70 | |
| 71 @staticmethod | |
| 72 def GetDeviceModel(adb): | |
| 73 """Returns the model of the device with the |adb| connection.""" | |
| 74 return adb.Shell(' '.join(['getprop', 'ro.product.model'])).strip() | |
| 75 | |
| 76 @staticmethod | |
| 77 def GetDeviceToTrack(preset=None): | |
| 78 """Returns a device serial to connect to. If |preset| is specified it will | |
| 79 return |preset| if it is connected and |None| otherwise. If |preset| is not | |
| 80 specified it will return the first connected device.""" | |
| 81 devices = android_commands.GetAttachedDevices() | |
| 82 if not devices: | |
| 83 return None | |
| 84 | |
| 85 if preset: | |
| 86 return preset if preset in devices else None | |
| 87 | |
| 88 return devices[0] | |
| 89 | |
| 90 @staticmethod | |
| 91 def GetPidsToTrack(adb, default_pid=None, process_filter=None): | |
| 92 """Returns a list of pids based on the input arguments. If |default_pid| is | |
| 93 specified it will return that pid if it exists. If |process_filter| is | |
| 94 specified it will return the pids of processes with that string in the name. | |
| 95 If both are specified it will intersect the two.""" | |
| 96 pids = [] | |
| 97 try: | |
| 98 cmd = ['ps'] | |
| 99 if default_pid: | |
| 100 cmd.extend(['|', 'grep', '-F', str(default_pid)]) | |
| 101 if process_filter: | |
| 102 cmd.extend(['|', 'grep', '-F', process_filter]) | |
| 103 pid_str = adb.Shell(' '.join(cmd)) | |
| 104 for line in pid_str.splitlines(): | |
| 105 data = re.split('\s+', line.strip()) | |
| 106 pid = data[1] | |
| 107 name = data[-1] | |
| 108 | |
| 109 # Confirm that the pid and name match. Using a regular grep isn't | |
| 110 # reliable when doing it on the whole 'ps' input line. | |
| 111 if (not default_pid or pid == str(default_pid)) and (not process_filter | |
|
nyquist
2015/07/17 21:42:10
this if-block looks a bit confusing since only the
David Trainor- moved to gerrit
2015/07/21 18:19:56
Done.
| |
| 112 or name.find(process_filter) != -1): | |
| 113 pids.append((pid, name)) | |
| 114 except device_errors.AdbShellCommandFailedError: | |
| 115 pass | |
| 116 return pids | |
| 117 | |
| 118 class MemoryHelper(object): | |
| 119 """A helper class to query basic memory usage of a process.""" | |
| 120 | |
| 121 @staticmethod | |
| 122 def QueryMemory(adb, pid): | |
| 123 """Queries the device for memory information about the process with a pid of | |
| 124 |pid|. It will query Native, Dalvik, and Pss memory of the process. It | |
| 125 returns a list of values: [ Native, Pss, Dalvik]. If the process is not | |
|
nyquist
2015/07/17 21:42:11
Nit: Missing space after Dalvid, or extra space be
David Trainor- moved to gerrit
2015/07/21 18:19:57
Done.
| |
| 126 found it will return [ 0, 0, 0 ].""" | |
| 127 results = [0, 0, 0] | |
| 128 | |
| 129 memstr = adb.Shell(' '.join(['dumpsys', 'meminfo', pid])) | |
| 130 for line in memstr.splitlines(): | |
| 131 match = re.split('\s+', line.strip()) | |
| 132 result_idx = None | |
| 133 data_idx = None | |
|
nyquist
2015/07/17 21:42:10
query_idx?
David Trainor- moved to gerrit
2015/07/21 18:19:56
gah good catch!
| |
| 134 if match[0] == 'Native': | |
|
nyquist
2015/07/17 21:42:10
Would this match the app summary as well?
...
App
David Trainor- moved to gerrit
2015/07/21 18:19:56
Since the query_idx is -2, wouldn't it pull the "h
| |
| 135 result_idx = 0 | |
| 136 query_idx = -2 | |
| 137 elif match[0] == 'Dalvik' and match[1] == 'Heap': | |
| 138 result_idx = 2 | |
| 139 query_idx = -2 | |
| 140 elif match[0] == 'TOTAL': | |
| 141 result_idx = 1 | |
| 142 query_idx = 1 | |
| 143 | |
| 144 if result_idx is not None and query_idx is not None: | |
| 145 results[result_idx] = round(float(match[query_idx]) / 1000.0, 2) | |
| 146 return results | |
| 147 | |
| 148 class GraphicsHelper(object): | |
| 149 """A helper class to query basic graphics memory usage of a process.""" | |
| 150 | |
| 151 # TODO(dtrainor): Find a generic way to query/fall back for other devices. | |
|
nyquist
2015/07/17 21:42:11
Nit: Is this indent using spaces instead of tabs?
David Trainor- moved to gerrit
2015/07/21 18:19:56
I don't think so. At least not in my editor! Wil
| |
| 152 # Is showmap consistently reliable? | |
| 153 __NV_MAP_MODELS = ['Xoom'] | |
|
nyquist
2015/07/17 21:42:11
Do we really need to support Xoom?
David Trainor- moved to gerrit
2015/07/21 18:19:57
No, but I'm not sure if other drivers use NV_MAP f
| |
| 154 __NV_MAP_FILE_LOCATIONS = ['/d/nvmap/generic-0/clients', | |
| 155 '/d/nvmap/iovmm/clients'] | |
| 156 | |
| 157 __SHOWMAP_MODELS = ['Nexus S', | |
| 158 'Nexus S 4G', | |
| 159 'Galaxy Nexus', | |
| 160 'Nexus 4', | |
| 161 'Nexus 5', | |
| 162 'Nexus 7'] | |
| 163 __SHOWMAP_KEY_MATCHES = ['/dev/pvrsrvkm', | |
| 164 '/dev/kgsl-3d0'] | |
| 165 | |
| 166 @staticmethod | |
| 167 def __QueryShowmap(adb, pid): | |
| 168 """Attempts to query graphics memory via the 'showmap' command. It will | |
| 169 look for |self.__SHOWMAP_KEY_MATCHES| entries to try to find one that | |
| 170 represents the graphics memory usage. Will return this as a single entry | |
| 171 array of [ Graphics ]. If not found, will return [ 0 ].""" | |
| 172 try: | |
| 173 memstr = adb.Shell(' '.join(['showmap', '-t', pid])) | |
| 174 for line in memstr.splitlines(): | |
| 175 match = re.split('[ ]+', line.strip()) | |
| 176 if match[-1] in GraphicsHelper.__SHOWMAP_KEY_MATCHES: | |
| 177 return [ round(float(match[2]) / 1000.0, 2) ] | |
| 178 except device_errors.AdbShellCommandFailedError: | |
| 179 pass | |
| 180 return [ 0 ] | |
| 181 | |
| 182 @staticmethod | |
| 183 def __NvMapPath(adb): | |
| 184 """Attempts to find a valid NV Map file on the device. It will look for a | |
| 185 file in |self.__NV_MAP_FILE_LOCATIONS| and see if one exists. If so, it | |
| 186 will return it.""" | |
| 187 for nv_file in GraphicsHelper.__NV_MAP_FILE_LOCATIONS: | |
| 188 exists = adb.shell(' '.join(['ls', nv_file])) | |
| 189 if exists == nv_file.split('/')[-1]: | |
| 190 return nv_file | |
| 191 return None | |
| 192 | |
| 193 @staticmethod | |
| 194 def __QueryNvMap(adb, pid): | |
| 195 """Attempts to query graphics memory via the NV file map method. It will | |
| 196 find a possible NV Map file from |self.__NvMapPath| and try to parse the | |
| 197 graphics memory from it. Will return this as a single entry array of | |
| 198 [ Graphics ]. If not found, will return [ 0 ].""" | |
| 199 nv_file = GraphicsHelper.__NvMapPath(adb) | |
| 200 if nv_file: | |
| 201 memstr = adb.Shell(' '.join(['cat', nv_file])) | |
| 202 for line in memstr.splitlines(): | |
| 203 match = re.split(' +', line.strip()) | |
| 204 if match[2] == pid: | |
| 205 return [ round(float(match[3]) / 1000000.0, 2) ] | |
| 206 return [ 0 ] | |
| 207 | |
| 208 @staticmethod | |
| 209 def QueryVideoMemory(adb, pid): | |
| 210 """Queries the device for graphics memory information about the process with | |
| 211 a pid of |pid|. Not all devices are currently supported. If possible, this | |
| 212 will return a single entry array of [ Graphics ]. Otherwise it will return | |
| 213 [ 0 ]. | |
| 214 | |
| 215 Please see |self.__NV_MAP_MODELS| and |self.__SHOWMAP_MODELS| | |
| 216 to see if the device is supported. For new devices, see if they can be | |
| 217 supported by existing methods and add their entry appropriately. Also, | |
| 218 please add any new way of querying graphics memory as they become | |
| 219 available.""" | |
| 220 model = DeviceHelper.GetDeviceModel(adb) | |
| 221 if model in GraphicsHelper.__NV_MAP_MODELS: | |
| 222 return GraphisHelper.__QueryNvMap(adb, pid) | |
|
nyquist
2015/07/17 21:42:10
GraphicsHelper
David Trainor- moved to gerrit
2015/07/21 18:19:57
Guess you can tell I didn't test zoom since the re
| |
| 223 elif model in GraphicsHelper.__SHOWMAP_MODELS: | |
| 224 return GraphicsHelper.__QueryShowmap(adb, pid) | |
| 225 return [ 0 ] | |
| 226 | |
| 227 class MemorySnapshot(object): | |
| 228 """A class holding a snapshot of memory for various pids that are being | |
| 229 tracked. | |
| 230 | |
| 231 Attributes: | |
| 232 pids: A list of tuples (pid, process name) that should be tracked. | |
| 233 memory: A map of entries of pid => memory consumption array. Right now | |
| 234 the indices are [ Native, Pss, Dalvik, Graphics ]. | |
| 235 timestamp: The amount of time (in seconds) between when this program started | |
| 236 and this snapshot was taken. | |
| 237 """ | |
| 238 | |
| 239 def __init__(self, adb, pids): | |
| 240 """Creates an instances of a MemorySnapshot with an |adb| device connection | |
| 241 and a list of (pid, process name) tuples.""" | |
| 242 super(MemorySnapshot, self).__init__() | |
| 243 | |
| 244 self.pids = pids | |
| 245 self.memory = {} | |
| 246 self.timestamp = Timer.GetTimestamp() | |
| 247 | |
| 248 for (pid, name) in pids: | |
| 249 self.memory[pid] = self.__QueryMemoryForPid(adb, pid) | |
| 250 | |
| 251 @staticmethod | |
| 252 def __QueryMemoryForPid(adb, pid): | |
| 253 """Queries the |adb| device for memory information about |pid|. This will | |
| 254 return a list of memory values that map to [ Native, Pss, Dalvik, | |
| 255 Graphics ].""" | |
| 256 results = MemoryHelper.QueryMemory(adb, pid) | |
| 257 results.extend(GraphicsHelper.QueryVideoMemory(adb, pid)) | |
| 258 return results | |
| 259 | |
| 260 def __GetProcessNames(self): | |
| 261 """Returns a list of all of the process names tracked by this snapshot.""" | |
| 262 return [tuple[1] for tuple in self.pids] | |
| 263 | |
| 264 def HasResults(self): | |
| 265 """Whether or not this snapshot was tracking any processes.""" | |
| 266 return self.pids | |
| 267 | |
| 268 def GetPidAndNames(self): | |
| 269 """Returns a list of (pid, process name) tuples that are being tracked in | |
| 270 this snapshot.""" | |
| 271 return self.pids | |
| 272 | |
| 273 def GetNameForPid(self, search_pid): | |
| 274 """Returns the process name of a tracked |search_pid|. This only works if | |
| 275 |search_pid| is tracked by this snapshot.""" | |
| 276 for (pid, name) in self.pids: | |
| 277 if pid == search_pid: | |
| 278 return name | |
| 279 return None | |
| 280 | |
| 281 def GetResults(self, pid): | |
| 282 """Returns a list of entries about the memory usage of the process specified | |
| 283 by |pid|. This will be of the format [ Native, Pss, Dalvik, Graphics ].""" | |
| 284 if pid in self.memory: | |
| 285 return self.memory[pid] | |
| 286 return None | |
| 287 | |
| 288 def GetLongestNameLength(self): | |
| 289 """Returns the length of the longest process name tracked by this | |
| 290 snapshot.""" | |
| 291 return len(max(self.__GetProcessNames(), key=len)) | |
| 292 | |
| 293 def GetTimestamp(self): | |
| 294 """Returns the time since program start that this snapshot was taken.""" | |
| 295 return self.timestamp | |
| 296 | |
| 297 class OutputBeautifier(object): | |
| 298 """A helper class to beautify the memory output to various destinations. | |
| 299 | |
| 300 Attributes: | |
| 301 can_color: Whether or not the output should include ASCII color codes to | |
| 302 make it look nicer. Default is |True|. This is disabled when | |
| 303 writing to a file or a graph. | |
| 304 overwrite: Whether or not the output should overwrite the previous output. | |
| 305 Default is |True|. This is disabled when writing to a file or a | |
| 306 graph. | |
| 307 """ | |
| 308 | |
| 309 __MEMORY_COLUMN_TITLES = ['Native', | |
| 310 'Pss', | |
| 311 'Dalvik', | |
| 312 'Graphics'] | |
| 313 | |
| 314 __TERMINAL_COLORS = {'ENDC': 0, | |
| 315 'BOLD': 1, | |
| 316 'GREY30': 90, | |
| 317 'RED': 91, | |
| 318 'DARK_YELLOW': 33, | |
| 319 'GREEN': 92} | |
| 320 | |
| 321 def __init__(self, can_color=True, overwrite=True): | |
| 322 """Creates an instance of an OutputBeautifier.""" | |
| 323 super(OutputBeautifier, self).__init__() | |
| 324 self.can_color = can_color | |
| 325 self.overwrite = overwrite | |
| 326 | |
| 327 self.lines_printed = 0 | |
| 328 self.printed_header = False | |
| 329 | |
| 330 @staticmethod | |
| 331 def __TermCode(num): | |
| 332 """Escapes a terminal code. See |self.__TERMINAL_COLORS| for a list of some | |
| 333 terminal codes that are used by this program.""" | |
| 334 return '\033[%sm'%num | |
|
nyquist
2015/07/17 21:42:11
Nit: Add spaces around the last % to separate form
David Trainor- moved to gerrit
2015/07/21 18:19:56
Done.
| |
| 335 | |
| 336 def __ColorString(self, string, color): | |
| 337 """Colors |string| based on |color|. |color| must be in | |
| 338 |self.__TERMINAL_COLORS|. Returns the colored string or the original | |
| 339 string if |self.can_color| is |False| or the |color| is invalid.""" | |
| 340 if not self.can_color or not color or not self.__TERMINAL_COLORS[color]: | |
| 341 return string | |
| 342 | |
| 343 return '%s%s%s' % (self.__TermCode(self.__TERMINAL_COLORS[color]), | |
| 344 string, | |
| 345 self.__TermCode(self.__TERMINAL_COLORS['ENDC'])) | |
| 346 | |
| 347 @staticmethod | |
| 348 def __PadString(string, length, left_align): | |
| 349 """Pads |string| to at least |length| with spaces. Depending on | |
| 350 |left_align| the padding will appear at either the left or the right of the | |
| 351 original string.""" | |
| 352 return (('%' if left_align else '%-') + str(length) + 's') % string | |
| 353 | |
| 354 def __PadAndColor(self, string, length, left_align, color): | |
| 355 """A helper method to both pad and color the string. See | |
| 356 |self.__ColorString| and |self.__PadString|.""" | |
| 357 return self.__ColorString( | |
| 358 self.__PadString(string, length, left_align), color) | |
| 359 | |
| 360 @staticmethod | |
| 361 def __GetDiffColor(delta): | |
| 362 """Returns a color based on |delta|. Used to color the deltas between | |
| 363 different snapshots.""" | |
| 364 if not delta or delta == 0.0: | |
| 365 return 'GREY30' | |
| 366 elif delta < 0: | |
| 367 return 'GREEN' | |
| 368 elif delta > 0: | |
| 369 return 'RED' | |
| 370 | |
| 371 def __OutputLine(self, line): | |
| 372 """Writes a line to the screen. This also tracks how many times this method | |
| 373 was called so that the screen can be cleared properly if |self.overwrite| is | |
| 374 |True|.""" | |
| 375 sys.stdout.write(line + '\n') | |
| 376 if self.overwrite: | |
| 377 self.lines_printed += 1 | |
| 378 | |
| 379 def __ClearScreen(self): | |
| 380 """Clears the screen based on the number of times |self.__OutputLine| was | |
| 381 called.""" | |
| 382 if self.lines_printed == 0 or not self.overwrite: | |
| 383 return | |
| 384 | |
| 385 key_term_up = curses.tparm(curses.tigetstr('cuu1')) | |
| 386 key_term_clear_eol = curses.tparm(curses.tigetstr('el')) | |
| 387 key_term_go_to_bol = curses.tparm(curses.tigetstr('cr')) | |
| 388 | |
| 389 sys.stdout.write(key_term_go_to_bol) | |
| 390 sys.stdout.write(key_term_clear_eol) | |
| 391 | |
| 392 for i in range(self.lines_printed): | |
| 393 sys.stdout.write(key_term_up) | |
| 394 sys.stdout.write(key_term_clear_eol) | |
| 395 self.lines_printed = 0 | |
| 396 | |
| 397 def __PrintBasicStatsHeader(self): | |
| 398 """Returns a common header for the memory usage stats.""" | |
| 399 titles = '' | |
| 400 for title in self.__MEMORY_COLUMN_TITLES: | |
| 401 titles += self.__PadString(title, 8, True) + ' ' | |
| 402 titles += self.__PadString('', 8, True) | |
| 403 return self.__ColorString(titles, 'BOLD') | |
| 404 | |
| 405 def __PrintLabeledStatsHeader(self, snapshot): | |
| 406 """Returns a header for the memory usage stats that includes sections for | |
| 407 the pid and the process name. The available room given to the process name | |
| 408 is based on the length of the longest process name tracked by |snapshot|. | |
| 409 This header also puts the timestamp of the snapshot on the right.""" | |
| 410 if not snapshot or not snapshot.HasResults(): | |
| 411 return | |
| 412 | |
| 413 name_length = max(8, snapshot.GetLongestNameLength()) | |
| 414 | |
| 415 titles = self.__PadString('Pid', 8, True) + ' ' | |
| 416 titles += self.__PadString('Name', name_length, False) + ' ' | |
| 417 titles += self.__PrintBasicStatsHeader() | |
| 418 titles += '(' + str(round(snapshot.GetTimestamp(), 2)) + 's)' | |
| 419 titles = self.__ColorString(titles, 'BOLD') | |
| 420 return titles | |
| 421 | |
| 422 def __PrintTimestampedBasicStatsHeader(self): | |
| 423 """Returns a header for the memory usage stats that includes a the | |
| 424 timestamp of the snapshot.""" | |
| 425 titles = self.__PadString('Timestamp', 8, False) + ' ' | |
| 426 titles = self.__ColorString(titles, 'BOLD') | |
| 427 titles += self.__PrintBasicStatsHeader() | |
| 428 return titles | |
| 429 | |
| 430 def __PrintBasicSnapshotStats(self, pid, snapshot, prev_snapshot): | |
| 431 """Returns a string that contains the basic snapshot memory statistics. | |
| 432 This string should line up with the header returned by | |
| 433 |self.__PrintBasicStatsHeader|.""" | |
| 434 if not snapshot or not snapshot.HasResults(): | |
| 435 return | |
| 436 | |
| 437 results = snapshot.GetResults(pid) | |
| 438 if not results: | |
| 439 return | |
| 440 | |
| 441 old_results = prev_snapshot.GetResults(pid) if prev_snapshot else None | |
| 442 | |
| 443 # Build Delta List | |
| 444 deltas = [ 0, 0, 0, 0 ] | |
| 445 if old_results: | |
| 446 deltas = map(sub, results, old_results) | |
| 447 assert len(deltas) == len(results) | |
| 448 for idx, delta in enumerate(deltas): | |
| 449 delta = round(delta, 2) | |
|
nyquist
2015/07/17 21:42:10
Does this really edit the values in |deltas|? You
David Trainor- moved to gerrit
2015/07/21 18:19:56
Turns out this code can be removed because I run b
| |
| 450 | |
| 451 output = '' | |
| 452 for idx, mem in enumerate(results): | |
| 453 output += self.__PadString(mem, 8, True) + ' ' | |
| 454 output += self.__PadAndColor('(' + str(round(deltas[idx], 2)) + ')', | |
| 455 8, False, self.__GetDiffColor(deltas[idx])) | |
|
nyquist
2015/07/17 21:42:11
Nit: Should this be indented one more step?
David Trainor- moved to gerrit
2015/07/21 18:19:57
Done.
| |
| 456 | |
| 457 return output | |
| 458 | |
| 459 def __PrintLabeledSnapshotStats(self, pid, snapshot, prev_snapshot): | |
| 460 """Returns a string that contains memory usage stats along with the pid and | |
| 461 process name. This string should line up with the header returned by | |
| 462 |self.__PrintLabeledStatsHeader|.""" | |
| 463 if not snapshot or not snapshot.HasResults(): | |
| 464 return | |
| 465 | |
| 466 name_length = max(8, snapshot.GetLongestNameLength()) | |
| 467 name = snapshot.GetNameForPid(pid) | |
| 468 | |
| 469 output = self.__PadAndColor(pid, 8, True, 'DARK_YELLOW') + ' ' | |
| 470 output += self.__PadAndColor(name, name_length, False, None) + ' ' | |
| 471 output += self.__PrintBasicSnapshotStats(pid, snapshot, prev_snapshot) | |
| 472 return output | |
| 473 | |
| 474 def __PrintTimestampedBasicSnapshotStats(self, pid, snapshot, prev_snapshot): | |
| 475 """Returns a string that contains memory usage stats along with the | |
| 476 timestamp of the snapshot. This string should line up with the header | |
| 477 returned by |self.__PrintTimestampedBasicStatsHeader|.""" | |
| 478 if not snapshot or not snapshot.HasResults(): | |
| 479 return | |
| 480 | |
| 481 timestamp_length = max(8, len("Timestamp")) | |
| 482 timestamp = round(snapshot.GetTimestamp(), 2) | |
| 483 | |
| 484 output = self.__PadString(str(timestamp), timestamp_length, True) + ' ' | |
| 485 output += self.__PrintBasicSnapshotStats(pid, snapshot, prev_snapshot) | |
| 486 return output | |
| 487 | |
| 488 def PrettyPrint(self, snapshot, prev_snapshot): | |
| 489 """Prints |snapshot| to the console. This will show memory deltas between | |
| 490 |snapshot| and |prev_snapshot|. This will also either color or overwrite | |
| 491 the previous entries based on |self.can_color| and |self.overwrite|.""" | |
| 492 self.__ClearScreen() | |
| 493 | |
| 494 if not snapshot or not snapshot.HasResults(): | |
| 495 self.__OutputLine("No results...") | |
| 496 return | |
| 497 | |
| 498 self.__OutputLine(self.__PrintLabeledStatsHeader(snapshot)) | |
| 499 | |
| 500 for (pid, name) in snapshot.GetPidAndNames(): | |
| 501 self.__OutputLine(self.__PrintLabeledSnapshotStats(pid, | |
| 502 snapshot, | |
| 503 prev_snapshot)) | |
| 504 | |
| 505 def PrettyFile(self, file_path, snapshots, diff_against_start): | |
| 506 """Writes |snapshots| (a list of MemorySnapshots) to |file_path|. | |
| 507 |diff_against_start| determines whether or not the snapshot deltas are | |
| 508 between the first entry and all entries or each previous entry. This output | |
| 509 will not follow |self.can_color| or |self.overwrite|.""" | |
| 510 if not file_path or not snapshots: | |
| 511 return | |
| 512 | |
| 513 pids = set() | |
|
nyquist
2015/07/17 21:42:11
This is done both here and in PrettyGraph. Extract
David Trainor- moved to gerrit
2015/07/21 18:19:56
Done.
| |
| 514 # Find all unique pids | |
| 515 for snapshot in snapshots: | |
| 516 for (pid, name) in snapshot.GetPidAndNames(): | |
| 517 pids.add((pid, name)) | |
| 518 | |
| 519 # Disable special output formatting for file writing. | |
| 520 can_color = self.can_color | |
| 521 self.can_color = False | |
| 522 | |
| 523 with open(file_path, 'w') as out: | |
| 524 for (pid, name) in pids: | |
| 525 out.write(name + ' (' + str(pid) + '):\n') | |
| 526 out.write(self.__PrintTimestampedBasicStatsHeader()) | |
| 527 out.write('\n') | |
| 528 | |
| 529 prev_snapshot = None | |
| 530 for snapshot in snapshots: | |
| 531 if not snapshot.GetResults(pid): | |
| 532 continue | |
| 533 out.write(self.__PrintTimestampedBasicSnapshotStats(pid, | |
| 534 snapshot, | |
| 535 prev_snapshot)) | |
| 536 out.write('\n') | |
| 537 if not prev_snapshot or not diff_against_start: | |
| 538 prev_snapshot = snapshot | |
| 539 out.write('\n\n') | |
| 540 | |
| 541 # Restore special output formatting. | |
| 542 self.can_color = can_color | |
| 543 | |
| 544 def PrettyGraph(self, file_path, snapshots): | |
| 545 """Creates a pdf graph of |snapshots| (a list of MemorySnapshots) at | |
| 546 |file_path|.""" | |
| 547 # Import these here so the rest of the functionality doesn't rely on | |
| 548 # matplotlib | |
| 549 from matplotlib import pyplot | |
| 550 from matplotlib.backends.backend_pdf import PdfPages | |
| 551 | |
| 552 if not file_path or not snapshots: | |
| 553 return | |
| 554 | |
| 555 # Find all unique (pid, name) pairs | |
| 556 pids = set() | |
| 557 for snapshot in snapshots: | |
| 558 for (pid, name) in snapshot.GetPidAndNames(): | |
| 559 pids.add((pid, name)) | |
| 560 | |
| 561 pp = PdfPages(file_path) | |
| 562 for (pid, name) in pids: | |
| 563 figure = pyplot.figure() | |
| 564 ax = figure.add_subplot(1, 1, 1) | |
| 565 ax.set_xlabel('Time (s)') | |
| 566 ax.set_ylabel('MB') | |
| 567 ax.set_title(name + ' (' + pid + ')') | |
| 568 | |
| 569 mem_list = [[] for x in range(len(self.__MEMORY_COLUMN_TITLES))] | |
| 570 timestamps = [] | |
| 571 | |
| 572 for snapshot in snapshots: | |
| 573 results = snapshot.GetResults(pid) | |
| 574 if not results: | |
| 575 continue | |
| 576 | |
| 577 timestamps.append(round(snapshot.GetTimestamp(), 2)) | |
| 578 | |
| 579 assert len(results) == len(self.__MEMORY_COLUMN_TITLES) | |
| 580 for idx, result in enumerate(results): | |
| 581 mem_list[idx].append(result) | |
| 582 | |
| 583 colors = [] | |
| 584 for data in mem_list: | |
| 585 colors.append(ax.plot(timestamps, data)[0]) | |
| 586 for i in xrange(len(timestamps)): | |
| 587 ax.annotate(data[i], xy=(timestamps[i], data[i])) | |
| 588 figure.legend(colors, self.__MEMORY_COLUMN_TITLES) | |
| 589 pp.savefig() | |
| 590 pp.close() | |
| 591 | |
| 592 def main(argv): | |
| 593 parser = argparse.ArgumentParser() | |
| 594 parser.add_argument('--process', | |
| 595 dest='procname', | |
| 596 help="A (sub)string to match against process names.") | |
| 597 parser.add_argument('-p', | |
| 598 '--pid', | |
| 599 dest='pid', | |
| 600 type=Validator.ValidatePositiveNumber, | |
| 601 help='Which pid to scan for.') | |
| 602 parser.add_argument('-d', | |
| 603 '--device', | |
| 604 dest='device', | |
| 605 help='Device serial to scan.') | |
| 606 parser.add_argument('-t', | |
| 607 '--timelimit', | |
| 608 dest='limit', | |
|
nyquist
2015/07/17 21:42:10
Nit: I personally find it easier to read later in
David Trainor- moved to gerrit
2015/07/21 18:19:56
Done.
| |
| 609 type=Validator.ValidatePositiveNumber, | |
| 610 help='How long to track memory in seconds.') | |
| 611 parser.add_argument('-f', | |
| 612 '--frequency', | |
| 613 dest='frequency', | |
| 614 default=0, | |
| 615 type=Validator.ValidatePositiveNumber, | |
| 616 help='How often to poll in seconds.') | |
| 617 parser.add_argument('-s', | |
| 618 '--diff-against-start', | |
| 619 dest='diff_against_start', | |
| 620 action='store_true', | |
| 621 help='Whether or not to always compare against the' | |
| 622 ' original memory values for deltas.') | |
| 623 parser.add_argument('-b', | |
| 624 '--boring-output', | |
| 625 dest='dull_output', | |
| 626 action='store_true', | |
| 627 help='Whether or not to dull down the output.') | |
| 628 parser.add_argument('-n', | |
| 629 '--no-overwrite', | |
| 630 dest='no_overwrite', | |
| 631 action='store_true', | |
| 632 help='Keeps printing the results in a list instead of' | |
| 633 ' overwriting the previous values.') | |
| 634 parser.add_argument('-g', | |
| 635 '--graph-file', | |
| 636 dest='graph_file', | |
| 637 type=Validator.ValidatePdfPath, | |
| 638 help='Pdf file to save graph of memory stats to.') | |
|
nyquist
2015/07/17 21:42:11
Nit: PDF
David Trainor- moved to gerrit
2015/07/21 18:19:57
Done.
| |
| 639 parser.add_argument('-o', | |
| 640 '--text-file', | |
| 641 dest='text_file', | |
| 642 type=Validator.ValidatePath, | |
| 643 help='File to save memory tracking stats to.') | |
| 644 | |
| 645 args = parser.parse_args() | |
| 646 | |
| 647 # Add a basic filter to make sure we search for something. | |
| 648 if not args.procname and not args.pid: | |
| 649 args.procname = 'chrome' | |
| 650 | |
| 651 curses.setupterm() | |
| 652 | |
| 653 printer = OutputBeautifier(not args.dull_output, not args.no_overwrite) | |
| 654 | |
| 655 sys.stdout.write("Running... Hold CTRL-C to stop (or specify timeout).\n") | |
| 656 try: | |
| 657 last_time = time.time() | |
| 658 | |
| 659 adb = None | |
| 660 old_snapshot = None | |
| 661 snapshots = [] | |
| 662 while not args.limit or Timer.GetTimestamp() < float(args.limit): | |
| 663 # Check if we need to track another device | |
| 664 device = DeviceHelper.GetDeviceToTrack(args.device) | |
| 665 if not device: | |
| 666 adb = None | |
| 667 elif not adb or device != str(adb): | |
|
nyquist
2015/07/17 21:42:10
I had to look up AdbWrapper.__str__. That's cute!
David Trainor- moved to gerrit
2015/07/21 18:19:56
Yeah I thought so too when I saw that! :)
| |
| 668 adb = adb_wrapper.AdbWrapper(device) | |
| 669 old_snapshot = None | |
|
nyquist
2015/07/17 21:42:11
Would we want to clear snapshots?
David Trainor- moved to gerrit
2015/07/21 18:19:57
O_o good point!
| |
| 670 try: | |
| 671 adb.Root() | |
| 672 except device_errors.AdbCommandFailedError: | |
| 673 sys.stderr.write('Unable to run adb as root.\n') | |
| 674 sys.exit(1) | |
| 675 | |
| 676 # Grab a snapshot if we have a device | |
| 677 snapshot = None | |
| 678 if adb: | |
| 679 pids = DeviceHelper.GetPidsToTrack(adb, args.pid, args.procname) | |
| 680 snapshot = MemorySnapshot(adb, pids) if pids else None | |
| 681 | |
| 682 if snapshot and snapshot.HasResults(): | |
| 683 snapshots.append(snapshot) | |
| 684 | |
| 685 printer.PrettyPrint(snapshot, old_snapshot) | |
| 686 | |
| 687 # Transfer state for the next iteration and sleep | |
| 688 delay = max(1, args.frequency) | |
| 689 if snapshot: | |
| 690 delay = max(0, args.frequency - (time.time() - last_time)) | |
| 691 time.sleep(delay) | |
| 692 | |
| 693 last_time = time.time() | |
| 694 if not old_snapshot or not args.diff_against_start: | |
| 695 old_snapshot = snapshot | |
| 696 except KeyboardInterrupt: | |
| 697 pass | |
| 698 | |
| 699 if args.graph_file: | |
| 700 printer.PrettyGraph(args.graph_file, snapshots) | |
| 701 | |
| 702 if args.text_file: | |
| 703 printer.PrettyFile(args.text_file, snapshots, args.diff_against_start) | |
| 704 | |
| 705 if __name__ == '__main__': | |
| 706 sys.exit(main(sys.argv)) | |
| 707 | |
| OLD | NEW |