OSDN Git Service

Merge "Simpleperf: add separate cpu_hotplug_test." am: 8460573b13
[android-x86/system-extras.git] / pagecache / pagecache.py
1 #!/usr/bin/env python
2
3 import curses
4 import operator
5 import optparse
6 import os
7 import re
8 import subprocess
9 import sys
10 import threading
11 import Queue
12
13 STATS_UPDATE_INTERVAL = 0.2
14 PAGE_SIZE = 4096
15
16 class PagecacheStats():
17   """Holds pagecache stats by accounting for pages added and removed.
18
19   """
20   def __init__(self, inode_to_filename):
21     self._inode_to_filename = inode_to_filename
22     self._file_size = {}
23     self._file_pages_added = {}
24     self._file_pages_removed = {}
25     self._total_pages_added = 0
26     self._total_pages_removed = 0
27
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
34       else:
35         self._file_pages_added[filename] += 1
36       self._total_pages_added += 1
37
38       if filename not in self._file_size:
39         self._file_size[filename] = filesize
40
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
46       else:
47         self._file_pages_removed[filename] += 1
48       self._total_pages_removed += 1
49
50       if filename not in self._file_size:
51         self._file_size[filename] = filesize
52
53   def pages_to_mb(self, num_pages):
54     return "%.2f" % round(num_pages * PAGE_SIZE / 1024.0 / 1024.0, 2)
55
56   def bytes_to_mb(self, num_bytes):
57     return "%.2f" % round(int(num_bytes) / 1024.0 / 1024.0, 2)
58
59   def print_pages_and_mb(self, num_pages):
60     pages_string = str(num_pages) + ' (' + str(self.pages_to_mb(num_pages)) + ' MB)'
61     return pages_string
62
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;
68
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()
72     pad.clear()
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)
77     y = 1
78     for filename, added in sorted_added:
79       filesize = self._file_size[filename]
80       removed = 0
81       if filename in self._file_pages_removed:
82         removed = self._file_pages_removed[filename]
83       if (filename > 64):
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))
89       y += 1
90       if y == height - 2:
91         pad.addstr(y, 4, "<more...>")
92         break
93     y += 1
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)
98
99 class FileReaderThread(threading.Thread):
100   """Reads data from a file/pipe on a worker thread.
101
102   Use the standard threading. Thread object API to start and interact with the
103   thread (start(), join(), etc.).
104   """
105
106   def __init__(self, file_object, output_queue, text_file, chunk_size=-1):
107     """Initializes a FileReaderThread.
108
109     Args:
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.
118     """
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
125
126   def run(self):
127     """Overrides Thread's run() function.
128
129     Returns when an EOF is encountered.
130     """
131     if self._text_file:
132       # Read a text file one line at a time.
133       for line in self._file_object:
134         self._output_queue.put(line)
135     else:
136       # Read binary or text data until we get to EOF.
137       while True:
138         chunk = self._file_object.read(self._chunk_size)
139         if not chunk:
140           break
141         self._output_queue.put(chunk)
142
143   def set_chunk_size(self, chunk_size):
144     """Change the read chunk size.
145
146     This function can only be called if the FileReaderThread object was
147     created with an initial chunk_size > 0.
148     Args:
149       chunk_size: the new chunk size for this file.  Must be > 0.
150     """
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
158
159 class AdbUtils():
160   @staticmethod
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')
165
166   @staticmethod
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)
170     return adb_command
171
172   @staticmethod
173   def run_adb_shell(shell_args, device_serial):
174     """Runs "adb shell" with the given arguments.
175
176     Args:
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.
180     Returns:
181       A tuple containing the adb output (stdout & stderr) and the return code
182       from adb.  Will exit if adb fails to start.
183     """
184     adb_command = AdbUtils.construct_adb_shell_command(shell_args, device_serial)
185
186     adb_output = []
187     adb_return_code = 0
188     try:
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
198       adb_output = error
199     except subprocess.CalledProcessError as error:
200       # The process exited with an error.
201       adb_return_code = error.returncode
202       adb_output = error.output
203
204     return (adb_output, adb_return_code)
205
206   @staticmethod
207   def do_preprocess_adb_cmd(command, serial):
208     args = [command]
209     dump, ret_code = AdbUtils.run_adb_shell(args, serial)
210     if ret_code != 0:
211       return None
212
213     dump = ''.join(dump)
214     return dump
215
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)
219   if m != None:
220     # Get filename
221     device_number = int(m.group(2)) << 8 | int(m.group(3))
222     if device_number == 0:
223       return
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))
229
230 def build_inode_lookup_table(inode_dump):
231   inode2filename = {}
232   text = inode_dump.splitlines()
233   for line in text:
234     result = re.match('([0-9]+) ([0-9]+) ([0-9]+) (.*)', line)
235     if result:
236       inode2filename[(int(result.group(1)), int(result.group(2)))] = (result.group(4), result.group(3))
237
238   return inode2filename;
239
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();
245   else:
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.'
252       sys.exit(1)
253
254     if dumpfile is not None:
255       print 'Storing inode data in ' + dumpfile
256       f = open(dumpfile, 'w')
257       f.write(stat_dump)
258       f.close()
259
260     sys.stdout.write('Done.\n')
261
262   return stat_dump
263
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()
268
269   stdout_thread = FileReaderThread(atrace.stdout, stdout_queue,
270                                    text_file=True, chunk_size=64)
271   stderr_thread = FileReaderThread(atrace.stderr, stderr_queue,
272                                    text_file=True)
273   stdout_thread.start()
274   stderr_thread.start()
275
276   stdscr = curses.initscr()
277
278   try:
279     height, width = stdscr.getmaxyx()
280     curses.noecho()
281     curses.cbreak()
282     stdscr.keypad(True)
283     stdscr.nodelay(True)
284     stdscr.refresh()
285     # We need at least a 30x100 window
286     used_width = max(width, 100)
287     used_height = max(height, 30)
288
289     # Create a pad for pagecache stats
290     pagecache_pad = curses.newpad(used_height - 2, used_width)
291
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)
299       while True:
300         try:
301           line = stdout_queue.get(True, STATS_UPDATE_INTERVAL)
302           parse_atrace_line(line, pagecache_stats)
303         except Queue.Empty:
304           break
305
306       key = ''
307       try:
308         key = stdscr.getkey()
309       except:
310         pass
311
312       if key == 'r':
313         pagecache_stats.reset_stats()
314
315       pagecache_stats.print_stats(pagecache_pad)
316   except Exception, e:
317     curses.endwin()
318     print e
319   finally:
320     curses.endwin()
321     # The threads should already have stopped, so this is just for cleanup.
322     stdout_thread.join()
323     stderr_thread.join()
324
325     atrace.stdout.close()
326     atrace.stderr.close()
327
328
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'
339                     ' -d option.')
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)
346
347 def main():
348   options, categories = parse_options(sys.argv)
349
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)
357
358   # Construct and execute trace command
359   trace_cmd = AdbUtils.construct_adb_shell_command(['atrace', '--stream', 'pagecache'],
360       options.device_serial)
361
362   try:
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')
367     sys.exit(1)
368
369   read_and_parse_trace_data(atrace, pagecache_stats)
370
371 if __name__ == "__main__":
372   main()