| #! /usr/bin/env python3 |
| # Copyright 2015 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| |
| import argparse |
| import os |
| import re |
| import zipfile |
| |
| from pylib.dex import dex_parser |
| |
| |
| class DexStatsCollector: |
| """Tracks count of method/field/string/type as well as unique methods.""" |
| |
| def __init__(self): |
| # Signatures of all methods from all seen dex files. |
| self._unique_methods = set() |
| # Map of label -> { metric -> count }. |
| self._counts_by_label = {} |
| |
| def _CollectFromDexfile(self, label, dexfile): |
| assert label not in self._counts_by_label, 'exists: ' + label |
| self._counts_by_label[label] = { |
| 'fields': dexfile.header.field_ids_size, |
| 'methods': dexfile.header.method_ids_size, |
| 'strings': dexfile.header.string_ids_size, |
| 'types': dexfile.header.type_ids_size, |
| } |
| self._unique_methods.update(dexfile.IterMethodSignatureParts()) |
| |
| def CollectFromZip(self, label, path): |
| """Add dex stats from an .apk/.jar/.aab/.zip.""" |
| with zipfile.ZipFile(path, 'r') as z: |
| for subpath in z.namelist(): |
| if not re.match(r'.*classes\d*\.dex$', subpath): |
| continue |
| dexfile = dex_parser.DexFile(bytearray(z.read(subpath))) |
| self._CollectFromDexfile('{}!{}'.format(label, subpath), dexfile) |
| |
| def CollectFromDex(self, label, path): |
| """Add dex stats from a .dex file.""" |
| with open(path, 'rb') as f: |
| dexfile = dex_parser.DexFile(bytearray(f.read())) |
| self._CollectFromDexfile(label, dexfile) |
| |
| def MergeFrom(self, parent_label, other): |
| """Add dex stats from another DexStatsCollector.""" |
| # pylint: disable=protected-access |
| for label, other_counts in other._counts_by_label.items(): |
| new_label = '{}-{}'.format(parent_label, label) |
| self._counts_by_label[new_label] = other_counts.copy() |
| self._unique_methods.update(other._unique_methods) |
| # pylint: enable=protected-access |
| |
| def GetUniqueMethodCount(self): |
| """Returns total number of unique methods across encountered dex files.""" |
| return len(self._unique_methods) |
| |
| def GetCountsByLabel(self): |
| """Returns dict of label -> {metric -> count}.""" |
| return self._counts_by_label |
| |
| def GetTotalCounts(self): |
| """Returns dict of {metric -> count}, where |count| is sum(metric).""" |
| ret = {} |
| for metric in ('fields', 'methods', 'strings', 'types'): |
| ret[metric] = sum(x[metric] for x in self._counts_by_label.values()) |
| return ret |
| |
| def GetDexCacheSize(self, pre_oreo): |
| """Returns number of bytes of dirty RAM is consumed from all dex files.""" |
| # Dex Cache was optimized in Android Oreo: |
| # https://source.android.com/devices/tech/dalvik/improvements#dex-cache-removal |
| if pre_oreo: |
| total = sum(self.GetTotalCounts().values()) |
| else: |
| total = sum(c['methods'] for c in self._counts_by_label.values()) |
| return total * 4 # 4 bytes per entry. |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('paths', nargs='+') |
| args = parser.parse_args() |
| |
| collector = DexStatsCollector() |
| for path in args.paths: |
| if os.path.splitext(path)[1] in ('.zip', '.apk', '.jar', '.aab'): |
| collector.CollectFromZip(path, path) |
| else: |
| collector.CollectFromDex(path, path) |
| |
| counts_by_label = collector.GetCountsByLabel() |
| for label, counts in sorted(counts_by_label.items()): |
| print('{}:'.format(label)) |
| for metric, count in sorted(counts.items()): |
| print(' {}:'.format(metric), count) |
| print() |
| |
| if len(counts_by_label) > 1: |
| print('Totals:') |
| for metric, count in sorted(collector.GetTotalCounts().items()): |
| print(' {}:'.format(metric), count) |
| print() |
| |
| print('Unique Methods:', collector.GetUniqueMethodCount()) |
| print('DexCache (Pre-Oreo):', collector.GetDexCacheSize(pre_oreo=True), |
| 'bytes of dirty memory') |
| print('DexCache (Oreo+):', collector.GetDexCacheSize(pre_oreo=False), |
| 'bytes of dirty memory') |
| |
| |
| if __name__ == '__main__': |
| main() |