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
15 from operator
import sub
17 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
),
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."""
30 def ValidatePath(path
):
31 """An argparse validation method to make sure a file path is writable."""
32 if os
.path
.exists(path
):
34 elif os
.access(os
.path
.dirname(path
), os
.W_OK
):
36 raise argparse
.ArgumentTypeError("%s is an invalid file path" % path
)
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
43 if os
.path
.splitext(path
)[-1].lower() != 'pdf':
45 return Validator
.ValidatePath(path
)
48 def ValidateNonNegativeNumber(val
):
49 """An argparse validation method to make sure a number is not negative."""
52 raise argparse
.ArgumentTypeError("%s is a negative integer" % val
)
56 """A helper class to track timestamps based on when this program was
58 starting_time
= time
.time()
62 """A helper method to return the time (in seconds) since this program was
64 return time
.time() - Timer
.starting_time
66 class DeviceHelper(object):
67 """A helper class with various generic device interaction methods."""
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()
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()
84 return preset
if preset
in devices
else None
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."""
98 cmd
.extend(['|', 'grep', '-F', str(default_pid
)])
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())
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
:
117 class MemoryHelper(object):
118 """A helper class to query basic memory usage of a process."""
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 ]."""
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':
139 if match
[0] == 'Native' and match
[1] == 'Heap':
142 elif match
[0] == 'Dalvik' and match
[1] == 'Heap':
145 elif match
[0] == 'TOTAL':
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:
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)
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',
172 __SHOWMAP_KEY_MATCHES
= ['/dev/pvrsrvkm',
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 ]."""
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
:
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
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]:
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
)
210 memstr
= adb
.Shell(' '.join(['cat', nv_file
]))
211 for line
in memstr
.splitlines():
212 match
= re
.split(' +', line
.strip())
214 return [ round(float(match
[3]) / 1000000.0, 2) ]
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
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
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
)
236 class MemorySnapshot(object):
237 """A class holding a snapshot of memory for various pids that are being
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
__()
255 self
.timestamp
= Timer
.GetTimestamp()
257 for (pid
, name
) in pids
:
258 self
.memory
[pid
] = self
.__QueryMemoryForPid
(adb
, pid
)
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,
265 results
= MemoryHelper
.QueryMemory(adb
, pid
)
266 results
.extend(GraphicsHelper
.QueryVideoMemory(adb
, pid
))
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."""
277 def GetPidAndNames(self
):
278 """Returns a list of (pid, process name) tuples that are being tracked in
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
:
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
]
297 def GetLongestNameLength(self
):
298 """Returns the length of the longest process name tracked by this
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.
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
318 __MEMORY_COLUMN_TITLES
= ['Native',
323 __TERMINAL_COLORS
= {'ENDC': 0,
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
340 def __FindPidsForSnapshotList(snapshots
):
341 """Find the set of unique pids across all every snapshot in |snapshots|."""
343 for snapshot
in snapshots
:
344 for (pid
, name
) in snapshot
.GetPidAndNames():
345 pids
.add((pid
, name
))
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
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
359 return (('%' if left_align
else '%-') + str(length
) + 's') % string
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:
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
]:
380 self
.__TermCode
(self
.__TERMINAL
_COLORS
[color
]),
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
394 sys
.stdout
.write(line
+ '\n')
396 self
.lines_printed
+= 1
398 def __ClearScreen(self
):
399 """Clears the screen based on the number of times |self.__OutputLine| was
401 if self
.lines_printed
== 0 or not self
.overwrite
:
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."""
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():
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')
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
()
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():
456 results
= snapshot
.GetResults(pid
)
460 old_results
= prev_snapshot
.GetResults(pid
) if prev_snapshot
else None
463 deltas
= [ 0, 0, 0, 0 ]
465 deltas
= map(sub
, results
, old_results
)
466 assert len(deltas
) == len(results
)
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
]))
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():
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
)
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():
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
)
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|."""
511 if not snapshot
or not snapshot
.HasResults():
512 self
.__OutputLine
("No results...")
515 self
.__OutputLine
(self
.__PrintLabeledStatsHeader
(snapshot
))
517 for (pid
, name
) in snapshot
.GetPidAndNames():
518 self
.__OutputLine
(self
.__PrintLabeledSnapshotStats
(pid
,
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
:
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
())
543 for snapshot
in snapshots
:
544 if not snapshot
.GetResults(pid
):
546 out
.write(self
.__PrintTimestampedBasicSnapshotStats
(pid
,
550 if not prev_snapshot
or not diff_against_start
:
551 prev_snapshot
= snapshot
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
560 # Import these here so the rest of the functionality doesn't rely on
562 from matplotlib
import pyplot
563 from matplotlib
.backends
.backend_pdf
import PdfPages
565 if not file_path
or not snapshots
:
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)')
576 ax
.set_title(name
+ ' (' + pid
+ ')')
578 mem_list
= [[] for x
in range(len(self
.__MEMORY
_COLUMN
_TITLES
))]
581 for snapshot
in snapshots
:
582 results
= snapshot
.GetResults(pid
)
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
)
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
)
602 parser
= argparse
.ArgumentParser()
603 parser
.add_argument('--process',
605 help="A (sub)string to match against process names.")
606 parser
.add_argument('-p',
609 type=Validator
.ValidateNonNegativeNumber
,
610 help='Which pid to scan for.')
611 parser
.add_argument('-d',
614 help='Device serial to scan.')
615 parser
.add_argument('-t',
618 type=Validator
.ValidateNonNegativeNumber
,
619 help='How long to track memory in seconds.')
620 parser
.add_argument('-f',
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',
630 help='Whether or not to always compare against the'
631 ' original memory values for deltas.')
632 parser
.add_argument('-b',
636 help='Whether or not to dull down the output.')
637 parser
.add_argument('-n',
641 help='Keeps printing the results in a list instead of'
642 ' overwriting the previous values.')
643 parser
.add_argument('-g',
646 type=Validator
.ValidatePdfPath
,
647 help='PDF file to save graph of memory stats to.')
648 parser
.add_argument('-o',
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'
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")
666 last_time
= time
.time()
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
)
676 elif not adb
or device
!= str(adb
):
677 adb
= adb_wrapper
.AdbWrapper(device
)
682 except device_errors
.AdbCommandFailedError
:
683 sys
.stderr
.write('Unable to run adb as root.\n')
686 # Grab a snapshot if we have a device
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
)
700 delay
= max(0, args
.frequency
- (time
.time() - last_time
))
703 last_time
= time
.time()
704 if not old_snapshot
or not args
.diff_against_start
:
705 old_snapshot
= snapshot
706 except KeyboardInterrupt:
710 printer
.PrettyGraph(args
.graph_file
, snapshots
)
713 printer
.PrettyFile(args
.text_file
, snapshots
, args
.diff_against_start
)
715 if __name__
== '__main__':
716 sys
.exit(main(sys
.argv
))