Rebaseline global-interface-listing-expected.txt
[chromium-blink-merge.git] / tools / android / meminfo.py
blob4958d5b450ab0f15a9f553e64a3c52b7bf19059e
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.
6 # 'top'-like memory polling for Chrome on Android
8 import argparse
9 import curses
10 import os
11 import re
12 import sys
13 import time
15 from operator import sub
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
26 class Validator(object):
27 """A helper class with validation methods for argparse."""
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)
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)
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
55 class Timer(object):
56 """A helper class to track timestamps based on when this program was
57 started"""
58 starting_time = time.time()
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
66 class DeviceHelper(object):
67 """A helper class with various generic device interaction methods."""
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()
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
83 if preset:
84 return preset if preset in devices else None
86 return devices[0]
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]
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
117 class MemoryHelper(object):
118 """A helper class to query basic memory usage of a process."""
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]
128 memstr = adb.Shell(' '.join(['dumpsys', 'meminfo', pid]))
129 for line in memstr.splitlines():
130 match = re.split('\s+', line.strip())
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
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
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
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
157 class GraphicsHelper(object):
158 """A helper class to query basic graphics memory usage of a process."""
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']
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']
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 ]
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
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 ]
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 ].
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 ]
236 class MemorySnapshot(object):
237 """A class holding a snapshot of memory for various pids that are being
238 tracked.
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.
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__()
253 self.pids = pids
254 self.memory = {}
255 self.timestamp = Timer.GetTimestamp()
257 for (pid, name) in pids:
258 self.memory[pid] = self.__QueryMemoryForPid(adb, pid)
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
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]
273 def HasResults(self):
274 """Whether or not this snapshot was tracking any processes."""
275 return self.pids
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
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
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
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))
302 def GetTimestamp(self):
303 """Returns the time since program start that this snapshot was taken."""
304 return self.timestamp
306 class OutputBeautifier(object):
307 """A helper class to beautify the memory output to various destinations.
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.
318 __MEMORY_COLUMN_TITLES = ['Native',
319 'Pss',
320 'Dalvik',
321 'Graphics']
323 __TERMINAL_COLORS = {'ENDC': 0,
324 'BOLD': 1,
325 'GREY30': 90,
326 'RED': 91,
327 'DARK_YELLOW': 33,
328 'GREEN': 92}
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
336 self.lines_printed = 0
337 self.printed_header = False
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
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
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
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'
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
379 return '%s%s%s' % (
380 self.__TermCode(self.__TERMINAL_COLORS[color]),
381 string,
382 self.__TermCode(self.__TERMINAL_COLORS['ENDC']))
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)
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
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
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'))
408 sys.stdout.write(key_term_go_to_bol)
409 sys.stdout.write(key_term_clear_eol)
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
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')
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
432 name_length = max(8, snapshot.GetLongestNameLength())
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
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
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
456 results = snapshot.GetResults(pid)
457 if not results:
458 return
460 old_results = prev_snapshot.GetResults(pid) if prev_snapshot else None
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)
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]))
474 return output
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
483 name_length = max(8, snapshot.GetLongestNameLength())
484 name = snapshot.GetNameForPid(pid)
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
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
498 timestamp_length = max(8, len("Timestamp"))
499 timestamp = round(snapshot.GetTimestamp(), 2)
501 output = self.__PadString(str(timestamp), timestamp_length, True) + ' '
502 output += self.__PrintBasicSnapshotStats(pid, snapshot, prev_snapshot)
503 return output
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()
511 if not snapshot or not snapshot.HasResults():
512 self.__OutputLine("No results...")
513 return
515 self.__OutputLine(self.__PrintLabeledStatsHeader(snapshot))
517 for (pid, name) in snapshot.GetPidAndNames():
518 self.__OutputLine(self.__PrintLabeledSnapshotStats(pid,
519 snapshot,
520 prev_snapshot))
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
530 pids = self.__FindPidsForSnapshotList(snapshots)
532 # Disable special output formatting for file writing.
533 can_color = self.can_color
534 self.can_color = False
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')
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')
554 # Restore special output formatting.
555 self.can_color = can_color
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
565 if not file_path or not snapshots:
566 return
568 pids = self.__FindPidsForSnapshotList(snapshots)
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 + ')')
578 mem_list = [[] for x in range(len(self.__MEMORY_COLUMN_TITLES))]
579 timestamps = []
581 for snapshot in snapshots:
582 results = snapshot.GetResults(pid)
583 if not results:
584 continue
586 timestamps.append(round(snapshot.GetTimestamp(), 2))
588 assert len(results) == len(self.__MEMORY_COLUMN_TITLES)
589 for idx, result in enumerate(results):
590 mem_list[idx].append(result)
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()
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.')
654 args = parser.parse_args()
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'
660 curses.setupterm()
662 printer = OutputBeautifier(not args.dull_output, not args.no_overwrite)
664 sys.stdout.write("Running... Hold CTRL-C to stop (or specify timeout).\n")
665 try:
666 last_time = time.time()
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)
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
692 if snapshot and snapshot.HasResults():
693 snapshots.append(snapshot)
695 printer.PrettyPrint(snapshot, old_snapshot)
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)
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
709 if args.graph_file:
710 printer.PrettyGraph(args.graph_file, snapshots)
712 if args.text_file:
713 printer.PrettyFile(args.text_file, snapshots, args.diff_against_start)
715 if __name__ == '__main__':
716 sys.exit(main(sys.argv))