Add OWNERS to content/browser/quota
[chromium-blink-merge.git] / tools / android / appstats.py
blobba53eacb6c593021b1ff9e0c635207ec11260899
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/network polling for Android apps.
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.device import device_errors
23 from pylib.device import device_utils
25 class Utils(object):
26 """A helper class to hold various utility methods."""
28 @staticmethod
29 def FindLines(haystack, needle):
30 """A helper method to find lines in |haystack| that contain the string
31 |needle|."""
32 return [ hay for hay in haystack if needle in hay ]
34 class Validator(object):
35 """A helper class with validation methods for argparse."""
37 @staticmethod
38 def ValidatePath(path):
39 """An argparse validation method to make sure a file path is writable."""
40 if os.path.exists(path):
41 return path
42 elif os.access(os.path.dirname(path), os.W_OK):
43 return path
44 raise argparse.ArgumentTypeError("%s is an invalid file path" % path)
46 @staticmethod
47 def ValidatePdfPath(path):
48 """An argparse validation method to make sure a pdf file path is writable.
49 Validates a file path to make sure it is writable and also appends '.pdf' if
50 necessary."""
51 if os.path.splitext(path)[-1].lower() != 'pdf':
52 path = path + '.pdf'
53 return Validator.ValidatePath(path)
55 @staticmethod
56 def ValidateNonNegativeNumber(val):
57 """An argparse validation method to make sure a number is not negative."""
58 ival = int(val)
59 if ival < 0:
60 raise argparse.ArgumentTypeError("%s is a negative integer" % val)
61 return ival
63 class Timer(object):
64 """A helper class to track timestamps based on when this program was
65 started"""
66 starting_time = time.time()
68 @staticmethod
69 def GetTimestamp():
70 """A helper method to return the time (in seconds) since this program was
71 started."""
72 return time.time() - Timer.starting_time
74 class DeviceHelper(object):
75 """A helper class with various generic device interaction methods."""
77 @staticmethod
78 def __GetUserIdForProcessName(adb, process_name):
79 """Returns the userId of the application associated by |pid| or None if
80 not found."""
81 try:
82 process_name = process_name.split(':')[0]
83 cmd = ['dumpsys', 'package', process_name]
84 user_id_lines = adb.RunShellCommand(' '.join(cmd), large_output=True)
85 user_id_lines = Utils.FindLines(user_id_lines, 'userId=')
87 if not user_id_lines:
88 return None
90 columns = re.split('\s+|=', user_id_lines[0].strip())
92 if len(columns) >= 2:
93 return columns[1]
94 except device_errors.AdbShellCommandFailedError:
95 pass
96 return None
98 @staticmethod
99 def GetDeviceModel(adb):
100 """Returns the model of the device with the |adb| connection."""
101 return adb.GetProp('ro.product.model').strip()
103 @staticmethod
104 def GetDeviceToTrack(preset=None):
105 """Returns a device serial to connect to. If |preset| is specified it will
106 return |preset| if it is connected and |None| otherwise. If |preset| is not
107 specified it will return the first connected device."""
108 devices = [d.adb.GetDeviceSerial()
109 for d in device_utils.DeviceUtils.HealthyDevices()]
110 if not devices:
111 return None
113 if preset:
114 return preset if preset in devices else None
116 return devices[0]
118 @staticmethod
119 def GetPidsToTrack(adb, default_pid=None, process_filter=None):
120 """Returns a list of tuples of (userid, pids, process name) based on the
121 input arguments. If |default_pid| is specified it will return that pid if
122 it exists. If |process_filter| is specified it will return the pids of
123 processes with that string in the name. If both are specified it will
124 intersect the two. The returned result is sorted based on userid."""
125 pids = []
126 try:
127 cmd = ['ps']
128 pid_lines = adb.RunShellCommand(' '.join(cmd), large_output=True)
129 if default_pid:
130 pid_lines = Utils.FindLines(pid_lines, str(default_pid))
131 if process_filter:
132 pid_lines = Utils.FindLines(pid_lines, process_filter)
133 for line in pid_lines:
134 data = re.split('\s+', line.strip())
135 pid = data[1]
136 name = data[-1]
138 # Confirm that the pid and name match. Using a regular grep isn't
139 # reliable when doing it on the whole 'ps' input line.
140 pid_matches = not default_pid or pid == str(default_pid)
141 name_matches = not process_filter or name.find(process_filter) != -1
142 if pid_matches and name_matches:
143 userid = DeviceHelper.__GetUserIdForProcessName(adb, name)
144 pids.append((userid, pid, name))
145 except device_errors.AdbShellCommandFailedError:
146 pass
147 return sorted(pids, key=lambda tup: tup[0])
149 class NetworkHelper(object):
150 """A helper class to query basic network usage of an application."""
151 @staticmethod
152 def QueryNetwork(adb, userid):
153 """Queries the device for network information about the application with a
154 user id of |userid|. It will return a list of values:
155 [ Download Background, Upload Background, Download Foreground, Upload
156 Foreground ]. If the application is not found it will return
157 [ 0, 0, 0, 0 ]."""
158 results = [0, 0, 0, 0]
160 if not userid:
161 return results
163 try:
164 # Parsing indices for scanning a row from /proc/net/xt_qtaguid/stats.
165 # The application id
166 userid_idx = 3
168 # Whether or not the transmission happened with the application in the
169 # background (0) or foreground (1).
170 bg_or_fg_idx = 4
172 # The number of bytes received.
173 rx_idx = 5
175 # The number of bytes sent.
176 tx_idx = 7
178 cmd = ['cat', '/proc/net/xt_qtaguid/stats']
179 net_lines = adb.RunShellCommand(' '.join(cmd), large_output=True)
180 net_lines = Utils.FindLines(net_lines, userid)
181 for line in net_lines:
182 data = re.split('\s+', line.strip())
183 if data[userid_idx] != userid:
184 continue
186 dst_idx_offset = None
187 if data[bg_or_fg_idx] == '0':
188 dst_idx_offset = 0
189 elif data[bg_or_fg_idx] == '1':
190 dst_idx_offset = 2
192 if dst_idx_offset is None:
193 continue
195 results[dst_idx_offset] = round(float(data[rx_idx]) / 1000.0, 2)
196 results[dst_idx_offset + 1] = round(float(data[tx_idx]) / 1000.0, 2)
197 except device_errors.AdbShellCommandFailedError:
198 pass
199 return results
201 class MemoryHelper(object):
202 """A helper class to query basic memory usage of a process."""
204 @staticmethod
205 def QueryMemory(adb, pid):
206 """Queries the device for memory information about the process with a pid of
207 |pid|. It will query Native, Dalvik, and Pss memory of the process. It
208 returns a list of values: [ Native, Pss, Dalvik ]. If the process is not
209 found it will return [ 0, 0, 0 ]."""
210 results = [0, 0, 0]
212 mem_lines = adb.RunShellCommand(' '.join(['dumpsys', 'meminfo', pid]))
213 for line in mem_lines:
214 match = re.split('\s+', line.strip())
216 # Skip data after the 'App Summary' line. This is to fix builds where
217 # they have more entries that might match the other conditions.
218 if len(match) >= 2 and match[0] == 'App' and match[1] == 'Summary':
219 break
221 result_idx = None
222 query_idx = None
223 if match[0] == 'Native' and match[1] == 'Heap':
224 result_idx = 0
225 query_idx = -2
226 elif match[0] == 'Dalvik' and match[1] == 'Heap':
227 result_idx = 2
228 query_idx = -2
229 elif match[0] == 'TOTAL':
230 result_idx = 1
231 query_idx = 1
233 # If we already have a result, skip it and don't overwrite the data.
234 if result_idx is not None and results[result_idx] != 0:
235 continue
237 if result_idx is not None and query_idx is not None:
238 results[result_idx] = round(float(match[query_idx]) / 1000.0, 2)
239 return results
241 class GraphicsHelper(object):
242 """A helper class to query basic graphics memory usage of a process."""
244 # TODO(dtrainor): Find a generic way to query/fall back for other devices.
245 # Is showmap consistently reliable?
246 __NV_MAP_MODELS = ['Xoom']
247 __NV_MAP_FILE_LOCATIONS = ['/d/nvmap/generic-0/clients',
248 '/d/nvmap/iovmm/clients']
250 __SHOWMAP_MODELS = ['Nexus S',
251 'Nexus S 4G',
252 'Galaxy Nexus',
253 'Nexus 4',
254 'Nexus 5',
255 'Nexus 7']
256 __SHOWMAP_KEY_MATCHES = ['/dev/pvrsrvkm',
257 '/dev/kgsl-3d0']
259 @staticmethod
260 def __QueryShowmap(adb, pid):
261 """Attempts to query graphics memory via the 'showmap' command. It will
262 look for |self.__SHOWMAP_KEY_MATCHES| entries to try to find one that
263 represents the graphics memory usage. Will return this as a single entry
264 array of [ Graphics ]. If not found, will return [ 0 ]."""
265 try:
266 mem_lines = adb.RunShellCommand(' '.join(['showmap', '-t', pid]))
267 for line in mem_lines:
268 match = re.split('[ ]+', line.strip())
269 if match[-1] in GraphicsHelper.__SHOWMAP_KEY_MATCHES:
270 return [ round(float(match[2]) / 1000.0, 2) ]
271 except device_errors.AdbShellCommandFailedError:
272 pass
273 return [ 0 ]
275 @staticmethod
276 def __NvMapPath(adb):
277 """Attempts to find a valid NV Map file on the device. It will look for a
278 file in |self.__NV_MAP_FILE_LOCATIONS| and see if one exists. If so, it
279 will return it."""
280 for nv_file in GraphicsHelper.__NV_MAP_FILE_LOCATIONS:
281 exists = adb.RunShellCommand(' '.join(['ls', nv_file]))
282 if exists[0] == nv_file.split('/')[-1]:
283 return nv_file
284 return None
286 @staticmethod
287 def __QueryNvMap(adb, pid):
288 """Attempts to query graphics memory via the NV file map method. It will
289 find a possible NV Map file from |self.__NvMapPath| and try to parse the
290 graphics memory from it. Will return this as a single entry array of
291 [ Graphics ]. If not found, will return [ 0 ]."""
292 nv_file = GraphicsHelper.__NvMapPath(adb)
293 if nv_file:
294 mem_lines = adb.RunShellCommand(' '.join(['cat', nv_file]))
295 for line in mem_lines:
296 match = re.split(' +', line.strip())
297 if match[2] == pid:
298 return [ round(float(match[3]) / 1000000.0, 2) ]
299 return [ 0 ]
301 @staticmethod
302 def QueryVideoMemory(adb, pid):
303 """Queries the device for graphics memory information about the process with
304 a pid of |pid|. Not all devices are currently supported. If possible, this
305 will return a single entry array of [ Graphics ]. Otherwise it will return
306 [ 0 ].
308 Please see |self.__NV_MAP_MODELS| and |self.__SHOWMAP_MODELS|
309 to see if the device is supported. For new devices, see if they can be
310 supported by existing methods and add their entry appropriately. Also,
311 please add any new way of querying graphics memory as they become
312 available."""
313 model = DeviceHelper.GetDeviceModel(adb)
314 if model in GraphicsHelper.__NV_MAP_MODELS:
315 return GraphicsHelper.__QueryNvMap(adb, pid)
316 elif model in GraphicsHelper.__SHOWMAP_MODELS:
317 return GraphicsHelper.__QueryShowmap(adb, pid)
318 return [ 0 ]
320 class DeviceSnapshot(object):
321 """A class holding a snapshot of memory and network usage for various pids
322 that are being tracked. If |show_mem| is True, this will track memory usage.
323 If |show_net| is True, this will track network usage.
325 Attributes:
326 pids: A list of tuples (userid, pid, process name) that should be
327 tracked.
328 memory: A map of entries of pid => memory consumption array. Right now
329 the indices are [ Native, Pss, Dalvik, Graphics ].
330 network: A map of entries of userid => network consumption array. Right
331 now the indices are [ Download Background, Upload Background,
332 Download Foreground, Upload Foreground ].
333 timestamp: The amount of time (in seconds) between when this program started
334 and this snapshot was taken.
337 def __init__(self, adb, pids, show_mem, show_net):
338 """Creates an instances of a DeviceSnapshot with an |adb| device connection
339 and a list of (pid, process name) tuples."""
340 super(DeviceSnapshot, self).__init__()
342 self.pids = pids
343 self.memory = {}
344 self.network = {}
345 self.timestamp = Timer.GetTimestamp()
347 for (userid, pid, name) in pids:
348 if show_mem:
349 self.memory[pid] = self.__QueryMemoryForPid(adb, pid)
351 if show_net and userid not in self.network:
352 self.network[userid] = NetworkHelper.QueryNetwork(adb, userid)
354 @staticmethod
355 def __QueryMemoryForPid(adb, pid):
356 """Queries the |adb| device for memory information about |pid|. This will
357 return a list of memory values that map to [ Native, Pss, Dalvik,
358 Graphics ]."""
359 results = MemoryHelper.QueryMemory(adb, pid)
360 results.extend(GraphicsHelper.QueryVideoMemory(adb, pid))
361 return results
363 def __GetProcessNames(self):
364 """Returns a list of all of the process names tracked by this snapshot."""
365 return [tuple[2] for tuple in self.pids]
367 def HasResults(self):
368 """Whether or not this snapshot was tracking any processes."""
369 return self.pids
371 def GetPidInfo(self):
372 """Returns a list of (userid, pid, process name) tuples that are being
373 tracked in this snapshot."""
374 return self.pids
376 def GetNameForPid(self, search_pid):
377 """Returns the process name of a tracked |search_pid|. This only works if
378 |search_pid| is tracked by this snapshot."""
379 for (userid, pid, name) in self.pids:
380 if pid == search_pid:
381 return name
382 return None
384 def GetUserIdForPid(self, search_pid):
385 """Returns the application userId for an associated |pid|. This only works
386 if |search_pid| is tracked by this snapshot and the application userId is
387 queryable."""
388 for (userid, pid, name) in self.pids:
389 if pid == search_pid:
390 return userid
391 return None
393 def IsFirstPidForUserId(self, search_pid):
394 """Returns whether or not |search_pid| is the first pid in the |pids| with
395 the associated application userId. This is used to determine if network
396 statistics should be shown for this pid or if they have already been shown
397 for a pid associated with this application."""
398 prev_userid = None
399 for idx, (userid, pid, name) in enumerate(self.pids):
400 if pid == search_pid:
401 return prev_userid != userid
402 prev_userid = userid
403 return False
405 def GetMemoryResults(self, pid):
406 """Returns a list of entries about the memory usage of the process specified
407 by |pid|. This will be of the format [ Native, Pss, Dalvik, Graphics ]."""
408 if pid in self.memory:
409 return self.memory[pid]
410 return None
412 def GetNetworkResults(self, userid):
413 """Returns a list of entries about the network usage of the application
414 specified by |userid|. This will be of the format [ Download Background,
415 Upload Background, Download Foreground, Upload Foreground ]."""
416 if userid in self.network:
417 return self.network[userid]
418 return None
420 def GetLongestNameLength(self):
421 """Returns the length of the longest process name tracked by this
422 snapshot."""
423 return len(max(self.__GetProcessNames(), key=len))
425 def GetTimestamp(self):
426 """Returns the time since program start that this snapshot was taken."""
427 return self.timestamp
429 class OutputBeautifier(object):
430 """A helper class to beautify the memory output to various destinations.
432 Attributes:
433 can_color: Whether or not the output should include ASCII color codes to
434 make it look nicer. Default is |True|. This is disabled when
435 writing to a file or a graph.
436 overwrite: Whether or not the output should overwrite the previous output.
437 Default is |True|. This is disabled when writing to a file or a
438 graph.
441 __MEMORY_COLUMN_TITLES = ['Native',
442 'Pss',
443 'Dalvik',
444 'Graphics']
446 __NETWORK_COLUMN_TITLES = ['Bg Rx',
447 'Bg Tx',
448 'Fg Rx',
449 'Fg Tx']
451 __TERMINAL_COLORS = {'ENDC': 0,
452 'BOLD': 1,
453 'GREY30': 90,
454 'RED': 91,
455 'DARK_YELLOW': 33,
456 'GREEN': 92}
458 def __init__(self, can_color=True, overwrite=True):
459 """Creates an instance of an OutputBeautifier."""
460 super(OutputBeautifier, self).__init__()
461 self.can_color = can_color
462 self.overwrite = overwrite
464 self.lines_printed = 0
465 self.printed_header = False
467 @staticmethod
468 def __FindPidsForSnapshotList(snapshots):
469 """Find the set of unique pids across all every snapshot in |snapshots|."""
470 pids = set()
471 for snapshot in snapshots:
472 for (userid, pid, name) in snapshot.GetPidInfo():
473 pids.add((userid, pid, name))
474 return pids
476 @staticmethod
477 def __TermCode(num):
478 """Escapes a terminal code. See |self.__TERMINAL_COLORS| for a list of some
479 terminal codes that are used by this program."""
480 return '\033[%sm' % num
482 @staticmethod
483 def __PadString(string, length, left_align):
484 """Pads |string| to at least |length| with spaces. Depending on
485 |left_align| the padding will appear at either the left or the right of the
486 original string."""
487 return (('%' if left_align else '%-') + str(length) + 's') % string
489 @staticmethod
490 def __GetDiffColor(delta):
491 """Returns a color based on |delta|. Used to color the deltas between
492 different snapshots."""
493 if not delta or delta == 0.0:
494 return 'GREY30'
495 elif delta < 0:
496 return 'GREEN'
497 elif delta > 0:
498 return 'RED'
500 @staticmethod
501 def __CleanRound(val, precision):
502 """Round |val| to |precision|. If |precision| is 0, completely remove the
503 decimal point."""
504 return int(val) if precision == 0 else round(float(val), precision)
506 def __ColorString(self, string, color):
507 """Colors |string| based on |color|. |color| must be in
508 |self.__TERMINAL_COLORS|. Returns the colored string or the original
509 string if |self.can_color| is |False| or the |color| is invalid."""
510 if not self.can_color or not color or not self.__TERMINAL_COLORS[color]:
511 return string
513 return '%s%s%s' % (
514 self.__TermCode(self.__TERMINAL_COLORS[color]),
515 string,
516 self.__TermCode(self.__TERMINAL_COLORS['ENDC']))
518 def __PadAndColor(self, string, length, left_align, color):
519 """A helper method to both pad and color the string. See
520 |self.__ColorString| and |self.__PadString|."""
521 return self.__ColorString(
522 self.__PadString(string, length, left_align), color)
524 def __OutputLine(self, line):
525 """Writes a line to the screen. This also tracks how many times this method
526 was called so that the screen can be cleared properly if |self.overwrite| is
527 |True|."""
528 sys.stdout.write(line + '\n')
529 if self.overwrite:
530 self.lines_printed += 1
532 def __ClearScreen(self):
533 """Clears the screen based on the number of times |self.__OutputLine| was
534 called."""
535 if self.lines_printed == 0 or not self.overwrite:
536 return
538 key_term_up = curses.tparm(curses.tigetstr('cuu1'))
539 key_term_clear_eol = curses.tparm(curses.tigetstr('el'))
540 key_term_go_to_bol = curses.tparm(curses.tigetstr('cr'))
542 sys.stdout.write(key_term_go_to_bol)
543 sys.stdout.write(key_term_clear_eol)
545 for i in range(self.lines_printed):
546 sys.stdout.write(key_term_up)
547 sys.stdout.write(key_term_clear_eol)
548 self.lines_printed = 0
550 def __PrintPidLabelHeader(self, snapshot):
551 """Returns a header string with columns Pid and Name."""
552 if not snapshot or not snapshot.HasResults():
553 return
555 name_length = max(8, snapshot.GetLongestNameLength())
557 header = self.__PadString('Pid', 8, True) + ' '
558 header += self.__PadString('Name', name_length, False)
559 header = self.__ColorString(header, 'BOLD')
560 return header
562 def __PrintTimestampHeader(self):
563 """Returns a header string with a Timestamp column."""
564 header = self.__PadString('Timestamp', 8, False)
565 header = self.__ColorString(header, 'BOLD')
566 return header
568 def __PrintMemoryStatsHeader(self):
569 """Returns a header string for memory usage statistics."""
570 headers = ''
571 for header in self.__MEMORY_COLUMN_TITLES:
572 headers += self.__PadString(header, 8, True) + ' '
573 headers += self.__PadString('(mB)', 8, False)
574 return self.__ColorString(headers, 'BOLD')
576 def __PrintNetworkStatsHeader(self):
577 """Returns a header string for network usage statistics."""
578 headers = ''
579 for header in self.__NETWORK_COLUMN_TITLES:
580 headers += self.__PadString(header, 8, True) + ' '
581 headers += self.__PadString('(kB)', 8, False)
582 return self.__ColorString(headers, 'BOLD')
584 def __PrintTrailingHeader(self, snapshot):
585 """Returns a header string for the header trailer (includes timestamp)."""
586 if not snapshot or not snapshot.HasResults():
587 return
589 header = '(' + str(round(snapshot.GetTimestamp(), 2)) + 's)'
590 return self.__ColorString(header, 'BOLD')
592 def __PrintArrayWithDeltas(self, results, old_results, precision=2):
593 """Helper method to return a string of statistics with their deltas. This
594 takes two arrays and prints out "current (current - old)" for all entries in
595 the arrays."""
596 if not results:
597 return
598 deltas = [0] * len(results)
599 if old_results:
600 assert len(old_results) == len(results)
601 deltas = map(sub, results, old_results)
602 output = ''
603 for idx, val in enumerate(results):
604 round_val = self.__CleanRound(val, precision)
605 round_delta = self.__CleanRound(deltas[idx], precision)
606 output += self.__PadString(str(round_val), 8, True) + ' '
607 output += self.__PadAndColor('(' + str(round_delta) + ')', 8, False,
608 self.__GetDiffColor(deltas[idx]))
610 return output
612 def __PrintPidLabelStats(self, pid, snapshot):
613 """Returns a string that includes the columns pid and process name for
614 the specified |pid|. This lines up with the associated header."""
615 if not snapshot or not snapshot.HasResults():
616 return
618 name_length = max(8, snapshot.GetLongestNameLength())
619 name = snapshot.GetNameForPid(pid)
621 output = self.__PadAndColor(pid, 8, True, 'DARK_YELLOW') + ' '
622 output += self.__PadAndColor(name, name_length, False, None)
623 return output
625 def __PrintTimestampStats(self, snapshot):
626 """Returns a string that includes the timestamp of the |snapshot|. This
627 lines up with the associated header."""
628 if not snapshot or not snapshot.HasResults():
629 return
631 timestamp_length = max(8, len("Timestamp"))
632 timestamp = round(snapshot.GetTimestamp(), 2)
634 output = self.__PadString(str(timestamp), timestamp_length, True)
635 return output
637 def __PrintMemoryStats(self, pid, snapshot, prev_snapshot):
638 """Returns a string that includes memory statistics of the |snapshot|. This
639 lines up with the associated header."""
640 if not snapshot or not snapshot.HasResults():
641 return
643 results = snapshot.GetMemoryResults(pid)
644 if not results:
645 return
647 old_results = prev_snapshot.GetMemoryResults(pid) if prev_snapshot else None
648 return self.__PrintArrayWithDeltas(results, old_results, 2)
650 def __PrintNetworkStats(self, userid, snapshot, prev_snapshot):
651 """Returns a string that includes network statistics of the |snapshot|. This
652 lines up with the associated header."""
653 if not snapshot or not snapshot.HasResults():
654 return
656 results = snapshot.GetNetworkResults(userid)
657 if not results:
658 return
660 old_results = None
661 if prev_snapshot:
662 old_results = prev_snapshot.GetNetworkResults(userid)
663 return self.__PrintArrayWithDeltas(results, old_results, 0)
665 def __PrintNulledNetworkStats(self):
666 """Returns a string that includes empty network statistics. This lines up
667 with the associated header. This is used when showing statistics for pids
668 that share the same application userId. Network statistics should only be
669 shown once for each application userId."""
670 stats = ''
671 for title in self.__NETWORK_COLUMN_TITLES:
672 stats += self.__PadString('-', 8, True) + ' '
673 stats += self.__PadString('', 8, True)
674 return stats
676 def __PrintHeaderHelper(self,
677 snapshot,
678 show_labels,
679 show_timestamp,
680 show_mem,
681 show_net,
682 show_trailer):
683 """Helper method to concat various header entries together into one header.
684 This will line up with a entry built by __PrintStatsHelper if the same
685 values are passed to it."""
686 titles = []
687 if show_labels:
688 titles.append(self.__PrintPidLabelHeader(snapshot))
690 if show_timestamp:
691 titles.append(self.__PrintTimestampHeader())
693 if show_mem:
694 titles.append(self.__PrintMemoryStatsHeader())
696 if show_net:
697 titles.append(self.__PrintNetworkStatsHeader())
699 if show_trailer:
700 titles.append(self.__PrintTrailingHeader(snapshot))
702 return ' '.join(titles)
704 def __PrintStatsHelper(self,
705 pid,
706 snapshot,
707 prev_snapshot,
708 show_labels,
709 show_timestamp,
710 show_mem,
711 show_net):
712 """Helper method to concat various stats entries together into one line.
713 This will line up with a header built by __PrintHeaderHelper if the same
714 values are passed to it."""
715 stats = []
716 if show_labels:
717 stats.append(self.__PrintPidLabelStats(pid, snapshot))
719 if show_timestamp:
720 stats.append(self.__PrintTimestampStats(snapshot))
722 if show_mem:
723 stats.append(self.__PrintMemoryStats(pid, snapshot, prev_snapshot))
725 if show_net:
726 userid = snapshot.GetUserIdForPid(pid)
727 show_userid = snapshot.IsFirstPidForUserId(pid)
728 if userid and show_userid:
729 stats.append(self.__PrintNetworkStats(userid, snapshot, prev_snapshot))
730 else:
731 stats.append(self.__PrintNulledNetworkStats())
733 return ' '.join(stats)
735 def PrettyPrint(self, snapshot, prev_snapshot, show_mem=True, show_net=True):
736 """Prints |snapshot| to the console. This will show memory and/or network
737 deltas between |snapshot| and |prev_snapshot|. This will also either color
738 or overwrite the previous entries based on |self.can_color| and
739 |self.overwrite|. If |show_mem| is True, this will attempt to show memory
740 statistics. If |show_net| is True, this will attempt to show network
741 statistics."""
742 self.__ClearScreen()
744 if not snapshot or not snapshot.HasResults():
745 self.__OutputLine("No results...")
746 return
748 # Output Format
749 show_label = True
750 show_timestamp = False
751 show_trailer = True
753 self.__OutputLine(self.__PrintHeaderHelper(snapshot,
754 show_label,
755 show_timestamp,
756 show_mem,
757 show_net,
758 show_trailer))
760 for (userid, pid, name) in snapshot.GetPidInfo():
761 self.__OutputLine(self.__PrintStatsHelper(pid,
762 snapshot,
763 prev_snapshot,
764 show_label,
765 show_timestamp,
766 show_mem,
767 show_net))
769 def PrettyFile(self,
770 file_path,
771 snapshots,
772 diff_against_start,
773 show_mem=True,
774 show_net=True):
775 """Writes |snapshots| (a list of DeviceSnapshots) to |file_path|.
776 |diff_against_start| determines whether or not the snapshot deltas are
777 between the first entry and all entries or each previous entry. This output
778 will not follow |self.can_color| or |self.overwrite|. If |show_mem| is
779 True, this will attempt to show memory statistics. If |show_net| is True,
780 this will attempt to show network statistics."""
781 if not file_path or not snapshots:
782 return
784 # Output Format
785 show_label = False
786 show_timestamp = True
787 show_trailer = False
789 pids = self.__FindPidsForSnapshotList(snapshots)
791 # Disable special output formatting for file writing.
792 can_color = self.can_color
793 self.can_color = False
795 with open(file_path, 'w') as out:
796 for (userid, pid, name) in pids:
797 out.write(name + ' (' + str(pid) + '):\n')
798 out.write(self.__PrintHeaderHelper(None,
799 show_label,
800 show_timestamp,
801 show_mem,
802 show_net,
803 show_trailer))
804 out.write('\n')
806 prev_snapshot = None
807 for snapshot in snapshots:
808 has_mem = show_mem and snapshot.GetMemoryResults(pid) is not None
809 has_net = show_net and snapshot.GetNetworkResults(userid) is not None
810 if not has_mem and not has_net:
811 continue
812 out.write(self.__PrintStatsHelper(pid,
813 snapshot,
814 prev_snapshot,
815 show_label,
816 show_timestamp,
817 show_mem,
818 show_net))
819 out.write('\n')
820 if not prev_snapshot or not diff_against_start:
821 prev_snapshot = snapshot
822 out.write('\n\n')
824 # Restore special output formatting.
825 self.can_color = can_color
827 def PrettyGraph(self, file_path, snapshots):
828 """Creates a pdf graph of |snapshots| (a list of DeviceSnapshots) at
829 |file_path|. This currently only shows memory stats and no network
830 stats."""
831 # Import these here so the rest of the functionality doesn't rely on
832 # matplotlib
833 from matplotlib import pyplot
834 from matplotlib.backends.backend_pdf import PdfPages
836 if not file_path or not snapshots:
837 return
839 pids = self.__FindPidsForSnapshotList(snapshots)
841 pp = PdfPages(file_path)
842 for (userid, pid, name) in pids:
843 figure = pyplot.figure()
844 ax = figure.add_subplot(1, 1, 1)
845 ax.set_xlabel('Time (s)')
846 ax.set_ylabel('MB')
847 ax.set_title(name + ' (' + pid + ')')
849 mem_list = [[] for x in range(len(self.__MEMORY_COLUMN_TITLES))]
850 timestamps = []
852 for snapshot in snapshots:
853 results = snapshot.GetMemoryResults(pid)
854 if not results:
855 continue
857 timestamps.append(round(snapshot.GetTimestamp(), 2))
859 assert len(results) == len(self.__MEMORY_COLUMN_TITLES)
860 for idx, result in enumerate(results):
861 mem_list[idx].append(result)
863 colors = []
864 for data in mem_list:
865 colors.append(ax.plot(timestamps, data)[0])
866 for i in xrange(len(timestamps)):
867 ax.annotate(data[i], xy=(timestamps[i], data[i]))
868 figure.legend(colors, self.__MEMORY_COLUMN_TITLES)
869 pp.savefig()
870 pp.close()
872 def main(argv):
873 parser = argparse.ArgumentParser()
874 parser.add_argument('--process',
875 dest='procname',
876 help="A (sub)string to match against process names.")
877 parser.add_argument('-p',
878 '--pid',
879 dest='pid',
880 type=Validator.ValidateNonNegativeNumber,
881 help='Which pid to scan for.')
882 parser.add_argument('-d',
883 '--device',
884 dest='device',
885 help='Device serial to scan.')
886 parser.add_argument('-t',
887 '--timelimit',
888 dest='timelimit',
889 type=Validator.ValidateNonNegativeNumber,
890 help='How long to track memory in seconds.')
891 parser.add_argument('-f',
892 '--frequency',
893 dest='frequency',
894 default=0,
895 type=Validator.ValidateNonNegativeNumber,
896 help='How often to poll in seconds.')
897 parser.add_argument('-s',
898 '--diff-against-start',
899 dest='diff_against_start',
900 action='store_true',
901 help='Whether or not to always compare against the'
902 ' original memory values for deltas.')
903 parser.add_argument('-b',
904 '--boring-output',
905 dest='dull_output',
906 action='store_true',
907 help='Whether or not to dull down the output.')
908 parser.add_argument('-k',
909 '--keep-results',
910 dest='no_overwrite',
911 action='store_true',
912 help='Keeps printing the results in a list instead of'
913 ' overwriting the previous values.')
914 parser.add_argument('-g',
915 '--graph-file',
916 dest='graph_file',
917 type=Validator.ValidatePdfPath,
918 help='PDF file to save graph of memory stats to.')
919 parser.add_argument('-o',
920 '--text-file',
921 dest='text_file',
922 type=Validator.ValidatePath,
923 help='File to save memory tracking stats to.')
924 parser.add_argument('-m',
925 '--memory',
926 dest='show_mem',
927 action='store_true',
928 help='Whether or not to show memory stats. True by'
929 ' default unless --n is specified.')
930 parser.add_argument('-n',
931 '--net',
932 dest='show_net',
933 action='store_true',
934 help='Whether or not to show network stats. False by'
935 ' default.')
937 args = parser.parse_args()
939 # Add a basic filter to make sure we search for something.
940 if not args.procname and not args.pid:
941 args.procname = 'chrome'
943 # Make sure we show memory stats if nothing was specifically requested.
944 if not args.show_net and not args.show_mem:
945 args.show_mem = True
947 curses.setupterm()
949 printer = OutputBeautifier(not args.dull_output, not args.no_overwrite)
951 sys.stdout.write("Running... Hold CTRL-C to stop (or specify timeout).\n")
952 try:
953 last_time = time.time()
955 adb = None
956 old_snapshot = None
957 snapshots = []
958 while not args.timelimit or Timer.GetTimestamp() < float(args.timelimit):
959 # Check if we need to track another device
960 device = DeviceHelper.GetDeviceToTrack(args.device)
961 if not device:
962 adb = None
963 elif not adb or device != str(adb):
964 #adb = adb_wrapper.AdbWrapper(device)
965 adb = device_utils.DeviceUtils(device)
966 old_snapshot = None
967 snapshots = []
968 try:
969 adb.EnableRoot()
970 except device_errors.CommandFailedError:
971 sys.stderr.write('Unable to run adb as root.\n')
972 sys.exit(1)
974 # Grab a snapshot if we have a device
975 snapshot = None
976 if adb:
977 pids = DeviceHelper.GetPidsToTrack(adb, args.pid, args.procname)
978 snapshot = None
979 if pids:
980 snapshot = DeviceSnapshot(adb, pids, args.show_mem, args.show_net)
982 if snapshot and snapshot.HasResults():
983 snapshots.append(snapshot)
985 printer.PrettyPrint(snapshot, old_snapshot, args.show_mem, args.show_net)
987 # Transfer state for the next iteration and sleep
988 delay = max(1, args.frequency)
989 if snapshot:
990 delay = max(0, args.frequency - (time.time() - last_time))
991 time.sleep(delay)
993 last_time = time.time()
994 if not old_snapshot or not args.diff_against_start:
995 old_snapshot = snapshot
996 except KeyboardInterrupt:
997 pass
999 if args.graph_file:
1000 printer.PrettyGraph(args.graph_file, snapshots)
1002 if args.text_file:
1003 printer.PrettyFile(args.text_file,
1004 snapshots,
1005 args.diff_against_start,
1006 args.show_mem,
1007 args.show_net)
1009 if __name__ == '__main__':
1010 sys.exit(main(sys.argv))