blob: 7b1920ba9188f4fc2d769b9f8cf4715455441417 [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:typed_data';
import 'package:heapsnapshot/src/cli.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'utils.dart';
class ErrorCollector extends Output {
final errors = <String>[];
final output = <String>[];
final all = <String>[];
void printError(String error) {
errors.add(error);
all.add(error);
}
void print(String message) {
output.add(message);
all.add(message);
}
void clear() {
errors.clear();
output.clear();
all.clear();
}
String get log => all.join('\n');
}
main([List<String> args = const []]) {
if (!args.isEmpty) {
// We're in the child.
if (args.single != '--child') throw 'failed';
// Force initialize of the data we want in the heapsnapshot.
print(global.use);
print(weakTest.use);
if (supportsExternalTypedDataTest) {
print(externalTypedData1234567.length);
}
print('Child ready');
return;
}
group('cli', () {
late Testee testee;
late String testeeUrl;
late Directory snapshotDir;
late String heapsnapshotFile;
late ErrorCollector errorCollector;
late CliState cliState;
setUpAll(() async {
snapshotDir = Directory.systemTemp.createTempSync('snapshot');
heapsnapshotFile = path.join(snapshotDir.path, 'current.heapsnapshot');
testee = Testee('test/cli_test.dart');
testeeUrl = await testee.start(['--child']);
await testee.getHeapsnapshotAndWriteTo(heapsnapshotFile);
errorCollector = ErrorCollector();
cliState = CliState(errorCollector);
});
tearDownAll(() async {
snapshotDir.deleteSync(recursive: true);
await testee.close();
});
late String log;
Future run(String commandString) async {
final args = commandString.split(' ').where((p) => !p.isEmpty).toList();
print('-----------------------');
print('Running: $commandString');
await cliCommandRunner.run(cliState, args);
log = errorCollector.log;
print(log.trim());
errorCollector.clear();
}
expectLog(String expected) {
expect(log.trim(), expected.trim());
}
expectLogPattern(String pattern) {
final logLines = log
.split('\n')
.map((p) => p.trim())
.where((p) => !p.isEmpty)
.toList();
final patternLines = pattern
.split('\n')
.map((p) => p.trim())
.where((p) => !p.isEmpty)
.toList();
if (logLines.length != patternLines.length) {
print('Expected pattern:');
print(' ' + patternLines.join('\n '));
print('But got:');
print(' ' + logLines.join('\n '));
}
for (int i = 0; i < logLines.length; ++i) {
final log = logLines[i];
final pattern = patternLines[i];
if (!RegExp(pattern).hasMatch(log)) {
print('[$i] $log does not match pattern "$pattern"');
expect(false, true);
}
}
}
const sp = r'\{#\d+\}';
test('cli commands', () async {
// Test loading from Uri & search for the Global object.
await run('load $testeeUrl');
expectLog('Loaded heapsnapshot from "$testeeUrl".');
await run('stat filter (closure roots) Global');
expectLogPattern(r'''
size count class
-------- -------- --------
0 kb 1 Global .*/cli_test.dart
''');
// Test loading from file & do remaining tests.
await run('load $heapsnapshotFile');
expectLog('Loaded heapsnapshot from "$heapsnapshotFile".');
await run('info');
expectLogPattern('''
Known named sets:
roots $sp
''');
await run('eval all = closure roots');
expectLogPattern('all $sp');
await run('info');
expectLogPattern('''
Known named sets:
roots $sp
all $sp
''');
await run('global = filter all Global');
expectLogPattern(r'global \{#1\}');
await run('stats global');
expectLogPattern('''
size *count *class
-------- *-------- *--------
0 kb *1 *Global .*cli_test.dart
''');
await run('dstats -c filter (closure global) String');
expectLogPattern('''
size unique-size count class data
-------- -------- -------- -------- --------
0 kb 0 kb 4 _OneByteString #nonSharedString#
0 kb 0 kb 2 _OneByteString #fooUniqueString
0 kb 0 kb 2 _OneByteString #barUniqueString
0 kb 0 kb 1 _OneByteString #sharedString
-------- -------- --------
0 kb 0 kb 9
''');
await run('lists = filter (closure global) _List');
expectLogPattern('lists $sp');
await run('dstats -c lists');
expectLogPattern('''
size unique-size count class data
-------- -------- -------- -------- --------
0 kb 0 kb 1 _List len:0
0 kb 0 kb 1 _List len:0
0 kb 0 kb 1 _List len:2
0 kb 0 kb 1 _List len:1
0 kb 0 kb 1 _List len:1
0 kb 0 kb 1 _List len:2
-------- -------- --------
0 kb 0 kb 6
''');
if (supportsExternalTypedDataTest) {
await run('etd = dfilter (filter all _ExternalUint8Array) ==1234567');
expectLogPattern('etd $sp');
await run('stats etd');
expectLogPattern('''
size count class
-------- -------- --------
1205 kb 1 _ExternalUint8Array dart:typed_data
''');
await run('dstats etd');
expectLogPattern('''
size unique-size count class data
-------- -------- -------- -------- --------
1205 kb 1205 kb 1 _ExternalUint8Array len:1234567
''');
}
await run(
'stats foobar = (follow (follow global) ^:type_arguments ^Root ^Smi)');
expectLogPattern('''
size count class
-------- -------- --------
0 kb 2 Foo .*cli_test.dart
0 kb 2 Bar .*cli_test.dart
-------- --------
0 kb 4
''');
await run('examine users foobar');
expectLogPattern(r'''
_List@\d+ .* {
type_arguments_
length_
\[0\] *Foo@\d+ .*/cli_test.dart
\[1\] *Foo@\d+ .*/cli_test.dart
}
_List@\d+ .* {
type_arguments_
length_
\[0\] *Bar@\d+ .*/cli_test.dart
\[1\] *Bar@\d+ .*/cli_test.dart
}
''');
await run('examine users users foobar');
expectLogPattern(r'''
Global@\d+ .*/cli_test.dart.* {
foos _List@\d+
bars _List@\d+
}
''');
await run('users (follow global :bars)');
await run('retainers -n10 filter (closure global) String');
expectLogPattern(r'''
There are 2 retaining paths of
_OneByteString
⮑ ・Foo.fooLocal .*/cli_test.dart
⮑ ・_List
⮑ ・Global.foos .*/cli_test.dart
⮑ ・Isolate.global
⮑ ・Root
There are 2 retaining paths of
_OneByteString
⮑ ・Foo.fooUnique .*/cli_test.dart
⮑ ・_List
⮑ ・Global.foos .*/cli_test.dart
⮑ ・Isolate.global
⮑ ・Root
There are 2 retaining paths of
_OneByteString
⮑ ・Bar.barLocal .*/cli_test.dart
⮑ ・_List
⮑ ・Global.bars .*/cli_test.dart
⮑ ・Isolate.global
⮑ ・Root
There are 2 retaining paths of
_OneByteString
⮑ ・Bar.barUnique .*/cli_test.dart
⮑ ・_List
⮑ ・Global.bars .*/cli_test.dart
⮑ ・Isolate.global
⮑ ・Root
There are 1 retaining paths of
_OneByteString
⮑ ﹢Foo.fooShared .*/cli_test.dart
⮑ ・_List
⮑ ・Global.foos .*/cli_test.dart
⮑ ・Isolate.global
⮑ ・Root
''');
await run('describe-filter Foo:List ^Bar');
expectLogPattern(r'''
The traverse filter expression "Foo:List \^Bar" matches:
\[\-\] Bar
\[ \] Foo
\[\+\] \.fooList0
''');
await run('weakly-held-object = follow (filter all WeakTest) :object');
await run('stats uclosure weakly-held-object');
expectLogPattern(r'''
size count class
-------- -------- --------
0 kb 1 WeakTest .*/cli_test.dart
0 kb 1 Object dart:core
0 kb 1 Root
0 kb 1 Isolate
-------- --------
0 kb 4
''');
});
});
}
final global = Global();
var marker = '|';
var sharedString = 'x';
class Foo {
final fooShared = sharedString;
final fooLocal = marker + 'nonSharedString' + marker;
final fooUnique = marker + 'fooUniqueString';
final fooList0 = List.filled(0, null);
String get use => 'Foo($fooShared, $fooLocal, $fooUnique, $fooList0)';
}
class Bar {
final barShared = sharedString;
final barLocal = marker + 'nonSharedString' + marker;
final barUnique = marker + 'barUniqueString';
final barList1 = List.filled(1, null);
String get use => 'Bar($barShared, $barLocal, $barUnique, $barList1)';
}
class Global {
late final foos;
late final bars;
Global() {
marker = '#';
sharedString = marker + 'sharedString';
foos = [Foo(), Foo()].toList(growable: false);
bars = [Bar(), Bar()].toList(growable: false);
sharedString = '';
}
String get use =>
'${foos.map((l) => l.use).toList()}|${bars.map((l) => l.use).toList()}|$sharedString';
}
// In order to create external typed data, we rely on the fact that dart:io will
// produce `Uint8List`s with external typed data if reading files that will not
// report their length (such as /dev/zero).
final bool supportsExternalTypedDataTest = File('/dev/zero').existsSync();
final Uint8List externalTypedData1234567 =
File('/dev/zero').openSync().readSync(1234567);
final weakTest = WeakTest(Object());
class WeakTest {
final Object object;
final List<WeakReference<Object>> weakList;
final Finalizer finalizer;
WeakTest(this.object)
: weakList = List.filled(1, WeakReference(object)),
finalizer = Finalizer((_) {})..attach(object, Object(), detach: object);
String get use => '$object|$weakList|$finalizer';
}