blob: c32ab19e0170139d5d163013da9f15ad9ee34fe4 [file] [log] [blame]
// 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});
}