| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // @dart = 2.8 |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| |
| import 'common_util.dart'; |
| import 'crash_handling.dart'; |
| import 'io.dart'; |
| import 'queries/index.dart' as queries; |
| import 'queries/index.dart'; |
| import 'queries/source_lang.dart'; |
| import 'reflect.dart'; |
| import 'run_queries.dart'; |
| |
| /// Parse command line arguments |
| ParsedArgs parseArgs(List<String> args) { |
| final parser = ArgParser(allowTrailingOptions: true) |
| ..addSeparator('Core options:\n') |
| ..addFlag('help', help: 'Give this help.', negatable: false) |
| // The `--[no-]cache` parameter is a tri-state: |
| // |
| // fx codesize --cache => Always use cached bloaty reports |
| // fx codesize --no-cache => Never use cached bloaty reports |
| // fx codesize => If the system image is newer than the report index, |
| // re-run bloaty. Otherwise use cache. This is the default. |
| ..addFlag(cache, |
| help: |
| 'Use the cached version of the list of bloaty reports if available', |
| defaultsTo: false, |
| negatable: false) |
| ..addFlag(noCache, |
| help: 'Do not use the cache of the list of bloaty reports', |
| defaultsTo: false, |
| negatable: false) |
| ..addOption(buildDir, |
| help: 'The build output directory (e.g. out/default).\n' |
| 'If absent, defaults to the current output directory ' |
| '(FUCHSIA_BUILD_DIR)', |
| defaultsTo: Platform.environment['FUCHSIA_BUILD_DIR']) |
| ..addOption(concurrency, |
| help: 'The number of worker threads.\n' |
| 'If absent, defaults to a reasonable number based on the CPU cores', |
| abbr: 'j') |
| ..addSeparator('Filtering by name and language:\n') |
| ..addOption(fileRegex, |
| help: 'Optionally specify a regex to only incorporate statistics\n' |
| 'from binaries whose name match the regex.', |
| abbr: 'f', |
| defaultsTo: '.*') |
| ..addOption(onlyLang, |
| help: 'Only incorporate statistics from binaries ' |
| 'of this programming language.', |
| abbr: 'l', |
| allowed: SourceLang.values.map((e) => e.name)) |
| ..addSeparator('Filtering by run-time page-in frequency:\n') |
| ..addOption(heatmap, |
| help: 'Optionally specify a blob access heatmap to only show symbols ' |
| 'that have never been used at run-time.\n' |
| 'The heatmap is a CSV in [merkle],[[frame index]:[frequency],...] ' |
| 'format, where each frame is 8 KiB by default.\n' |
| 'See the detailed explanation of `cold_bytes_filter` in ' |
| 'https://fuchsia.googlesource.com/third_party/bloaty/+/refs/heads/fuchsia/src/bloaty.proto\n' |
| '') |
| ..addOption(heatmapFrameSize, |
| help: 'When a heatmap is used, specify the size of a frame in bytes', |
| defaultsTo: '8192') |
| ..addSeparator('Output controls:\n') |
| ..addOption(outputFile, |
| help: 'The destination for writing stats. If absent, assumes stdout.', |
| abbr: 'o') |
| ..addOption(format, |
| help: 'Format for the output.', |
| allowed: OutputFormat.values.map((e) => e.name), |
| allowedHelp: <OutputFormat, String>{ |
| OutputFormat.terminal: |
| 'Character-based rendering suitable for terminal', |
| OutputFormat.basic: 'Plain-text minimal summary', |
| OutputFormat.html: 'Rich HTML formatting', |
| OutputFormat.tsv: 'Tab-separated values table', |
| }.map((key, value) => MapEntry<String, String>(key.name, value)), |
| defaultsTo: OutputFormat.terminal.name); |
| |
| final queryHelp = StringBuffer(); |
| for (final query in queries.allQueries) { |
| queryHelp |
| ..write(' ${query.name.padRight(24)}') |
| ..writeln(query.description); |
| if (ReflectQuery.hasCustomArguments(query)) { |
| for (final constructor in ReflectQuery.describeQueryConstructors(query)) { |
| queryHelp.writeln(' $constructor <- optional arguments\n'); |
| } |
| } else { |
| queryHelp.writeln(); |
| } |
| } |
| queryHelp.write(r''' |
| The syntax for specifying optional arguments to queries is similar to Dart |
| function calls and passing arguments by name, with the exception that strings |
| and string-like argument values are un-quoted. For instance, to specify the |
| `sortBySize` argument to `BinaryNames`, one could write on the command line: |
| |
| # Use single-quotes to escape any special characters on the shell. |
| fx codesize 'BinaryNames(sortBySize: true)' |
| |
| '''); |
| |
| const defaultQueryConstructors = <String>['CodeCategory', 'SourceLang']; |
| final argResults = parser.parse(args); |
| if (argResults['help']) { |
| final examples = |
| // ignore: prefer_interpolation_to_compose_strings |
| ('# By default, codesize runs the ${defaultQueryConstructors.join(" and ")} signal\n' |
| r''' |
| fx codesize |
| |
| # Can run on a subset of binaries, filtered by a regex |
| # For instance, "(?!\[prebuilt\])" will skip all the prebuilts, which might be helpful |
| # if changing SDK libraries that won't lead to their update |
| fx codesize --file-regex '^(?!\[prebuilt\])' |
| |
| # Another example, only looking at the ELF binaries in Zircon Boot Images |
| fx codesize --file-regex '^\[zbi: ' |
| |
| # Another example, only running on appmgr... |
| fx codesize --file-regex appmgr CodeCategory |
| |
| # Listing all symbols with annotation in a binary, grouped by compile units, also hiding unknown symbols |
| fx codesize --file-regex appmgr 'Symbols(hideUnknown: true)' |
| |
| # Look through all C++ symbols alongside their containing programs, sorted by aggregate size |
| fx codesize --only-lang=cpp 'UniqueSymbol(showCompileUnit: true, showProgram: true, hideUnknown: false)' | less |
| ''') |
| .split('\n') |
| .map((s) => ' $s') |
| .join('\n'); |
| print('Usage: fx codesize [OPTION]... [QUERY]...\n\n' |
| 'Looks at all the ELF binaries in the fvm/zbi images in the out dir,\n' |
| 'computes various bary size queries specified in [QUERY]...\n' |
| 'If [QUERY] is absent, defaults to CodeCategory and SourceLang\n\n' |
| '${parser.usage}\n\n' |
| 'Supported queries:\n\n$queryHelp' |
| 'Some examples:\n\n$examples\n\n' |
| '''Exit codes: |
| |
| 0: Success |
| 1: General unhandled exception (indicates a bug in codesize) |
| 2: Known errors/unsatisfied preconditions (not a bug in codesize) |
| '''); |
| return null; |
| } |
| |
| // Default query to `SourceLang` if none was set. |
| List<String> queryConstructors = argResults.rest ?? []; |
| if (queryConstructors.isEmpty) queryConstructors = defaultQueryConstructors; |
| |
| final List<QueryThunk> selectedQueries = queryConstructors |
| .map((String s) => parseQueryConstructor(queriesByName, s)) |
| .toList(growable: false); |
| |
| // We close the sink in the main function. |
| // ignore: close_sinks |
| IOSink output = Io.get().out; |
| if (argResults[outputFile] != null) { |
| output = File(argResults[outputFile]).openWrite(); |
| } |
| |
| CachingBehavior cachingBehavior; |
| if (argResults[cache] && argResults[noCache]) |
| throw KnownFailure('--cache and --no-cache cannot both be specified'); |
| if (argResults[cache]) |
| cachingBehavior = CachingBehavior.alwaysUseCache; |
| else if (argResults[noCache]) |
| cachingBehavior = CachingBehavior.neverUseCache; |
| else |
| cachingBehavior = CachingBehavior.useIfUpToDate; |
| |
| return ParsedArgs( |
| buildDir: argResults[buildDir], |
| fileRegex: RegExp(argResults[fileRegex]), |
| output: output, |
| cachingBehavior: cachingBehavior, |
| // ignore: avoid_as |
| concurrency: flatMap(argResults[concurrency] as String, int.parse), |
| selectedQueries: List<QueryThunk>.from(selectedQueries), |
| format: toOutputFormat(argResults[format]), |
| // ignore: avoid_as |
| onlyLang: flatMap(argResults[onlyLang] as String, toSourceLang), |
| // ignore: avoid_as |
| heatmap: flatMap(argResults[heatmap] as String, (x) => File(x)), |
| heatmapFrameSize: int.parse(argResults[heatmapFrameSize])); |
| } |
| |
| enum OutputFormat { terminal, basic, html, tsv } |
| |
| extension on OutputFormat { |
| String get name => removePrefix(toString(), 'OutputFormat.'); |
| } |
| |
| OutputFormat toOutputFormat(String name) { |
| for (final format in OutputFormat.values) { |
| if (format.name == name) return format; |
| } |
| return null; |
| } |
| |
| extension on SourceLang { |
| String get name => removePrefix(toString(), 'SourceLang.'); |
| } |
| |
| SourceLang toSourceLang(String name) { |
| for (final lang in SourceLang.values) { |
| if (lang.name == name) return lang; |
| } |
| return null; |
| } |
| |
| enum CachingBehavior { alwaysUseCache, neverUseCache, useIfUpToDate } |
| |
| final queriesByName = Map.fromEntries( |
| allQueries.map((s) => MapEntry<String, QueryFactory>(s.name, s))); |
| |
| /// Turns |
| /// |
| /// ``` |
| /// "foo: 5, bar: abc" |
| /// ``` |
| /// |
| /// into |
| /// |
| /// ``` |
| /// {'foo': '5', 'bar': 'abc'} |
| /// ``` |
| Map<String, String> parseQueryConstructorArgs(String args) => |
| Map.fromEntries(args.split(',').map((s) { |
| final tokens = s.trim().split(':'); |
| assert(tokens.length == 2); |
| final name = tokens[0].trim(); |
| final value = tokens[1].trim(); |
| return MapEntry<String, String>(name, value); |
| })); |
| |
| /// Turns a string of the form `MyQuery(foo: 5, bar: 'abc')` |
| /// into a zero-arg function which when evaluated, produces |
| /// a new instance of `MyQuery` with those arguments. |
| QueryThunk parseQueryConstructor( |
| Map<String, QueryFactory> queries, String _constructor) { |
| final constructor = _constructor.trim(); |
| final startOfArgs = constructor.indexOf('('); |
| String name; |
| Map<String, String> args = {}; |
| if (startOfArgs == -1) { |
| name = constructor; |
| } else { |
| name = constructor.substring(0, startOfArgs); |
| if (constructor[constructor.length - 1] != ')') { |
| throw KnownFailure('$constructor should end with `)`'); |
| } |
| args = parseQueryConstructorArgs( |
| constructor.substring(startOfArgs + 1, constructor.length - 1)); |
| } |
| QueryFactory f; |
| if (queries.containsKey(name)) { |
| f = queries[name]; |
| } else { |
| if (name == 'DumpNames') { |
| throw KnownFailure('DumpNames have been renamed to BinaryNames'); |
| } |
| throw KnownFailure('Query `$name` not found. Pick from: ${queries.keys}'); |
| } |
| return () => ReflectQuery.instantiate(f, args); |
| } |
| |
| // Defining command line arguments --------------------------------------------- |
| |
| const cache = 'cache'; |
| const noCache = 'no-cache'; |
| const buildDir = 'build-dir'; |
| const fileRegex = 'file-regex'; |
| const onlyLang = 'only-lang'; |
| const outputFile = 'output'; |
| const format = 'format'; |
| const concurrency = 'concurrency'; |
| const heatmap = 'heatmap'; |
| const heatmapFrameSize = 'heatmap-frame-size'; |
| |
| class ParsedArgs { |
| final CachingBehavior cachingBehavior; |
| final String buildDir; |
| final RegExp fileRegex; |
| final SourceLang onlyLang; |
| final IOSink output; |
| final OutputFormat format; |
| final int concurrency; |
| final List<QueryThunk> selectedQueries; |
| final File heatmap; |
| final int heatmapFrameSize; |
| |
| ParsedArgs( |
| {this.cachingBehavior, |
| this.buildDir, |
| this.fileRegex, |
| this.onlyLang, |
| this.output, |
| this.format, |
| this.concurrency, |
| this.selectedQueries, |
| this.heatmap, |
| this.heatmapFrameSize}); |
| } |