OSDN Git Service

Pagecache tools.
authorMartijn Coenen <maco@google.com>
Mon, 30 Nov 2015 13:43:10 +0000 (14:43 +0100)
committerMartijn Coenen <maco@google.com>
Fri, 4 Dec 2015 15:59:36 +0000 (16:59 +0100)
Dumpcache: dumps complete pagecache of device.
pagecache.py: shows live info on files going in/out of pagecache

Change-Id: Ieb2960d9e5daea8a7d9dcf23d2c31986182bc359

pagecache/Android.mk [new file with mode: 0644]
pagecache/MODULE_LICENSE_APACHE2 [new file with mode: 0644]
pagecache/NOTICE [new file with mode: 0644]
pagecache/README [new file with mode: 0644]
pagecache/dumpcache.c [new file with mode: 0644]
pagecache/pagecache.py [new file with mode: 0755]

diff --git a/pagecache/Android.mk b/pagecache/Android.mk
new file mode 100644 (file)
index 0000000..fe06410
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/pagecache/NOTICE b/pagecache/NOTICE
new file mode 100644 (file)
index 0000000..34bdaf1
--- /dev/null
@@ -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 (file)
index 0000000..08f4b53
--- /dev/null
@@ -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 (file)
index 0000000..fc06bf2
--- /dev/null
@@ -0,0 +1,152 @@
+#include <ftw.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <ctype.h>
+#include <stddef.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+// 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 (executable)
index 0000000..30d9fc6
--- /dev/null
@@ -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, "<more...>")
+        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()