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