#!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # Firefox about:memory log parser. import argparse import gzip import json from collections import defaultdict # This value comes from nsIMemoryReporter.idl. KIND_HEAP = 1 def path_total(data, path): """ Calculates the sum for the given data point path and its children. If path does not end with a '/' then only the value for the exact path is returned. """ path_totals = defaultdict(int) # Bookkeeping for calculating the heap-unclassified measurement. explicit_heap = defaultdict(int) heap_allocated = defaultdict(int) discrete = not path.endswith("/") def match(value): """ Helper that performs either an explicit match or a prefix match depending on the format of the path passed in. """ if discrete: return value == path else: return value.startswith(path) def update_bookkeeping(report): """ Adds the value to the heap total if this an explicit entry that is a heap measurement and updates the heap allocated value if necessary. """ if report["kind"] == KIND_HEAP and report["path"].startswith("explicit/"): explicit_heap[report["process"]] += report["amount"] elif report["path"] == "heap-allocated": heap_allocated[report["process"]] = report["amount"] def heap_unclassified(process): """ Calculates the heap-unclassified value for the given process. This is simply the difference between all values reported as heap allocated under the explicit/ tree and the value reported for heap-allocated by the allocator. """ # Memory reports should always include heap-allocated. If it's missing # just assert. assert process in heap_allocated unclassified = heap_allocated[process] - explicit_heap[process] # Make sure the value is sane. A misbehaving reporter could lead to # negative values. # This assertion fails on Beta while running TP6, in the Google Docs process. # Disable this for now, but only on Beta. See bug 1735556. # assert unclassified >= 0, "heap-unclassified was negative: %d" % unclassified return unclassified needs_bookkeeping = path in ("explicit/", "explicit/heap-unclassified") # Process all the reports. for report in data["reports"]: if needs_bookkeeping: update_bookkeeping(report) if match(report["path"]): path_totals[report["process"]] += report["amount"] # Handle special processing for explicit and heap-unclassified. if path == "explicit/": # If 'explicit/' is requested we need to add the 'explicit/heap-unclassified' # node that is generated by about:memory. for k, v in explicit_heap.items(): path_totals[k] += heap_unclassified(k) elif path == "explicit/heap-unclassified": # If 'explicit/heap-unclassified' is requested we need to calculate the # value as it's generated by about:memory, not explicitly reported. for k, v in explicit_heap.items(): path_totals[k] = heap_unclassified(k) return path_totals def calculate_memory_report_values( memory_report_path, data_point_path, process_names=None ): """ Opens the given memory report file and calculates the value for the given data point. :param memory_report_path: Path to the memory report file to parse. :param data_point_path: Path of the data point to calculate in the memory report, ie: 'explicit/heap-unclassified'. :param process_name: Name of processes to limit reports to. ie 'Main' """ try: with open(memory_report_path) as f: data = json.load(f) except ValueError: # Check if the file is gzipped. with gzip.open(memory_report_path, "rb") as f: data = json.load(f) totals = path_total(data, data_point_path) # If a process name is provided, restricted output to processes matching # that name. if process_names is not None: for k in list(totals.keys()): if not any([process_name in k for process_name in process_names]): del totals[k] return totals if __name__ == "__main__": parser = argparse.ArgumentParser( description="Extract data points from about:memory reports" ) parser.add_argument("report", action="store", help="Path to a memory report file.") parser.add_argument( "prefix", action="store", help="Prefix of data point to measure. " "If the prefix does not end in a '/' " "then an exact match is made.", ) parser.add_argument( "--proc-filter", action="store", nargs="*", default=None, help="Process name filter. " "If not provided all processes will be included.", ) parser.add_argument( "--mebi", action="store_true", help="Output values as mebibytes (instead of bytes)" " to match about:memory.", ) args = parser.parse_args() totals = calculate_memory_report_values(args.report, args.prefix, args.proc_filter) sorted_totals = sorted(totals.items(), key=lambda item: (-item[1], item[0])) for k, v in sorted_totals: if v: print("{0}\t".format(k)), print("") bytes_per_mebibyte = 1024.0 * 1024.0 for k, v in sorted_totals: if v: if args.mebi: print("{0:.2f} MiB".format(v / bytes_per_mebibyte)), else: print("{0} bytes".format(v)), print("\t"), print("")