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.
15 from operator
import sub
17 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
),
22 from pylib
.device
import device_errors
23 from pylib
.device
import device_utils
26 """A helper class to hold various utility methods."""
29 def FindLines(haystack
, needle
):
30 """A helper method to find lines in |haystack| that contain the string
32 return [ hay
for hay
in haystack
if needle
in hay
]
34 class Validator(object):
35 """A helper class with validation methods for argparse."""
38 def ValidatePath(path
):
39 """An argparse validation method to make sure a file path is writable."""
40 if os
.path
.exists(path
):
42 elif os
.access(os
.path
.dirname(path
), os
.W_OK
):
44 raise argparse
.ArgumentTypeError("%s is an invalid file path" % path
)
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
51 if os
.path
.splitext(path
)[-1].lower() != 'pdf':
53 return Validator
.ValidatePath(path
)
56 def ValidateNonNegativeNumber(val
):
57 """An argparse validation method to make sure a number is not negative."""
60 raise argparse
.ArgumentTypeError("%s is a negative integer" % val
)
64 """A helper class to track timestamps based on when this program was
66 starting_time
= time
.time()
70 """A helper method to return the time (in seconds) since this program was
72 return time
.time() - Timer
.starting_time
74 class DeviceHelper(object):
75 """A helper class with various generic device interaction methods."""
78 def __GetUserIdForProcessName(adb
, process_name
):
79 """Returns the userId of the application associated by |pid| or None if
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=')
90 columns
= re
.split('\s+|=', user_id_lines
[0].strip())
94 except device_errors
.AdbShellCommandFailedError
:
99 def GetDeviceModel(adb
):
100 """Returns the model of the device with the |adb| connection."""
101 return adb
.GetProp('ro.product.model').strip()
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()]
114 return preset
if preset
in devices
else None
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."""
128 pid_lines
= adb
.RunShellCommand(' '.join(cmd
), large_output
=True)
130 pid_lines
= Utils
.FindLines(pid_lines
, str(default_pid
))
132 pid_lines
= Utils
.FindLines(pid_lines
, process_filter
)
133 for line
in pid_lines
:
134 data
= re
.split('\s+', line
.strip())
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
:
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."""
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
158 results
= [0, 0, 0, 0]
164 # Parsing indices for scanning a row from /proc/net/xt_qtaguid/stats.
168 # Whether or not the transmission happened with the application in the
169 # background (0) or foreground (1).
172 # The number of bytes received.
175 # The number of bytes sent.
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
:
186 dst_idx_offset
= None
187 if data
[bg_or_fg_idx
] == '0':
189 elif data
[bg_or_fg_idx
] == '1':
192 if dst_idx_offset
is None:
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
:
201 class MemoryHelper(object):
202 """A helper class to query basic memory usage of a process."""
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 ]."""
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':
223 if match
[0] == 'Native' and match
[1] == 'Heap':
226 elif match
[0] == 'Dalvik' and match
[1] == 'Heap':
229 elif match
[0] == 'TOTAL':
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:
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)
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',
256 __SHOWMAP_KEY_MATCHES
= ['/dev/pvrsrvkm',
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 ]."""
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
:
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
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]:
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
)
294 mem_lines
= adb
.RunShellCommand(' '.join(['cat', nv_file
]))
295 for line
in mem_lines
:
296 match
= re
.split(' +', line
.strip())
298 return [ round(float(match
[3]) / 1000000.0, 2) ]
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
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
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
)
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.
326 pids: A list of tuples (userid, pid, process name) that should be
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
__()
345 self
.timestamp
= Timer
.GetTimestamp()
347 for (userid
, pid
, name
) in pids
:
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
)
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,
359 results
= MemoryHelper
.QueryMemory(adb
, pid
)
360 results
.extend(GraphicsHelper
.QueryVideoMemory(adb
, pid
))
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."""
371 def GetPidInfo(self
):
372 """Returns a list of (userid, pid, process name) tuples that are being
373 tracked in this snapshot."""
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
:
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
388 for (userid
, pid
, name
) in self
.pids
:
389 if pid
== search_pid
:
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."""
399 for idx
, (userid
, pid
, name
) in enumerate(self
.pids
):
400 if pid
== search_pid
:
401 return prev_userid
!= userid
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
]
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
]
420 def GetLongestNameLength(self
):
421 """Returns the length of the longest process name tracked by this
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.
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
441 __MEMORY_COLUMN_TITLES
= ['Native',
446 __NETWORK_COLUMN_TITLES
= ['Bg Rx',
451 __TERMINAL_COLORS
= {'ENDC': 0,
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
468 def __FindPidsForSnapshotList(snapshots
):
469 """Find the set of unique pids across all every snapshot in |snapshots|."""
471 for snapshot
in snapshots
:
472 for (userid
, pid
, name
) in snapshot
.GetPidInfo():
473 pids
.add((userid
, pid
, name
))
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
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
487 return (('%' if left_align
else '%-') + str(length
) + 's') % string
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:
501 def __CleanRound(val
, precision
):
502 """Round |val| to |precision|. If |precision| is 0, completely remove the
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
]:
514 self
.__TermCode
(self
.__TERMINAL
_COLORS
[color
]),
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
528 sys
.stdout
.write(line
+ '\n')
530 self
.lines_printed
+= 1
532 def __ClearScreen(self
):
533 """Clears the screen based on the number of times |self.__OutputLine| was
535 if self
.lines_printed
== 0 or not self
.overwrite
:
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():
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')
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')
568 def __PrintMemoryStatsHeader(self
):
569 """Returns a header string for memory usage statistics."""
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."""
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():
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
598 deltas
= [0] * len(results
)
600 assert len(old_results
) == len(results
)
601 deltas
= map(sub
, results
, old_results
)
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
]))
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():
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)
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():
631 timestamp_length
= max(8, len("Timestamp"))
632 timestamp
= round(snapshot
.GetTimestamp(), 2)
634 output
= self
.__PadString
(str(timestamp
), timestamp_length
, True)
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():
643 results
= snapshot
.GetMemoryResults(pid
)
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():
656 results
= snapshot
.GetNetworkResults(userid
)
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."""
671 for title
in self
.__NETWORK
_COLUMN
_TITLES
:
672 stats
+= self
.__PadString
('-', 8, True) + ' '
673 stats
+= self
.__PadString
('', 8, True)
676 def __PrintHeaderHelper(self
,
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."""
688 titles
.append(self
.__PrintPidLabelHeader
(snapshot
))
691 titles
.append(self
.__PrintTimestampHeader
())
694 titles
.append(self
.__PrintMemoryStatsHeader
())
697 titles
.append(self
.__PrintNetworkStatsHeader
())
700 titles
.append(self
.__PrintTrailingHeader
(snapshot
))
702 return ' '.join(titles
)
704 def __PrintStatsHelper(self
,
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."""
717 stats
.append(self
.__PrintPidLabelStats
(pid
, snapshot
))
720 stats
.append(self
.__PrintTimestampStats
(snapshot
))
723 stats
.append(self
.__PrintMemoryStats
(pid
, snapshot
, prev_snapshot
))
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
))
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
744 if not snapshot
or not snapshot
.HasResults():
745 self
.__OutputLine
("No results...")
750 show_timestamp
= False
753 self
.__OutputLine
(self
.__PrintHeaderHelper
(snapshot
,
760 for (userid
, pid
, name
) in snapshot
.GetPidInfo():
761 self
.__OutputLine
(self
.__PrintStatsHelper
(pid
,
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
:
786 show_timestamp
= True
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,
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
:
812 out
.write(self
.__PrintStatsHelper
(pid
,
820 if not prev_snapshot
or not diff_against_start
:
821 prev_snapshot
= snapshot
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
831 # Import these here so the rest of the functionality doesn't rely on
833 from matplotlib
import pyplot
834 from matplotlib
.backends
.backend_pdf
import PdfPages
836 if not file_path
or not snapshots
:
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)')
847 ax
.set_title(name
+ ' (' + pid
+ ')')
849 mem_list
= [[] for x
in range(len(self
.__MEMORY
_COLUMN
_TITLES
))]
852 for snapshot
in snapshots
:
853 results
= snapshot
.GetMemoryResults(pid
)
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
)
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
)
873 parser
= argparse
.ArgumentParser()
874 parser
.add_argument('--process',
876 help="A (sub)string to match against process names.")
877 parser
.add_argument('-p',
880 type=Validator
.ValidateNonNegativeNumber
,
881 help='Which pid to scan for.')
882 parser
.add_argument('-d',
885 help='Device serial to scan.')
886 parser
.add_argument('-t',
889 type=Validator
.ValidateNonNegativeNumber
,
890 help='How long to track memory in seconds.')
891 parser
.add_argument('-f',
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',
901 help='Whether or not to always compare against the'
902 ' original memory values for deltas.')
903 parser
.add_argument('-b',
907 help='Whether or not to dull down the output.')
908 parser
.add_argument('-k',
912 help='Keeps printing the results in a list instead of'
913 ' overwriting the previous values.')
914 parser
.add_argument('-g',
917 type=Validator
.ValidatePdfPath
,
918 help='PDF file to save graph of memory stats to.')
919 parser
.add_argument('-o',
922 type=Validator
.ValidatePath
,
923 help='File to save memory tracking stats to.')
924 parser
.add_argument('-m',
928 help='Whether or not to show memory stats. True by'
929 ' default unless --n is specified.')
930 parser
.add_argument('-n',
934 help='Whether or not to show network stats. False by'
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
:
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")
953 last_time
= time
.time()
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
)
963 elif not adb
or device
!= str(adb
):
964 #adb = adb_wrapper.AdbWrapper(device)
965 adb
= device_utils
.DeviceUtils(device
)
970 except device_errors
.CommandFailedError
:
971 sys
.stderr
.write('Unable to run adb as root.\n')
974 # Grab a snapshot if we have a device
977 pids
= DeviceHelper
.GetPidsToTrack(adb
, args
.pid
, args
.procname
)
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
)
990 delay
= max(0, args
.frequency
- (time
.time() - last_time
))
993 last_time
= time
.time()
994 if not old_snapshot
or not args
.diff_against_start
:
995 old_snapshot
= snapshot
996 except KeyboardInterrupt:
1000 printer
.PrettyGraph(args
.graph_file
, snapshots
)
1003 printer
.PrettyFile(args
.text_file
,
1005 args
.diff_against_start
,
1009 if __name__
== '__main__':
1010 sys
.exit(main(sys
.argv
))