|  | #! /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() |