blob: fcc342c4d5d6801285add347f7d3d0752fec7a0c [file] [log] [blame]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'dart:async';
import 'package:args/args.dart';
import 'package:heap_snapshot/analysis.dart';
import 'package:heap_snapshot/format.dart';
import 'package:mmap/mmap.dart';
import 'package:vm_service/vm_service.dart';
import 'completion.dart';
import 'expression.dart';
import 'load.dart';
export 'expression.dart' show Output;
abstract class Command {
String get name;
String get description;
String get usage;
List<String> get nameAliases => const [];
final ArgParser argParser = ArgParser();
Future execute(CliState state, List<String> allArgs) async {
try {
int startOfRest = 0;
while (startOfRest < allArgs.length &&
allArgs[startOfRest].startsWith('-')) {
startOfRest++;
}
final options = argParser.parse(allArgs.take(startOfRest).toList());
final args = allArgs.skip(startOfRest).toList();
await executeInternal(state, options, args);
} catch (e, s) {
state.output.print('An error occurred: $e\n$s');
printUsage(state);
}
}
Future executeInternal(CliState state, ArgResults options, List<String> args);
void printUsage(CliState state) {
if (nameAliases.isEmpty) {
state.output.print('Usage for $name:');
} else {
state.output
.print('Usage for $name (aliases: ${nameAliases.join(' ')}):');
}
state.output.print(' $usage');
}
String? completeCommand(CliState state, String text) {
return null;
}
String? _completeExpression(CliState state, text) {
if (!state.isInitialized) return null;
final output = CompletionCollector();
parseExpression(text, output, state.namedSets.names.toSet());
return output.suggestedCompletion;
}
String? _completeOptions(String text) {
final pc = PostfixCompleter(text);
final lastWord = getLastWord(text);
if (lastWord.isEmpty || !lastWord.startsWith('-')) return null;
if (!lastWord.startsWith('--')) {
// For only one `-` we prefer to complete with abbreviated options.
final options = argParser.options.values
.where((o) => o.abbr != null)
.map((o) => '-' + o.abbr!)
.toList();
return pc.tryComplete(lastWord, options);
}
final options = argParser.options.values
.expand((o) => [o.name, ...o.aliases])
.map((o) => '--$o')
.toList();
return pc.tryComplete(lastWord, options);
}
}
abstract class SnapshotCommand extends Command {
SnapshotCommand();
Future executeInternal(
CliState state, ArgResults options, List<String> args) async {
if (!state.isInitialized) {
state.output.print('No `*.heapsnapshot` loaded. See `help load`.');
return;
}
await executeSnapshotCommand(state, options, args);
}
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args);
}
class LoadCommand extends Command {
final name = 'load';
final description = 'Loads a *.heapsnapshot produced by the Dart VM.';
final usage = 'load <file.heapsnapshot>';
LoadCommand();
Future executeInternal(
CliState state, ArgResults options, List<String> args) async {
if (args.length != 1) {
printUsage(state);
return;
}
final url = args.single;
if (url.startsWith('http') || url.startsWith('ws')) {
try {
final chunks = await loadFromUri(Uri.parse(url));
state.initialize(Analysis(HeapSnapshotGraph.fromChunks(chunks)));
state.output.print('Loaded heapsnapshot from "$url".');
} catch (e) {
state.output.print('Could not load heapsnapshot from "$url".');
}
return;
}
final filename = url.startsWith('~/')
? (Platform.environment['HOME']! + url.substring(1))
: url;
if (!File(filename).existsSync()) {
state.output.print('File "$filename" doesn\'t exist.');
return;
}
try {
final bytes = mmapOrReadFileSync(filename);
state.initialize(
Analysis(HeapSnapshotGraph.fromChunks([bytes.buffer.asByteData()])));
state.output.print('Loaded heapsnapshot from "$filename".');
} catch (e) {
state.output.print('Could not load heapsnapshot from "$filename".');
return;
}
}
String? completeCommand(CliState state, String text) {
return tryCompleteFileSystemEntity(
text, (filename) => filename.endsWith('.heapsnapshot'));
}
}
class StatsCommand extends SnapshotCommand {
final name = 'stats';
final description = 'Calculates statistics about a set of objects.';
final usage = 'stats [-n/--max=NUM] [-c/--sort-by-count] <expr> ';
final nameAliases = ['stat'];
StatsCommand() {
argParser.addFlag('sort-by-count',
abbr: 'c',
help: 'Sorts by count (instead of size).',
defaultsTo: false);
argParser.addOption('max',
abbr: 'n',
help: 'Limits the number of lines to be printed.',
defaultsTo: '20');
}
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
final oids = parseAndEvaluate(
state.namedSets, state.analysis, args.join(' '), state.output);
if (oids == null) return;
final sortByCount = options['sort-by-count'] as bool;
final lines = int.parse(options['max']!);
final stats =
state.analysis.generateObjectStats(oids, sortBySize: !sortByCount);
state.output.print(formatHeapStats(stats, maxLines: lines));
}
String? completeCommand(CliState state, String text) {
return _completeOptions(text) ?? _completeExpression(state, text);
}
}
class DataStatsCommand extends SnapshotCommand {
final name = 'dstats';
final description =
'Calculates statistics about the data portion of objects.';
final usage = 'dstats [-n/--max=NUM] [-c/--sort-by-count] <expr> ';
final nameAliases = ['dstat'];
DataStatsCommand() {
argParser.addFlag('sort-by-count',
abbr: 'c', help: 'Sort by count', defaultsTo: false);
argParser.addOption('max',
abbr: 'n',
help: 'Limits the number of max to be printed.',
defaultsTo: '20');
}
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
final oids = parseAndEvaluate(
state.namedSets, state.analysis, args.join(' '), state.output);
if (oids == null) return;
final sortByCount = options['sort-by-count'] as bool;
final lines = int.parse(options['max']!);
final stats =
state.analysis.generateDataStats(oids, sortBySize: !sortByCount);
state.output.print(formatDataStats(stats, maxLines: lines));
}
String? completeCommand(CliState state, String text) {
return _completeOptions(text) ?? _completeExpression(state, text);
}
}
class InfoCommand extends SnapshotCommand {
final name = 'info';
final description = 'Prints the known named sets.';
final usage = 'info';
InfoCommand();
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
if (args.length != 0) {
printUsage(state);
return;
}
state.output.print('Known named sets:');
final table = Table();
state.namedSets.forEach((String name, IntSet oids) {
table.addRow([name, '{#${oids.length}}']);
});
state.output.print(indent(' ', table.asString));
}
}
class ClearCommand extends SnapshotCommand {
final name = 'clear';
final description = 'Clears a specific named set (or all).';
final usage = 'clear <name>*';
ClearCommand();
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
if (args.isEmpty) {
state.namedSets.clearWhere((key) => key != 'roots');
return;
}
for (final arg in args) {
state.namedSets.clear(arg);
return;
}
}
}
class RetainingPathCommand extends SnapshotCommand {
final name = 'retainers';
final description = 'Prints information about retaining paths.';
final usage = 'retainers [-d/--depth=<num>] [-n/--max=NUM] <expr>';
final nameAliases = ['retain'];
RetainingPathCommand() {
argParser.addOption('depth',
abbr: 'd', help: 'Maximum depth of retaining paths.', defaultsTo: '10');
argParser.addOption('max',
abbr: 'n',
help: 'Limits the number of entries printed.',
defaultsTo: '3');
}
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
final oids = parseAndEvaluate(
state.namedSets, state.analysis, args.join(' '), state.output);
if (oids == null) return;
final depth = int.parse(options['depth']!);
final maxEntries = int.parse(options['max']!);
final paths = state.analysis.retainingPathsOf(oids, depth);
for (int i = 0; i < paths.length; ++i) {
if (maxEntries != -1 && i >= maxEntries) break;
final path = paths[i];
state.output.print('There are ${path.count} retaining paths of');
state.output.print(formatRetainingPath(state.analysis.graph, paths[i]));
state.output.print('');
}
}
String? completeCommand(CliState state, String text) {
return _completeOptions(text) ?? _completeExpression(state, text);
}
}
class ExamineCommand extends SnapshotCommand {
final name = 'examine';
final description = 'Examins a set of objects.';
final usage = 'examine [-n/--max=NUM] <expr>?';
final nameAliases = ['x'];
ExamineCommand() {
argParser.addOption('max',
abbr: 'n',
help: 'Limits the number of entries to be examined..',
defaultsTo: '5');
}
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
final limit = int.parse(options['max']!);
final oids = parseAndEvaluate(
state.namedSets, state.analysis, args.join(' '), state.output);
if (oids == null) return;
if (oids.isEmpty) return;
final it = oids.iterator;
int i = 0;
while (it.moveNext()) {
final oid = it.current;
final info = state.analysis.examine(oid);
state.output.print('${info.className}@$oid (${info.libraryUri}) {');
final table = Table();
info.fieldValues.forEach((name, value) {
table.addRow([name, value]);
});
state.output.print(indent(' ', table.asString));
state.output.print('}');
if (++i >= limit) break;
}
}
String? completeCommand(CliState state, String text) {
return _completeOptions(text) ?? _completeExpression(state, text);
}
}
class EvaluateCommand extends SnapshotCommand {
final name = 'eval';
final description = 'Evaluates a set expression.';
final usage = 'eval <expr>\n\n$dslDescription';
EvaluateCommand();
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
final sexpr = parseExpression(
args.join(' '), state.output, state.namedSets.names.toSet());
if (sexpr == null) return null;
final oids = sexpr.evaluate(state.namedSets, state.analysis, state.output);
if (oids == null) return null;
late final String name;
if (sexpr is SetNameExpression) {
name = sexpr.name;
} else {
name = state.namedSets.nameSet(oids);
}
state.output.print(' $name {#${oids.length}}');
}
String? completeCommand(CliState state, String text) {
return _completeExpression(state, text);
}
}
class DescFilterCommand extends SnapshotCommand {
final name = 'describe-filter';
final description = 'Describes what a filter expression will match.';
final usage = 'describe-filter $dslFilter';
final nameAliases = ['desc-filter', 'desc'];
DescFilterCommand();
Future executeSnapshotCommand(
CliState state, ArgResults options, List<String> args) async {
final tfilter = state.analysis.parseTraverseFilter(args);
if (tfilter == null) return null;
state.output.print(tfilter.asString(state.analysis.graph));
}
}
class HelpCommand extends Command {
final name = 'help';
final description = 'Shows general help or help for a specific command.';
final usage = 'help <command>?';
final nameAliases = ['h'];
HelpCommand();
Future executeInternal(
CliState state, ArgResults options, List<String> args) async {
if (args.length >= 1) {
final helpCommandName = args[0];
final helpCommand = cliCommandRunner.name2command[helpCommandName];
if (helpCommand != null) {
helpCommand.printUsage(state);
return;
}
}
final table = Table();
cliCommandRunner.name2command.forEach((name, command) {
if (name == command.name) {
table.addRow([command.name, command.description]);
}
});
state.output.print('Available commands:');
state.output.print(indent(' ', table.asString));
}
String? completeCommand(CliState state, String text) {
if (text.indexOf(' ') == -1) {
final pc = PostfixCompleter(text);
final possibleCommands = cliCommandRunner.name2command.keys.toList();
return pc.tryComplete(text, possibleCommands);
}
return null;
}
}
class ExitCommand extends Command {
final name = 'exit';
final description = 'Exits the program.';
final usage = 'exit';
final nameAliases = ['quit', 'q'];
ExitCommand();
Future executeInternal(
CliState state, ArgResults options, List<String> args) {
throw 'unreachable';
}
}
class CommandRunner {
final Command defaultCommand;
final Map<String, Command> name2command = {};
CommandRunner(List<Command> commands, this.defaultCommand) {
for (final command in commands) {
name2command[command.name] = command;
for (final alias in command.nameAliases) {
name2command[alias] = command;
}
}
}
/// Returns `true` if the CLI tool should exit and `false` otherwise.
Future<bool> run(CliState state, List<String> args) async {
if (args.isEmpty) return false;
final commandName = args.first;
final command = name2command[commandName];
if (command != null) {
if (command is ExitCommand) {
return true;
}
await command.execute(state, args.skip(1).toList());
return false;
}
await defaultCommand.execute(state, args);
return false;
}
String? completeCommand(CliState state, String text) {
// We only complete commands, no arguments (yet).
if (text.isEmpty) return null;
final left = getFirstWordWithSpaces(text);
if (left.endsWith(' ')) {
final command = name2command[left.trim()];
if (command != null) {
final right = text.substring(left.length);
final result = command.completeCommand(state, right);
return (result != null) ? (left + result) : null;
}
} else {
final pc = PostfixCompleter(text);
final possibleCommands = name2command.keys
.where((name) =>
state.isInitialized || name2command[name] is! SnapshotCommand)
.toList();
final completion = pc.tryComplete(text, possibleCommands);
if (completion != null) return completion;
}
return defaultCommand.completeCommand(state, text);
}
}
class CliState {
NamedSets? _namedSets;
Analysis? _analysis;
Output output;
CliState(this.output);
void initialize(Analysis analysis) {
_analysis = analysis;
_namedSets = NamedSets();
_namedSets!.nameSet(analysis.roots, 'roots');
}
bool get isInitialized => _analysis != null;
Analysis get analysis => _analysis!;
NamedSets get namedSets => _namedSets!;
}
final cliCommandRunner = CommandRunner([
LoadCommand(),
StatsCommand(),
DataStatsCommand(),
InfoCommand(),
ClearCommand(),
RetainingPathCommand(),
EvaluateCommand(),
ExamineCommand(),
DescFilterCommand(),
HelpCommand(),
ExitCommand(),
], EvaluateCommand());
class CompletionCollector extends Output {
String? suggestedCompletion;
void suggestCompletion(String text) {
suggestedCompletion = text;
}
}