From 0a1b0183f6a4a9c73dd91333b5dc3d5b72c62e6e Mon Sep 17 00:00:00 2001 From: Martijn Coenen Date: Mon, 30 Nov 2015 14:43:10 +0100 Subject: Pagecache tools. Dumpcache: dumps complete pagecache of device. pagecache.py: shows live info on files going in/out of pagecache Change-Id: Ieb2960d9e5daea8a7d9dcf23d2c31986182bc359 --- pagecache/Android.mk | 13 ++ pagecache/MODULE_LICENSE_APACHE2 | 0 pagecache/NOTICE | 190 ++++++++++++++++++++ pagecache/README | 4 + pagecache/dumpcache.c | 152 ++++++++++++++++ pagecache/pagecache.py | 372 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 731 insertions(+) create mode 100644 pagecache/Android.mk create mode 100644 pagecache/MODULE_LICENSE_APACHE2 create mode 100644 pagecache/NOTICE create mode 100644 pagecache/README create mode 100644 pagecache/dumpcache.c create mode 100755 pagecache/pagecache.py (limited to 'pagecache') diff --git a/pagecache/Android.mk b/pagecache/Android.mk new file mode 100644 index 00000000..fe064100 --- /dev/null +++ b/pagecache/Android.mk @@ -0,0 +1,13 @@ +# Copyright 2015 The Android Open Source Project + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= dumpcache.c +LOCAL_SHARED_LIBRARIES := libcutils +LOCAL_MODULE_PATH := $(TARGET_OUT_OPTIONAL_EXECUTABLES) +LOCAL_MODULE_TAGS := debug +LOCAL_MODULE:= dumpcache + +include $(BUILD_EXECUTABLE) + diff --git a/pagecache/MODULE_LICENSE_APACHE2 b/pagecache/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b diff --git a/pagecache/NOTICE b/pagecache/NOTICE new file mode 100644 index 00000000..34bdaf1d --- /dev/null +++ b/pagecache/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2015, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/pagecache/README b/pagecache/README new file mode 100644 index 00000000..08f4b536 --- /dev/null +++ b/pagecache/README @@ -0,0 +1,4 @@ +Pagecache tools. + +dumpcache.c: dumps complete pagecache of device. +pagecache.py: shows live info on files going in/out of pagecache. diff --git a/pagecache/dumpcache.c b/pagecache/dumpcache.c new file mode 100644 index 00000000..fc06bf24 --- /dev/null +++ b/pagecache/dumpcache.c @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// Initial size of the array holding struct file_info +#define INITIAL_NUM_FILES 512 + +// Max number of file descriptors to use for ntfw +#define MAX_NUM_FD 1 + +struct file_info { + char *name; + size_t file_size; + size_t num_cached_pages; +}; + +// Size of pages on this system +static int g_page_size; + +// Total number of cached pages found so far +static size_t g_total_cached = 0; + +// Total number of files scanned so far +static size_t g_num_files = 0; + +// Scanned files and their associated cached page counts +static struct file_info **g_files; + +// Current size of files array +size_t g_files_size; + +static struct file_info *get_file_info(const char* fpath, size_t file_size) { + struct file_info *info; + if (g_num_files >= g_files_size) { + g_files = realloc(g_files, 2 * g_files_size * sizeof(struct file_info*)); + if (!g_files) { + fprintf(stderr, "Couldn't allocate space for files array: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + g_files_size = 2 * g_files_size; + } + + info = calloc(1, sizeof(*info)); + if (!info) { + fprintf(stderr, "Couldn't allocate space for file struct: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + info->name = malloc(strlen(fpath) + 1); + if (!info->name) { + fprintf(stderr, "Couldn't allocate space for file struct: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + strcpy(info->name, fpath); + + info->num_cached_pages = 0; + info->file_size = file_size; + + g_files[g_num_files++] = info; + + return info; +} + +static int store_num_cached(const char* fpath, const struct stat *sb) { + int fd; + fd = open (fpath, O_RDONLY); + + if (fd == -1) { + printf("Could not open file."); + return -1; + } + + void* mapped_addr = mmap(NULL, sb->st_size, PROT_NONE, MAP_SHARED, fd, 0); + + if (mapped_addr != MAP_FAILED) { + // Calculate bit-vector size + size_t num_file_pages = (sb->st_size + g_page_size - 1) / g_page_size; + unsigned char* mincore_data = calloc(1, num_file_pages); + int ret = mincore(mapped_addr, sb->st_size, mincore_data); + int num_cached = 0; + unsigned int page = 0; + for (page = 0; page < num_file_pages; page++) { + if (mincore_data[page]) num_cached++; + } + if (num_cached > 0) { + struct file_info *info = get_file_info(fpath, sb->st_size); + info->num_cached_pages += num_cached; + g_total_cached += num_cached; + } + munmap(mapped_addr, sb->st_size); + } + + close(fd); + return 0; +} + +static int scan_entry(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { + if (typeflag == FTW_F) { + store_num_cached(fpath, sb); + } + return 0; +} + +static int cmpsize(size_t a, size_t b) { + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +static int cmpfiles(const void *a, const void *b) { + return cmpsize((*((struct file_info**)a))->num_cached_pages, + (*((struct file_info**)b))->num_cached_pages); +} + +int main() +{ + g_page_size = getpagesize(); + + g_files = malloc(INITIAL_NUM_FILES * sizeof(struct file_info*)); + g_files_size = INITIAL_NUM_FILES; + + // Walk filesystem trees + nftw("/system/", &scan_entry, MAX_NUM_FD, 0); + nftw("/vendor/", &scan_entry, MAX_NUM_FD, 0); + nftw("/data/", &scan_entry, MAX_NUM_FD, 0); + + // Sort entries + qsort(g_files, g_num_files, sizeof(g_files[0]), &cmpfiles); + + // Dump entries + for (size_t i = 0; i < g_num_files; i++) { + struct file_info *info = g_files[i]; + fprintf(stdout, "%s: %zu cached pages (%.2f MB, %lu%% of total file size.)\n", info->name, + info->num_cached_pages, + (float) (info->num_cached_pages * g_page_size) / 1024 / 1024, + (100 * info->num_cached_pages * g_page_size) / info->file_size); + } + + fprintf(stdout, "TOTAL CACHED: %zu pages (%f MB)\n", g_total_cached, + (float) (g_total_cached * 4096) / 1024 / 1024); + return 0; +} diff --git a/pagecache/pagecache.py b/pagecache/pagecache.py new file mode 100755 index 00000000..30d9fc67 --- /dev/null +++ b/pagecache/pagecache.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python + +import curses +import operator +import optparse +import os +import re +import subprocess +import sys +import threading +import Queue + +STATS_UPDATE_INTERVAL = 0.2 +PAGE_SIZE = 4096 + +class PagecacheStats(): + """Holds pagecache stats by accounting for pages added and removed. + + """ + def __init__(self, inode_to_filename): + self._inode_to_filename = inode_to_filename + self._file_size = {} + self._file_pages_added = {} + self._file_pages_removed = {} + self._total_pages_added = 0 + self._total_pages_removed = 0 + + def add_page(self, device_number, inode, offset): + # See if we can find the page in our lookup table + if (device_number, inode) in self._inode_to_filename: + filename, filesize = self._inode_to_filename[(device_number, inode)] + if filename not in self._file_pages_added: + self._file_pages_added[filename] = 1 + else: + self._file_pages_added[filename] += 1 + self._total_pages_added += 1 + + if filename not in self._file_size: + self._file_size[filename] = filesize + + def remove_page(self, device_number, inode, offset): + if (device_number, inode) in self._inode_to_filename: + filename, filesize = self._inode_to_filename[(device_number, inode)] + if filename not in self._file_pages_removed: + self._file_pages_removed[filename] = 1 + else: + self._file_pages_removed[filename] += 1 + self._total_pages_removed += 1 + + if filename not in self._file_size: + self._file_size[filename] = filesize + + def pages_to_mb(self, num_pages): + return "%.2f" % round(num_pages * PAGE_SIZE / 1024.0 / 1024.0, 2) + + def bytes_to_mb(self, num_bytes): + return "%.2f" % round(int(num_bytes) / 1024.0 / 1024.0, 2) + + def print_pages_and_mb(self, num_pages): + pages_string = str(num_pages) + ' (' + str(self.pages_to_mb(num_pages)) + ' MB)' + return pages_string + + def reset_stats(self): + self._file_pages_removed.clear() + self._file_pages_added.clear() + self._total_pages_added = 0; + self._total_pages_removed = 0; + + def print_stats(self, pad): + sorted_added = sorted(self._file_pages_added.items(), key=operator.itemgetter(1), reverse=True) + height, width = pad.getmaxyx() + pad.clear() + pad.addstr(0, 2, 'NAME'.ljust(68), curses.A_REVERSE) + pad.addstr(0, 70, 'ADDED (MB)'.ljust(12), curses.A_REVERSE) + pad.addstr(0, 82, 'REMOVED (MB)'.ljust(14), curses.A_REVERSE) + pad.addstr(0, 96, 'SIZE (MB)'.ljust(9), curses.A_REVERSE) + y = 1 + for filename, added in sorted_added: + filesize = self._file_size[filename] + removed = 0 + if filename in self._file_pages_removed: + removed = self._file_pages_removed[filename] + if (filename > 64): + filename = filename[-64:] + pad.addstr(y, 2, filename) + pad.addstr(y, 70, self.pages_to_mb(added).rjust(10)) + pad.addstr(y, 80, self.pages_to_mb(removed).rjust(14)) + pad.addstr(y, 96, self.bytes_to_mb(filesize).rjust(9)) + y += 1 + if y == height - 2: + pad.addstr(y, 4, "") + break + y += 1 + pad.addstr(y, 2, 'TOTAL'.ljust(74), curses.A_REVERSE) + pad.addstr(y, 70, str(self.pages_to_mb(self._total_pages_added)).rjust(10), curses.A_REVERSE) + pad.addstr(y, 80, str(self.pages_to_mb(self._total_pages_removed)).rjust(14), curses.A_REVERSE) + pad.refresh(0,0, 0,0, height,width) + +class FileReaderThread(threading.Thread): + """Reads data from a file/pipe on a worker thread. + + Use the standard threading. Thread object API to start and interact with the + thread (start(), join(), etc.). + """ + + def __init__(self, file_object, output_queue, text_file, chunk_size=-1): + """Initializes a FileReaderThread. + + Args: + file_object: The file or pipe to read from. + output_queue: A Queue.Queue object that will receive the data + text_file: If True, the file will be read one line at a time, and + chunk_size will be ignored. If False, line breaks are ignored and + chunk_size must be set to a positive integer. + chunk_size: When processing a non-text file (text_file = False), + chunk_size is the amount of data to copy into the queue with each + read operation. For text files, this parameter is ignored. + """ + threading.Thread.__init__(self) + self._file_object = file_object + self._output_queue = output_queue + self._text_file = text_file + self._chunk_size = chunk_size + assert text_file or chunk_size > 0 + + def run(self): + """Overrides Thread's run() function. + + Returns when an EOF is encountered. + """ + if self._text_file: + # Read a text file one line at a time. + for line in self._file_object: + self._output_queue.put(line) + else: + # Read binary or text data until we get to EOF. + while True: + chunk = self._file_object.read(self._chunk_size) + if not chunk: + break + self._output_queue.put(chunk) + + def set_chunk_size(self, chunk_size): + """Change the read chunk size. + + This function can only be called if the FileReaderThread object was + created with an initial chunk_size > 0. + Args: + chunk_size: the new chunk size for this file. Must be > 0. + """ + # The chunk size can be changed asynchronously while a file is being read + # in a worker thread. However, type of file can not be changed after the + # the FileReaderThread has been created. These asserts verify that we are + # only changing the chunk size, and not the type of file. + assert not self._text_file + assert chunk_size > 0 + self._chunk_size = chunk_size + +class AdbUtils(): + @staticmethod + def add_adb_serial(adb_command, device_serial): + if device_serial is not None: + adb_command.insert(1, device_serial) + adb_command.insert(1, '-s') + + @staticmethod + def construct_adb_shell_command(shell_args, device_serial): + adb_command = ['adb', 'shell', ' '.join(shell_args)] + AdbUtils.add_adb_serial(adb_command, device_serial) + return adb_command + + @staticmethod + def run_adb_shell(shell_args, device_serial): + """Runs "adb shell" with the given arguments. + + Args: + shell_args: array of arguments to pass to adb shell. + device_serial: if not empty, will add the appropriate command-line + parameters so that adb targets the given device. + Returns: + A tuple containing the adb output (stdout & stderr) and the return code + from adb. Will exit if adb fails to start. + """ + adb_command = AdbUtils.construct_adb_shell_command(shell_args, device_serial) + + adb_output = [] + adb_return_code = 0 + try: + adb_output = subprocess.check_output(adb_command, stderr=subprocess.STDOUT, + shell=False, universal_newlines=True) + except OSError as error: + # This usually means that the adb executable was not found in the path. + print >> sys.stderr, ('\nThe command "%s" failed with the following error:' + % ' '.join(adb_command)) + print >> sys.stderr, ' %s' % str(error) + print >> sys.stderr, 'Is adb in your path?' + adb_return_code = error.errno + adb_output = error + except subprocess.CalledProcessError as error: + # The process exited with an error. + adb_return_code = error.returncode + adb_output = error.output + + return (adb_output, adb_return_code) + + @staticmethod + def do_preprocess_adb_cmd(command, serial): + args = [command] + dump, ret_code = AdbUtils.run_adb_shell(args, serial) + if ret_code != 0: + return None + + dump = ''.join(dump) + return dump + +def parse_atrace_line(line, pagecache_stats): + # Find a mm_filemap_add_to_page_cache entry + 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) + if m != None: + # Get filename + device_number = int(m.group(2)) << 8 | int(m.group(3)) + if device_number == 0: + return + inode = int(m.group(4), 16) + if m.group(1) == 'mm_filemap_add_to_page_cache': + pagecache_stats.add_page(device_number, inode, m.group(4)) + elif m.group(1) == 'mm_filemap_delete_from_page_cache': + pagecache_stats.remove_page(device_number, inode, m.group(4)) + +def build_inode_lookup_table(inode_dump): + inode2filename = {} + text = inode_dump.splitlines() + for line in text: + result = re.match('([0-9]+) ([0-9]+) ([0-9]+) (.*)', line) + if result: + inode2filename[(int(result.group(1)), int(result.group(2)))] = (result.group(4), result.group(3)) + + return inode2filename; + +def get_inode_data(datafile, dumpfile, adb_serial): + if datafile is not None and os.path.isfile(datafile): + print('Using cached inode data from ' + datafile) + f = open(datafile, 'r') + stat_dump = f.read(); + else: + # Build inode maps if we were tracing page cache + print('Downloading inode data from device') + stat_dump = AdbUtils.do_preprocess_adb_cmd('find /system /data /vendor ' + + '-exec stat -c "%d %i %s %n" {} \;', adb_serial) + if stat_dump is None: + print 'Could not retrieve inode data from device.' + sys.exit(1) + + if dumpfile is not None: + print 'Storing inode data in ' + dumpfile + f = open(dumpfile, 'w') + f.write(stat_dump) + f.close() + + sys.stdout.write('Done.\n') + + return stat_dump + +def read_and_parse_trace_data(atrace, pagecache_stats): + # Start reading trace data + stdout_queue = Queue.Queue(maxsize=128) + stderr_queue = Queue.Queue() + + stdout_thread = FileReaderThread(atrace.stdout, stdout_queue, + text_file=True, chunk_size=64) + stderr_thread = FileReaderThread(atrace.stderr, stderr_queue, + text_file=True) + stdout_thread.start() + stderr_thread.start() + + stdscr = curses.initscr() + + try: + height, width = stdscr.getmaxyx() + curses.noecho() + curses.cbreak() + stdscr.keypad(True) + stdscr.nodelay(True) + stdscr.refresh() + # We need at least a 30x100 window + used_width = max(width, 100) + used_height = max(height, 30) + + # Create a pad for pagecache stats + pagecache_pad = curses.newpad(used_height - 2, used_width) + + stdscr.addstr(used_height - 1, 0, 'KEY SHORTCUTS: (r)eset stats, CTRL-c to quit') + while (stdout_thread.isAlive() or stderr_thread.isAlive() or + not stdout_queue.empty() or not stderr_queue.empty()): + while not stderr_queue.empty(): + # Pass along errors from adb. + line = stderr_queue.get() + sys.stderr.write(line) + while True: + try: + line = stdout_queue.get(True, STATS_UPDATE_INTERVAL) + parse_atrace_line(line, pagecache_stats) + except Queue.Empty: + break + + key = '' + try: + key = stdscr.getkey() + except: + pass + + if key == 'r': + pagecache_stats.reset_stats() + + pagecache_stats.print_stats(pagecache_pad) + except Exception, e: + curses.endwin() + print e + finally: + curses.endwin() + # The threads should already have stopped, so this is just for cleanup. + stdout_thread.join() + stderr_thread.join() + + atrace.stdout.close() + atrace.stderr.close() + + +def parse_options(argv): + usage = 'Usage: %prog [options]' + desc = 'Example: %prog' + parser = optparse.OptionParser(usage=usage, description=desc) + parser.add_option('-d', dest='inode_dump_file', metavar='FILE', + help='Dump the inode data read from a device to a file.' + ' This file can then be reused with the -i option to speed' + ' up future invocations of this script.') + parser.add_option('-i', dest='inode_data_file', metavar='FILE', + help='Read cached inode data from a file saved arlier with the' + ' -d option.') + parser.add_option('-s', '--serial', dest='device_serial', type='string', + help='adb device serial number') + options, categories = parser.parse_args(argv[1:]) + if options.inode_dump_file and options.inode_data_file: + parser.error('options -d and -i can\'t be used at the same time') + return (options, categories) + +def main(): + options, categories = parse_options(sys.argv) + + # Load inode data for this device + inode_data = get_inode_data(options.inode_data_file, options.inode_dump_file, + options.device_serial) + # Build (dev, inode) -> filename hash + inode_lookup_table = build_inode_lookup_table(inode_data) + # Init pagecache stats + pagecache_stats = PagecacheStats(inode_lookup_table) + + # Construct and execute trace command + trace_cmd = AdbUtils.construct_adb_shell_command(['atrace', '--stream', 'pagecache'], + options.device_serial) + + try: + atrace = subprocess.Popen(trace_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + except OSError as error: + print >> sys.stderr, ('The command failed') + sys.exit(1) + + read_and_parse_trace_data(atrace, pagecache_stats) + +if __name__ == "__main__": + main() -- cgit v1.2.3