13 STATS_UPDATE_INTERVAL = 0.2
16 class PagecacheStats():
17 """Holds pagecache stats by accounting for pages added and removed.
20 def __init__(self, inode_to_filename):
21 self._inode_to_filename = inode_to_filename
23 self._file_pages_added = {}
24 self._file_pages_removed = {}
25 self._total_pages_added = 0
26 self._total_pages_removed = 0
28 def add_page(self, device_number, inode, offset):
29 # See if we can find the page in our lookup table
30 if (device_number, inode) in self._inode_to_filename:
31 filename, filesize = self._inode_to_filename[(device_number, inode)]
32 if filename not in self._file_pages_added:
33 self._file_pages_added[filename] = 1
35 self._file_pages_added[filename] += 1
36 self._total_pages_added += 1
38 if filename not in self._file_size:
39 self._file_size[filename] = filesize
41 def remove_page(self, device_number, inode, offset):
42 if (device_number, inode) in self._inode_to_filename:
43 filename, filesize = self._inode_to_filename[(device_number, inode)]
44 if filename not in self._file_pages_removed:
45 self._file_pages_removed[filename] = 1
47 self._file_pages_removed[filename] += 1
48 self._total_pages_removed += 1
50 if filename not in self._file_size:
51 self._file_size[filename] = filesize
53 def pages_to_mb(self, num_pages):
54 return "%.2f" % round(num_pages * PAGE_SIZE / 1024.0 / 1024.0, 2)
56 def bytes_to_mb(self, num_bytes):
57 return "%.2f" % round(int(num_bytes) / 1024.0 / 1024.0, 2)
59 def print_pages_and_mb(self, num_pages):
60 pages_string = str(num_pages) + ' (' + str(self.pages_to_mb(num_pages)) + ' MB)'
63 def reset_stats(self):
64 self._file_pages_removed.clear()
65 self._file_pages_added.clear()
66 self._total_pages_added = 0;
67 self._total_pages_removed = 0;
69 def print_stats(self, pad):
70 sorted_added = sorted(self._file_pages_added.items(), key=operator.itemgetter(1), reverse=True)
71 height, width = pad.getmaxyx()
73 pad.addstr(0, 2, 'NAME'.ljust(68), curses.A_REVERSE)
74 pad.addstr(0, 70, 'ADDED (MB)'.ljust(12), curses.A_REVERSE)
75 pad.addstr(0, 82, 'REMOVED (MB)'.ljust(14), curses.A_REVERSE)
76 pad.addstr(0, 96, 'SIZE (MB)'.ljust(9), curses.A_REVERSE)
78 for filename, added in sorted_added:
79 filesize = self._file_size[filename]
81 if filename in self._file_pages_removed:
82 removed = self._file_pages_removed[filename]
84 filename = filename[-64:]
85 pad.addstr(y, 2, filename)
86 pad.addstr(y, 70, self.pages_to_mb(added).rjust(10))
87 pad.addstr(y, 80, self.pages_to_mb(removed).rjust(14))
88 pad.addstr(y, 96, self.bytes_to_mb(filesize).rjust(9))
91 pad.addstr(y, 4, "<more...>")
94 pad.addstr(y, 2, 'TOTAL'.ljust(74), curses.A_REVERSE)
95 pad.addstr(y, 70, str(self.pages_to_mb(self._total_pages_added)).rjust(10), curses.A_REVERSE)
96 pad.addstr(y, 80, str(self.pages_to_mb(self._total_pages_removed)).rjust(14), curses.A_REVERSE)
97 pad.refresh(0,0, 0,0, height,width)
99 class FileReaderThread(threading.Thread):
100 """Reads data from a file/pipe on a worker thread.
102 Use the standard threading. Thread object API to start and interact with the
103 thread (start(), join(), etc.).
106 def __init__(self, file_object, output_queue, text_file, chunk_size=-1):
107 """Initializes a FileReaderThread.
110 file_object: The file or pipe to read from.
111 output_queue: A Queue.Queue object that will receive the data
112 text_file: If True, the file will be read one line at a time, and
113 chunk_size will be ignored. If False, line breaks are ignored and
114 chunk_size must be set to a positive integer.
115 chunk_size: When processing a non-text file (text_file = False),
116 chunk_size is the amount of data to copy into the queue with each
117 read operation. For text files, this parameter is ignored.
119 threading.Thread.__init__(self)
120 self._file_object = file_object
121 self._output_queue = output_queue
122 self._text_file = text_file
123 self._chunk_size = chunk_size
124 assert text_file or chunk_size > 0
127 """Overrides Thread's run() function.
129 Returns when an EOF is encountered.
132 # Read a text file one line at a time.
133 for line in self._file_object:
134 self._output_queue.put(line)
136 # Read binary or text data until we get to EOF.
138 chunk = self._file_object.read(self._chunk_size)
141 self._output_queue.put(chunk)
143 def set_chunk_size(self, chunk_size):
144 """Change the read chunk size.
146 This function can only be called if the FileReaderThread object was
147 created with an initial chunk_size > 0.
149 chunk_size: the new chunk size for this file. Must be > 0.
151 # The chunk size can be changed asynchronously while a file is being read
152 # in a worker thread. However, type of file can not be changed after the
153 # the FileReaderThread has been created. These asserts verify that we are
154 # only changing the chunk size, and not the type of file.
155 assert not self._text_file
156 assert chunk_size > 0
157 self._chunk_size = chunk_size
161 def add_adb_serial(adb_command, device_serial):
162 if device_serial is not None:
163 adb_command.insert(1, device_serial)
164 adb_command.insert(1, '-s')
167 def construct_adb_shell_command(shell_args, device_serial):
168 adb_command = ['adb', 'shell', ' '.join(shell_args)]
169 AdbUtils.add_adb_serial(adb_command, device_serial)
173 def run_adb_shell(shell_args, device_serial):
174 """Runs "adb shell" with the given arguments.
177 shell_args: array of arguments to pass to adb shell.
178 device_serial: if not empty, will add the appropriate command-line
179 parameters so that adb targets the given device.
181 A tuple containing the adb output (stdout & stderr) and the return code
182 from adb. Will exit if adb fails to start.
184 adb_command = AdbUtils.construct_adb_shell_command(shell_args, device_serial)
189 adb_output = subprocess.check_output(adb_command, stderr=subprocess.STDOUT,
190 shell=False, universal_newlines=True)
191 except OSError as error:
192 # This usually means that the adb executable was not found in the path.
193 print >> sys.stderr, ('\nThe command "%s" failed with the following error:'
194 % ' '.join(adb_command))
195 print >> sys.stderr, ' %s' % str(error)
196 print >> sys.stderr, 'Is adb in your path?'
197 adb_return_code = error.errno
199 except subprocess.CalledProcessError as error:
200 # The process exited with an error.
201 adb_return_code = error.returncode
202 adb_output = error.output
204 return (adb_output, adb_return_code)
207 def do_preprocess_adb_cmd(command, serial):
209 dump, ret_code = AdbUtils.run_adb_shell(args, serial)
216 def parse_atrace_line(line, pagecache_stats):
217 # Find a mm_filemap_add_to_page_cache entry
218 m = re.match('.* (mm_filemap_add_to_page_cache|mm_filemap_delete_from_page_cache): dev (\d+):(\d+) ino ([0-9a-z]+) page=([0-9a-z]+) pfn=\d+ ofs=(\d+).*', line)
221 device_number = int(m.group(2)) << 8 | int(m.group(3))
222 if device_number == 0:
224 inode = int(m.group(4), 16)
225 if m.group(1) == 'mm_filemap_add_to_page_cache':
226 pagecache_stats.add_page(device_number, inode, m.group(4))
227 elif m.group(1) == 'mm_filemap_delete_from_page_cache':
228 pagecache_stats.remove_page(device_number, inode, m.group(4))
230 def build_inode_lookup_table(inode_dump):
232 text = inode_dump.splitlines()
234 result = re.match('([0-9]+) ([0-9]+) ([0-9]+) (.*)', line)
236 inode2filename[(int(result.group(1)), int(result.group(2)))] = (result.group(4), result.group(3))
238 return inode2filename;
240 def get_inode_data(datafile, dumpfile, adb_serial):
241 if datafile is not None and os.path.isfile(datafile):
242 print('Using cached inode data from ' + datafile)
243 f = open(datafile, 'r')
244 stat_dump = f.read();
246 # Build inode maps if we were tracing page cache
247 print('Downloading inode data from device')
248 stat_dump = AdbUtils.do_preprocess_adb_cmd('find /system /data /vendor ' +
249 '-exec stat -c "%d %i %s %n" {} \;', adb_serial)
250 if stat_dump is None:
251 print 'Could not retrieve inode data from device.'
254 if dumpfile is not None:
255 print 'Storing inode data in ' + dumpfile
256 f = open(dumpfile, 'w')
260 sys.stdout.write('Done.\n')
264 def read_and_parse_trace_data(atrace, pagecache_stats):
265 # Start reading trace data
266 stdout_queue = Queue.Queue(maxsize=128)
267 stderr_queue = Queue.Queue()
269 stdout_thread = FileReaderThread(atrace.stdout, stdout_queue,
270 text_file=True, chunk_size=64)
271 stderr_thread = FileReaderThread(atrace.stderr, stderr_queue,
273 stdout_thread.start()
274 stderr_thread.start()
276 stdscr = curses.initscr()
279 height, width = stdscr.getmaxyx()
285 # We need at least a 30x100 window
286 used_width = max(width, 100)
287 used_height = max(height, 30)
289 # Create a pad for pagecache stats
290 pagecache_pad = curses.newpad(used_height - 2, used_width)
292 stdscr.addstr(used_height - 1, 0, 'KEY SHORTCUTS: (r)eset stats, CTRL-c to quit')
293 while (stdout_thread.isAlive() or stderr_thread.isAlive() or
294 not stdout_queue.empty() or not stderr_queue.empty()):
295 while not stderr_queue.empty():
296 # Pass along errors from adb.
297 line = stderr_queue.get()
298 sys.stderr.write(line)
301 line = stdout_queue.get(True, STATS_UPDATE_INTERVAL)
302 parse_atrace_line(line, pagecache_stats)
308 key = stdscr.getkey()
313 pagecache_stats.reset_stats()
315 pagecache_stats.print_stats(pagecache_pad)
321 # The threads should already have stopped, so this is just for cleanup.
325 atrace.stdout.close()
326 atrace.stderr.close()
329 def parse_options(argv):
330 usage = 'Usage: %prog [options]'
331 desc = 'Example: %prog'
332 parser = optparse.OptionParser(usage=usage, description=desc)
333 parser.add_option('-d', dest='inode_dump_file', metavar='FILE',
334 help='Dump the inode data read from a device to a file.'
335 ' This file can then be reused with the -i option to speed'
336 ' up future invocations of this script.')
337 parser.add_option('-i', dest='inode_data_file', metavar='FILE',
338 help='Read cached inode data from a file saved arlier with the'
340 parser.add_option('-s', '--serial', dest='device_serial', type='string',
341 help='adb device serial number')
342 options, categories = parser.parse_args(argv[1:])
343 if options.inode_dump_file and options.inode_data_file:
344 parser.error('options -d and -i can\'t be used at the same time')
345 return (options, categories)
348 options, categories = parse_options(sys.argv)
350 # Load inode data for this device
351 inode_data = get_inode_data(options.inode_data_file, options.inode_dump_file,
352 options.device_serial)
353 # Build (dev, inode) -> filename hash
354 inode_lookup_table = build_inode_lookup_table(inode_data)
355 # Init pagecache stats
356 pagecache_stats = PagecacheStats(inode_lookup_table)
358 # Construct and execute trace command
359 trace_cmd = AdbUtils.construct_adb_shell_command(['atrace', '--stream', 'pagecache'],
360 options.device_serial)
363 atrace = subprocess.Popen(trace_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
364 stderr=subprocess.PIPE)
365 except OSError as error:
366 print >> sys.stderr, ('The command failed')
369 read_and_parse_trace_data(atrace, pagecache_stats)
371 if __name__ == "__main__":