def _PrintDexAnalysis(apk_filename, chartjson=None): sizes = method_count.ExtractSizesFromZip(apk_filename) graph_title = os.path.basename(apk_filename) + '_Dex' dex_metrics = method_count.CONTRIBUTORS_TO_DEX_CACHE for key, label in dex_metrics.iteritems(): perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, label, sizes[key], 'entries') graph_title = '%sCache' % graph_title perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, 'DexCache', sizes['dex_cache_size'], 'bytes')
def __call__(self, graph_title, trace_title, value, units): super(_ChartJsonReporter, self).__call__(graph_title, trace_title, value, units) perf_tests_results_helper.ReportPerfResult( self._chartjson, graph_title, self.trace_title_prefix + trace_title, value, units)
def _PrintDexAnalysis(apk_filename, chartjson=None): sizes, total_size = method_count.ExtractSizesFromZip(apk_filename) graph_title = os.path.basename(apk_filename) + '_Dex' dex_metrics = method_count.CONTRIBUTORS_TO_DEX_CACHE cumulative_sizes = collections.defaultdict(int) for classes_dex_sizes in sizes.values(): for key in dex_metrics: cumulative_sizes[key] += classes_dex_sizes[key] for key, label in dex_metrics.iteritems(): perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, label, cumulative_sizes[key], 'entries') graph_title = '%sCache' % graph_title perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, 'DexCache', total_size, 'bytes')
def SynthesizeTotals(self, unique_method_count): for tup, value in sorted(self._combined_metrics.items()): graph_title, trace_title, units = tup if trace_title == 'unique methods': value = unique_method_count perf_tests_results_helper.ReportPerfResult( self._chartjson, graph_title, 'Combined_' + trace_title, value, units)
def PrintApkAnalysis(apk_filename, tool_prefix, out_dir, chartjson=None): """Calls GenerateApkAnalysis() and report the value.""" def PrintUnknown(info): print('Unknown entry: %s %d' % (info.filename, info.compress_size)) title_prefix = os.path.basename(apk_filename) + '_' for data in GenerateApkAnalysis(apk_filename, tool_prefix, out_dir, PrintUnknown): title = title_prefix + data[0] perf_tests_results_helper.ReportPerfResult(chartjson, title, *data[1:])
def _PrintPatchSizeEstimate(new_apk, builder, bucket, chartjson=None): apk_name = os.path.basename(new_apk) title = apk_name + '_PatchSizeEstimate' # Reference APK paths have spaces replaced by underscores. builder = builder.replace(' ', '_') old_apk = apk_downloader.MaybeDownloadApk( builder, apk_downloader.CURRENT_MILESTONE, apk_name, apk_downloader.DEFAULT_DOWNLOAD_PATH, bucket) if old_apk: # Use a temp dir in case patch size functions fail to clean up temp files. with build_utils.TempDir() as tmp: tmp_name = os.path.join(tmp, 'patch.tmp') bsdiff = apk_patch_size_estimator.calculate_bsdiff( old_apk, new_apk, None, tmp_name) perf_tests_results_helper.ReportPerfResult(chartjson, title, 'BSDiff (gzipped)', bsdiff, 'bytes') fbf = apk_patch_size_estimator.calculate_filebyfile( old_apk, new_apk, None, tmp_name) perf_tests_results_helper.ReportPerfResult(chartjson, title, 'FileByFile (gzipped)', fbf, 'bytes')
def AddIntermediateResults(chartjson, base_results, diff_results): """Copies the intermediate size results into the output chartjson. Args: chartjson: A dictionary that chartjson results will be placed in. base_results: The chartjson-formatted size results of the base APK. diff_results: The chartjson-formatted size results of the diff APK. """ for graph_title, graph in base_results['charts'].iteritems(): for trace_title, trace in graph.iteritems(): perf_tests_results_helper.ReportPerfResult( chartjson, graph_title + '_base_apk', trace_title, trace['value'], trace['units'], trace['improvement_direction'], trace['important']) # Both base_results and diff_results should have the same charts/traces, but # loop over them separately in case they don't for graph_title, graph in diff_results['charts'].iteritems(): for trace_title, trace in graph.iteritems(): perf_tests_results_helper.ReportPerfResult( chartjson, graph_title + '_diff_apk', trace_title, trace['value'], trace['units'], trace['improvement_direction'], trace['important'])
def DiffResults(chartjson, base_results, diff_results): """Reports the diff between the two given results. Args: chartjson: A dictionary that chartjson results will be placed in, or None to only print results. base_results: The chartjson-formatted size results of the base APK. diff_results: The chartjson-formatted size results of the diff APK. """ for graph_title, graph in base_results['charts'].iteritems(): for trace_title, trace in graph.iteritems(): perf_tests_results_helper.ReportPerfResult( chartjson, graph_title, trace_title, diff_results['charts'][graph_title][trace_title]['value'] - trace['value'], trace['units'], trace['improvement_direction'], trace['important'])
def SynthesizeTotals(self): for tup, value in sorted(self._combined_metrics.iteritems()): graph_title, trace_title, units = tup perf_tests_results_helper.ReportPerfResult( self._chartjson, graph_title, 'Combined_' + trace_title, value, units)
def __call__(self, graph_title, trace_title, value, units): self._combined_metrics[(graph_title, trace_title, units)] += value perf_tests_results_helper.ReportPerfResult( self._chartjson, graph_title, self.trace_title_prefix + trace_title, value, units)
def report_func(*args): # Do not add any new metrics without also documenting them in: # //docs/speed/binary_size/metrics.md. perf_tests_results_helper.ReportPerfResult(chartjson, *args)
def main(): argparser = argparse.ArgumentParser(description='Print APK size metrics.') argparser.add_argument( '--min-pak-resource-size', type=int, default=20 * 1024, help='Minimum byte size of displayed pak resources.') argparser.add_argument('--chromium-output-directory', dest='out_dir', help='Location of the build artifacts.') argparser.add_argument('--chartjson', action='store_true', help='Sets output mode to chartjson.') argparser.add_argument('--output-dir', default='.', help='Directory to save chartjson to.') argparser.add_argument( '--dump-static-initializers', action='store_true', dest='dump_sis', help='Run dump-static-initializers.py to get the list' 'of static initializers (slow).') argparser.add_argument( '--estimate-patch-size', action='store_true', help='Include patch size estimates. Useful for perf ' 'builders where a reference APK is available but adds ' '~3 mins to run time.') argparser.add_argument( '--reference-apk-builder', default=apk_downloader.DEFAULT_BUILDER, help='Builder name to use for reference APK for patch ' 'size estimates.') argparser.add_argument('--reference-apk-bucket', default=apk_downloader.DEFAULT_BUCKET, help='Storage bucket holding reference APKs.') argparser.add_argument('apk', help='APK file path.') args = argparser.parse_args() chartjson = _BASE_CHART.copy() if args.chartjson else None out_dir, tool_prefix = _ConfigOutDirAndToolsPrefix(args.out_dir) if args.dump_sis and not out_dir: argparser.error( '--dump-static-initializers requires --chromium-output-directory') # Do not add any new metrics without also documenting them in: # //docs/speed/binary_size/metrics.md. PrintApkAnalysis(args.apk, tool_prefix, out_dir, chartjson=chartjson) _PrintDexAnalysis(args.apk, chartjson=chartjson) si_count = AnalyzeStaticInitializers(args.apk, tool_prefix, args.dump_sis, out_dir, []) perf_tests_results_helper.ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, 'count') if args.estimate_patch_size: _PrintPatchSizeEstimate(args.apk, args.reference_apk_builder, args.reference_apk_bucket, chartjson=chartjson) if chartjson: results_path = os.path.join(args.output_dir, 'results-chart.json') logging.critical('Dumping json to %s', results_path) with open(results_path, 'w') as json_file: json.dump(chartjson, json_file)
def PrintApkAnalysis(apk_filename, tool_prefix, out_dir, chartjson=None): """Analyse APK to determine size contributions of different file classes.""" file_groups = [] def make_group(name): group = _FileGroup(name) file_groups.append(group) return group native_code = make_group('Native code') java_code = make_group('Java code') native_resources_no_translations = make_group('Native resources (no l10n)') translations = make_group('Native resources (l10n)') stored_translations = make_group('Native resources stored (l10n)') icu_data = make_group('ICU (i18n library) data') v8_snapshots = make_group('V8 Snapshots') png_drawables = make_group('PNG drawables') res_directory = make_group('Non-compiled Android resources') arsc = make_group('Compiled Android resources') metadata = make_group('Package metadata') unknown = make_group('Unknown files') notices = make_group('licenses.notice file') unwind_cfi = make_group('unwind_cfi (dev and canary only)') apk = zipfile.ZipFile(apk_filename, 'r') try: apk_contents = apk.infolist() finally: apk.close() dex_multiplier, skip_extract_lib = _ParseManifestAttributes(apk_filename) total_apk_size = os.path.getsize(apk_filename) apk_basename = os.path.basename(apk_filename) for member in apk_contents: filename = member.filename if filename.endswith('/'): continue if filename.endswith('.so'): should_extract_lib = not (skip_extract_lib or 'crazy' in filename) native_code.AddZipInfo( member, extracted_multiplier=int(should_extract_lib)) elif filename.endswith('.dex'): java_code.AddZipInfo(member, extracted_multiplier=dex_multiplier) elif re.search(_RE_NON_LANGUAGE_PAK, filename): native_resources_no_translations.AddZipInfo(member) elif re.search(_RE_COMPRESSED_LANGUAGE_PAK, filename): translations.AddZipInfo( member, extracted_multiplier=int('en_' in filename or 'en-' in filename)) elif re.search(_RE_STORED_LANGUAGE_PAK, filename): stored_translations.AddZipInfo(member) elif filename == 'assets/icudtl.dat': icu_data.AddZipInfo(member) elif filename.endswith('.bin'): v8_snapshots.AddZipInfo(member) elif filename.endswith('.png') or filename.endswith('.webp'): png_drawables.AddZipInfo(member) elif filename.startswith('res/'): res_directory.AddZipInfo(member) elif filename.endswith('.arsc'): arsc.AddZipInfo(member) elif filename.startswith( 'META-INF') or filename == 'AndroidManifest.xml': metadata.AddZipInfo(member) elif filename.endswith('.notice'): notices.AddZipInfo(member) elif filename.startswith('assets/unwind_cfi'): unwind_cfi.AddZipInfo(member) else: unknown.AddZipInfo(member) total_install_size = total_apk_size zip_overhead = total_apk_size for group in file_groups: actual_size = group.ComputeZippedSize() install_size = group.ComputeInstallSize() uncompressed_size = group.ComputeUncompressedSize() total_install_size += group.ComputeExtractedSize() zip_overhead -= actual_size perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Breakdown', group.name + ' size', actual_size, 'bytes') perf_tests_results_helper.ReportPerfResult( chartjson, apk_basename + '_InstallBreakdown', group.name + ' size', install_size, 'bytes') # Only a few metrics are compressed in the first place. # To avoid over-reporting, track uncompressed size only for compressed # entries. if uncompressed_size != actual_size: perf_tests_results_helper.ReportPerfResult( chartjson, apk_basename + '_Uncompressed', group.name + ' size', uncompressed_size, 'bytes') # Per-file zip overhead is caused by: # * 30 byte entry header + len(file name) # * 46 byte central directory entry + len(file name) # * 0-3 bytes for zipalign. perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Breakdown', 'Zip Overhead', zip_overhead, 'bytes') perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_InstallSize', 'APK size', total_apk_size, 'bytes') perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_InstallSize', 'Estimated installed size', total_install_size, 'bytes') transfer_size = _CalculateCompressedSize(apk_filename) perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_TransferSize', 'Transfer size (deflate)', transfer_size, 'bytes') # Size of main dex vs remaining. main_dex_info = java_code.FindByPattern('classes.dex') if main_dex_info: main_dex_size = main_dex_info.file_size perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Specifics', 'main dex size', main_dex_size, 'bytes') secondary_size = java_code.ComputeUncompressedSize() - main_dex_size perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Specifics', 'secondary dex size', secondary_size, 'bytes') # Size of main .so vs remaining. main_lib_info = native_code.FindLargest() if main_lib_info: main_lib_size = main_lib_info.file_size perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Specifics', 'main lib size', main_lib_size, 'bytes') secondary_size = native_code.ComputeUncompressedSize() - main_lib_size perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Specifics', 'other lib size', secondary_size, 'bytes') main_lib_section_sizes = _ExtractMainLibSectionSizesFromApk( apk_filename, main_lib_info.filename, tool_prefix) for metric_name, size in main_lib_section_sizes.iteritems(): perf_tests_results_helper.ReportPerfResult( chartjson, apk_basename + '_MainLibInfo', metric_name, size, 'bytes') # Main metric that we want to monitor for jumps. normalized_apk_size = total_apk_size # unwind_cfi exists only in dev, canary, and non-channel builds. normalized_apk_size -= unwind_cfi.ComputeZippedSize() # Always look at uncompressed .so. normalized_apk_size -= native_code.ComputeZippedSize() normalized_apk_size += native_code.ComputeUncompressedSize() # TODO(agrieve): Once we have better tooling (which can tell you where dex # size came from), change this to "ComputeExtractedSize()". normalized_apk_size += java_code.ComputeUncompressedSize() # Avoid noise caused when strings change and translations haven't yet been # updated. num_translations = translations.GetNumEntries() num_stored_translations = stored_translations.GetNumEntries() if num_translations > 1: # Multipliers found by looking at MonochromePublic.apk and seeing how much # smaller en-US.pak is relative to the average locale.pak. normalized_apk_size = _NormalizeLanguagePaks(translations, normalized_apk_size, 1.17) if num_stored_translations > 1: normalized_apk_size = _NormalizeLanguagePaks(stored_translations, normalized_apk_size, 1.43) if num_translations + num_stored_translations > 1: if num_translations == 0: # WebView stores all locale paks uncompressed. num_arsc_translations = num_stored_translations else: # Monochrome has more configurations than Chrome since it includes # WebView (which supports more locales), but these should mostly be empty # so ignore them here. num_arsc_translations = num_translations normalized_apk_size += int( _NormalizeResourcesArsc(apk_filename, arsc.GetNumEntries(), num_arsc_translations, out_dir)) perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Specifics', 'normalized apk size', normalized_apk_size, 'bytes') perf_tests_results_helper.ReportPerfResult(chartjson, apk_basename + '_Specifics', 'file count', len(apk_contents), 'zip entries') for info in unknown.AllEntries(): print 'Unknown entry:', info.filename, info.compress_size
def main(): argparser = argparse.ArgumentParser(description='Print APK size metrics.') argparser.add_argument( '--min-pak-resource-size', type=int, default=20 * 1024, help='Minimum byte size of displayed pak resources.') argparser.add_argument('--chromium-output-directory', dest='out_dir', help='Location of the build artifacts.') argparser.add_argument('--chartjson', action='store_true', help='DEPRECATED. Use --output-format=chartjson ' 'instead.') argparser.add_argument('--output-format', choices=['chartjson', 'histograms'], help='Output the results to a file in the given ' 'format instead of printing the results.') argparser.add_argument('--output-dir', default='.', help='Directory to save chartjson to.') argparser.add_argument( '--dump-static-initializers', action='store_true', dest='dump_sis', help='Run dump-static-initializers.py to get the list' 'of static initializers (slow).') argparser.add_argument('--loadable_module', action='append', help='Use for libraries added via loadable_modules') argparser.add_argument( '--estimate-patch-size', action='store_true', help='Include patch size estimates. Useful for perf ' 'builders where a reference APK is available but adds ' '~3 mins to run time.') argparser.add_argument( '--reference-apk-builder', default=apk_downloader.DEFAULT_BUILDER, help='Builder name to use for reference APK for patch ' 'size estimates.') argparser.add_argument('--reference-apk-bucket', default=apk_downloader.DEFAULT_BUCKET, help='Storage bucket holding reference APKs.') argparser.add_argument('apk', help='APK file path.') args = argparser.parse_args() # TODO(bsheedy): Remove this once uses of --chartjson have been removed. if args.chartjson: args.output_format = 'chartjson' chartjson = _BASE_CHART.copy() if args.output_format else None out_dir, tool_prefix = _ConfigOutDirAndToolsPrefix(args.out_dir) if args.dump_sis and not out_dir: argparser.error( '--dump-static-initializers requires --chromium-output-directory') # Do not add any new metrics without also documenting them in: # //docs/speed/binary_size/metrics.md. PrintApkAnalysis(args.apk, tool_prefix, out_dir, chartjson=chartjson) _PrintDexAnalysis(args.apk, chartjson=chartjson) ignored_libs = args.loadable_module if args.loadable_module else [] si_count = AnalyzeStaticInitializers(args.apk, tool_prefix, args.dump_sis, out_dir, ignored_libs) perf_tests_results_helper.ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, 'count') if args.estimate_patch_size: _PrintPatchSizeEstimate(args.apk, args.reference_apk_builder, args.reference_apk_bucket, chartjson=chartjson) if chartjson: results_path = os.path.join(args.output_dir, 'results-chart.json') logging.critical('Dumping chartjson to %s', results_path) with open(results_path, 'w') as json_file: json.dump(chartjson, json_file) # We would ideally generate a histogram set directly instead of generating # chartjson then converting. However, perf_tests_results_helper is in # //build, which doesn't seem to have any precedent for depending on # anything in Catapult. This can probably be fixed, but since this doesn't # need to be super fast or anything, converting is a good enough solution # for the time being. if args.output_format == 'histograms': histogram_result = convert_chart_json.ConvertChartJson( results_path) if histogram_result.returncode != 0: logging.error('chartjson conversion failed with error: %s', histogram_result.stdout) return 1 histogram_path = os.path.join(args.output_dir, 'perf_results.json') logging.critical('Dumping histograms to %s', histogram_path) with open(histogram_path, 'w') as json_file: json_file.write(histogram_result.stdout)
def _PrintDexAnalysis(apk_filename, chartjson=None): title_prefix = os.path.basename(apk_filename) + '_' for data in GenerateDexAnalysis(apk_filename): title = title_prefix + data[0] perf_tests_results_helper.ReportPerfResult(chartjson, title, *data[1:])