tools/android: Estimate the footprint of a process.

Private memory footprint does not accurately reflect the additional footprint of
a child process, as it does not account for page tables or shared memory for
instance, but it includes memory footprint inherited (and shared with) the
Android zygote.

This makes a few assumptions to compute the memory footprint that are detailed
in the code, and would be most accurate on a high-memory device shortly after
the process is created, to make sure not to undercount swap.

Bug: 973813
Change-Id: Ifa4f4e36c0ddc3316f3b6a74fb5b1151b4337b11
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1657899
Reviewed-by: Matthew Cary (CET) <mattcary@chromium.org>
Reviewed-by: Egor Pasko <pasko@chromium.org>
Commit-Queue: Benoit L <lizeb@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#668795}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 96b0cc87e0871388e60d94a7f0393dd8bd169b67
diff --git a/native_lib_memory/parse_smaps.py b/native_lib_memory/parse_smaps.py
index 6cb711c..6113b2a 100755
--- a/native_lib_memory/parse_smaps.py
+++ b/native_lib_memory/parse_smaps.py
@@ -101,37 +101,121 @@
   return mappings
 
 
-def ParseProcSmaps(device, pid):
+def ParseProcSmaps(device, pid, store_file=False):
   """Parses /proc/[pid]/smaps on a device, and returns a list of Mapping.
 
   Args:
     device: (device_utils.DeviceUtils) device to parse the file from.
     pid: (int) PID of the process.
+    store_file: (bool) Whether to also write the file to disk.
 
   Returns:
     [Mapping] all the mappings in /proc/[pid]/smaps.
   """
   command = ['cat', '/proc/%d/smaps' % pid]
   lines = device.RunShellCommand(command, check_return=True)
+  if store_file:
+    with open('smaps-%d' % pid, 'w') as f:
+      f.write('\n'.join(lines))
   return _ParseProcSmapsLines(lines)
 
 
-def _PrintSwapStats(mappings):
-  total_swap_kb = sum(m.fields['Swap'] for m in mappings)
-  print 'Total Swap Size (kB) = %d' % total_swap_kb
-  swap_sorted = sorted(mappings, key=lambda m: m.fields['Swap'], reverse=True)
-  for mapping in swap_sorted:
-    swapped = mapping.fields['Swap']
-    if not swapped:
+def _GetPageTableFootprint(device, pid):
+  """Returns the page table footprint for a process in kiB."""
+  command = ['cat', '/proc/%d/status' % pid]
+  lines = device.RunShellCommand(command, check_return=True)
+  for line in lines:
+    if line.startswith('VmPTE:'):
+      value = int(line[len('VmPTE: '):line.index('kB')])
+      return value
+
+
+def _SummarizeMapping(mapping, metric):
+  return '%s %s %s: %d kB (Total Size: %d kB)' % (
+      hex(mapping.start),
+      mapping.pathname, mapping.permissions, metric,
+      (mapping.end - mapping.start) / 1024)
+
+
+def _PrintMappingsMetric(mappings, field_name):
+  """Shows a summary of mappings for a given metric.
+
+  For the given field, compute its aggregate value over all mappings, and
+  prints the mappings sorted by decreasing metric value.
+
+  Args:
+    mappings: ([Mapping]) all process mappings.
+    field_name: (str) Mapping field to process.
+  """
+  total_kb = sum(m.fields[field_name] for m in mappings)
+  print 'Total Size (kB) = %d' % total_kb
+  sorted_by_metric = sorted(mappings,
+                            key=lambda m: m.fields[field_name], reverse=True)
+  for mapping in sorted_by_metric:
+    metric = mapping.fields[field_name]
+    if not metric:
       break
-    print '%s %s: %d kB (Total Size: %d kB)' % (
-        mapping.pathname, mapping.permissions, swapped,
-        (mapping.end - mapping.start) / 1024)
+    print _SummarizeMapping(mapping, metric)
+
+
+def _PrintSwapStats(mappings):
+  print 'SWAP:'
+  _PrintMappingsMetric(mappings, 'Swap')
+
+
+def _PrintEstimatedFootprintStats(mappings, page_table_kb):
+  print 'Private Dirty:'
+  _PrintMappingsMetric(mappings, 'Private_Dirty')
+  print '\n\nShared Dirty:'
+  _PrintMappingsMetric(mappings, 'Shared_Dirty')
+  print '\n\nPrivate Clean:'
+  _PrintMappingsMetric(mappings, 'Private_Clean')
+  print '\n\nShared Clean:'
+  _PrintMappingsMetric(mappings, 'Shared_Clean')
+  print '\n\nSwap PSS:'
+  _PrintMappingsMetric(mappings, 'SwapPss')
+  print '\n\nPage table = %d kiB' % page_table_kb
+
+
+def _ComputeEstimatedFootprint(mappings, page_table_kb):
+  """Returns the estimated footprint in kiB."""
+  footprint = page_table_kb
+  for mapping in mappings:
+    # Chrome shared memory.
+    #
+    # Even though it is shared memory, it exists because the process exists, so
+    # account for its entirety.
+    if mapping.pathname.startswith('/dev/ashmem/shared_memory'):
+      footprint += mapping.fields['Rss']
+    elif mapping.pathname.startswith('[anon:libc_malloc]'):
+      if mapping.fields['Shared_Dirty'] == 0:
+        footprint += mapping.fields['Rss']
+      else:
+        footprint += mapping.fields['Private_Dirty']  # Shared dirty is likely
+                                                      # from the zygote.
+    # Mappings without a name are most likely Chrome's native memory allocators:
+    # v8, PartitionAlloc, Oilpan.
+    # All of it should be charged to our process.
+    elif mapping.pathname.strip() == '':
+      footprint += mapping.fields['Rss']
+    # Often inherited from the zygote, only count the private dirty part,
+    # especially as the swap part likely comes from the zygote.
+    elif mapping.pathname.startswith('['):
+      footprint += mapping.fields['Private_Dirty']
+    # File mappings. Can be a real file, and/or Dalvik/ART.
+    else:
+      footprint += mapping.fields['Private_Dirty']
+  return footprint
 
 
 def _CreateArgumentParser():
   parser = argparse.ArgumentParser()
-  parser.add_argument('--pid', help='PID.', required=True)
+  parser.add_argument('--pid', help='PID.', required=True, type=int)
+  parser.add_argument('--estimate-footprint',
+                      help='Show the estimated memory foootprint',
+                      action='store_true')
+  parser.add_argument('--store-smaps', help='Store the smaps file locally',
+                      action='store_true')
   return parser
 
 
@@ -146,8 +230,14 @@
   device.EnableRoot()
   # Enable logging after device handling as devil is noisy at INFO level.
   logging.basicConfig(level=logging.INFO)
-  mappings = ParseProcSmaps(device, int(args.pid))
-  _PrintSwapStats(mappings)
+  mappings = ParseProcSmaps(device, args.pid, args.store_smaps)
+  if args.estimate_footprint:
+    page_table_kb = _GetPageTableFootprint(device, args.pid)
+    _PrintEstimatedFootprintStats(mappings, page_table_kb)
+    footprint = _ComputeEstimatedFootprint(mappings, page_table_kb)
+    print '\n\nEstimated Footprint = %d kiB' % footprint
+  else:
+    _PrintSwapStats(mappings)
 
 
 if __name__ == '__main__':