blob: 408e7dc6feff75516db50625dea44df79437ca65 [file] [log] [blame]
// Copyright (c) 2018, 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:convert';
import 'dart:io';
import 'dart:ffi';
import 'dart:async';
import 'package:expect/expect.dart';
import 'package:native_stack_traces/elf.dart';
import 'package:path/path.dart' as path;
import 'use_flag_test_helper.dart';
const int headerSize = 8;
final int compressedWordSize =
sizeOf<Pointer>() == 8 && !Platform.executable.contains('64C') ? 8 : 4;
const int wordSize = 8; // analyze_snapshot is not supported on arm32
// Used to ensure we don't have multiple equivalent calls to test.
final _seenDescriptions = <String>{};
Future<void> testAOT(
String dillPath, {
bool useAsm = false,
bool forceDrops = false,
bool stripUtil = false, // Note: forced true if useAsm.
bool stripFlag = false,
bool disassemble = false,
}) async {
const isProduct = const bool.fromEnvironment('dart.vm.product');
if (isProduct && disassemble) {
Expect.isFalse(disassemble, 'no use of disassembler in PRODUCT mode');
}
final analyzeSnapshot = path.join(buildDir, 'analyze_snapshot');
// For assembly, we can't test the sizes of the snapshot sections, since we
// don't have a Mach-O reader for Mac snapshots and for ELF, the assembler
// merges the text/data sections and the VM/isolate section symbols may not
// have length information. Thus, we force external stripping so we can test
// the approximate size of the stripped snapshot.
if (useAsm) {
stripUtil = true;
}
final descriptionBuilder = StringBuffer()..write(useAsm ? 'assembly' : 'elf');
if (forceDrops) {
descriptionBuilder.write('-dropped');
}
if (stripFlag) {
descriptionBuilder.write('-intstrip');
}
if (stripUtil) {
descriptionBuilder.write('-extstrip');
}
if (disassemble) {
descriptionBuilder.write('-disassembled');
}
final description = descriptionBuilder.toString();
Expect.isTrue(
_seenDescriptions.add(description),
"test configuration $description would be run multiple times",
);
await withTempDir('analyze_snapshot_binary-$description', (
String tempDir,
) async {
// Generate the snapshot
final snapshotPath = path.join(tempDir, 'test.snap');
final commonSnapshotArgs = [
if (stripFlag) '--strip', // gen_snapshot specific and not a VM flag.
if (forceDrops) ...[
'--dwarf-stack-traces',
'--no-retain-function-objects',
'--no-retain-code-objects',
],
if (disassemble) '--disassemble', // Not defined in PRODUCT mode.
dillPath,
];
if (useAsm) {
final assemblyPath = path.join(tempDir, 'test.S');
await (disassemble ? runSilent : run)(genSnapshot, <String>[
'--snapshot-kind=app-aot-assembly',
'--assembly=$assemblyPath',
...commonSnapshotArgs,
]);
await assembleSnapshot(assemblyPath, snapshotPath);
} else {
await (disassemble ? runSilent : run)(genSnapshot, <String>[
'--snapshot-kind=app-aot-elf',
'--elf=$snapshotPath',
...commonSnapshotArgs,
]);
}
print("Snapshot generated at $snapshotPath.");
// May not be ELF, but another format.
final elf = Elf.fromFile(snapshotPath);
if (!useAsm) {
Expect.isNotNull(elf);
}
if (elf != null) {
// Verify some ELF file format parameters.
final textSections = elf.namedSections(".text");
Expect.isNotEmpty(textSections);
Expect.isTrue(
textSections.length <= 2,
"More text sections than expected",
);
final dataSections = elf.namedSections(".rodata");
Expect.isNotEmpty(dataSections);
Expect.isTrue(
dataSections.length <= 2,
"More data sections than expected",
);
}
final analyzerOutputPath = path.join(tempDir, 'analyze_test.json');
// This will throw if exit code is not 0.
await run(analyzeSnapshot, <String>[
'--out=$analyzerOutputPath',
'$snapshotPath',
]);
final analyzerJsonBytes = await readFile(analyzerOutputPath);
final analyzerJson = json.decode(analyzerJsonBytes);
Expect.isFalse(analyzerJson.isEmpty);
Expect.isTrue(
analyzerJson.keys.toSet().containsAll([
'snapshot_data',
'objects',
'metadata',
]),
);
final objects = (analyzerJson['objects'] as List)
.map((o) => o as Map)
.toList();
final classes = objects.where((o) => o['type'] == 'Class').toList();
final classnames = <int, String>{};
final superclass = <int, int>{};
final implementedInterfaces = <int, List<int>>{};
for (final klass in classes) {
final id = klass['id'];
superclass[id] = klass['super_class'] as int;
classnames[id] = klass['name'];
implementedInterfaces[id] = [
for (final superTypeId in klass['interfaces'] as List? ?? [])
objects[superTypeId]['type_class']!,
];
}
// Find MethodChannel class.
final methodChannelId = classnames.entries
.singleWhere((e) => e.value == 'MethodChannel')
.key;
// Find string instance.
final stringList = objects
.where((o) => o['type'] == 'String' && o['value'] == 'constChannel1')
.toList();
Expect.isTrue(
stringList.length == 1,
'one "constChannel1" string must exist in output',
);
final int stringObjId = stringList.first['id'];
// Find MethodChannel instance.
final instanceList = objects
.where(
(o) =>
o['type'] == 'Instance' &&
o['class'] == methodChannelId &&
o['references'].contains(stringObjId),
)
.toList();
Expect.isTrue(instanceList.length == 1, '''one instance of MethodChannel
with reference to "constChannel1" must exist in output''');
// Test class hierarchy information
final myBaseClassId = classnames.entries
.singleWhere((e) => e.value == 'MyBase')
.key;
final mySubClassId = classnames.entries
.singleWhere((e) => e.value == 'MySub')
.key;
final myInterfaceClassId = classnames.entries
.singleWhere((e) => e.value == 'MyInterface')
.key;
Expect.equals(myBaseClassId, superclass[mySubClassId]);
Expect.equals(
myInterfaceClassId,
implementedInterfaces[mySubClassId]!.single,
);
// Ensure instance fields of classes are reported.
final baseClass =
objects[classnames.entries
.singleWhere((e) => e.value == 'FieldTestBase')
.key];
final subClass =
objects[classnames.entries
.singleWhere((e) => e.value == 'FieldTestSub')
.key];
final baseFieldIds = baseClass['fields'];
final baseFields = [for (final int id in baseFieldIds) objects[id]];
final baseSlots = baseClass['instance_slots']
.map<Map>((e) => e as Map)
.toList();
final subFieldIds = subClass['fields'];
final subFields = [for (final int id in subFieldIds) objects[id]];
final subSlots = subClass['instance_slots'];
// We have:
// class Base {
// static int baseS0 = int.parse('1');
// static int baseS1 = int.parse('2');
// int base0;
// double base1;
// Object? base2;
// Float32x4 base3;
// Float64x2 base4;
// }
//
// This static field is never tree shaken.
expectField(baseFields[0], name: 'baseS0', flags: ['static']);
expectField(baseFields[1], name: 'baseS1', flags: ['static']);
// Neighboring static fields should always be one word away
final int staticFieldOffset0 = baseFields[0]["static_field_offset"];
final int staticFieldOffset1 = baseFields[1]["static_field_offset"];
Expect.equals(staticFieldOffset1 - staticFieldOffset0, wordSize);
if (isProduct) {
// Most [Field] objests are tree shaken.
Expect.equals(2, baseFields.length);
int slotOffset = 0;
slotOffset += expectUnknown8Bytes(
baseSlots.skip(slotOffset),
offsetReferences: 0,
offsetBytes: 0,
);
slotOffset += expectUnknown8Bytes(
baseSlots.skip(slotOffset),
offsetReferences: 0,
offsetBytes: 8,
);
slotOffset += expectUnknownReference(
baseSlots[slotOffset],
offsetReferences: 0,
offsetBytes: 16,
);
slotOffset += expectUnknown16Bytes(
baseSlots.skip(slotOffset),
offsetReferences: 1,
offsetBytes: 16,
);
slotOffset += expectUnknown16Bytes(
baseSlots.skip(slotOffset),
offsetReferences: 1,
offsetBytes: 32,
);
} else {
// We don't tree shake [Field] objects in non-product builds.
Expect.equals(7, baseFields.length);
expectField(
baseFields[2],
name: 'base0',
isReference: false,
unboxedType: 'int',
);
expectField(
baseFields[3],
name: 'base1',
isReference: false,
unboxedType: 'double',
);
expectField(baseFields[4], name: 'base2');
expectField(
baseFields[5],
name: 'base3',
isReference: false,
unboxedType: 'Float32x4',
);
expectField(
baseFields[6],
name: 'base4',
isReference: false,
unboxedType: 'Float64x2',
);
int slotOffset = 0;
slotOffset += expectInstanceSlot(
baseSlots[slotOffset],
offsetReferences: 0,
offsetBytes: 0,
isReference: false,
fieldId: baseFieldIds[2],
);
slotOffset += expectInstanceSlot(
baseSlots[slotOffset],
offsetReferences: 0,
offsetBytes: 8,
isReference: false,
fieldId: baseFieldIds[3],
);
slotOffset += expectInstanceSlot(
baseSlots[slotOffset],
offsetReferences: 0,
offsetBytes: 16,
fieldId: baseFieldIds[4],
);
slotOffset += expectInstanceSlot(
baseSlots[slotOffset],
offsetReferences: 1,
offsetBytes: 16,
isReference: false,
fieldId: baseFieldIds[5],
);
slotOffset += expectInstanceSlot(
baseSlots[slotOffset],
offsetReferences: 1,
offsetBytes: 32,
isReference: false,
fieldId: baseFieldIds[6],
);
}
// We have:
// class Sub<T> extends Base{
// late int subL1 = int.parse('1');
// late final double subL2 = double.parse('1.2');
// }
// These late instance fields are never tree shaken.
expectField(subFields[0], name: 'subL1', flags: ['late']);
expectField(subFields[1], name: 'subL2', flags: ['final', 'late']);
expectTypeArgumentsSlot(
subSlots[0],
offsetReferences: 1,
offsetBytes: 8 + 8 + 16 + 16,
);
expectSlot(
subSlots[1],
offsetReferences: 2,
offsetBytes: 8 + 8 + 16 + 16,
type: 'instance_field',
fieldId: subFieldIds[0],
);
expectSlot(
subSlots[2],
offsetReferences: 3,
offsetBytes: 8 + 8 + 16 + 16,
type: 'instance_field',
fieldId: subFieldIds[1],
);
Expect.isTrue(
analyzerJson['metadata'].containsKey('analyzer_version'),
'snapshot analyzer version must be reported',
);
Expect.isTrue(
analyzerJson['metadata']['analyzer_version'] == 2,
'invalid snapshot analyzer version',
);
});
}
void expectField(
Map fieldJson, {
required String name,
List<String> flags = const [],
bool isReference = true,
String? unboxedType,
}) {
Expect.equals(name, fieldJson['name']);
Expect.equals(isReference, fieldJson['is_reference']);
Expect.listEquals(flags, fieldJson['flags']);
if (unboxedType != null) {
Expect.equals(unboxedType, fieldJson['unboxed_type']);
} else {
Expect.isFalse(fieldJson.containsKey('unboxed_type'));
}
}
void expectSlot(
Map slotJson, {
required int offsetReferences,
required int offsetBytes,
required String type,
bool isReference = true,
int? fieldId,
}) {
Expect.equals(type, slotJson['slot_type']);
if (fieldId != null) {
Expect.equals(fieldId, slotJson['field']);
} else {
Expect.isFalse(slotJson.containsKey('field'));
}
final int offset =
headerSize + offsetReferences * compressedWordSize + offsetBytes;
Expect.equals(offset, slotJson['offset']);
Expect.equals(isReference, slotJson['is_reference']);
}
int expectInstanceSlot(
Map slotJson, {
required int offsetReferences,
required int offsetBytes,
bool isReference = true,
int? fieldId,
}) {
expectSlot(
slotJson,
isReference: isReference,
fieldId: fieldId,
offsetReferences: offsetReferences,
offsetBytes: offsetBytes,
type: 'instance_field',
);
return 1;
}
int expectUnknownReference(
Map slotJson, {
required int offsetReferences,
required int offsetBytes,
}) {
expectSlot(
slotJson,
offsetReferences: offsetReferences,
offsetBytes: offsetBytes,
type: 'unknown_slot',
);
return 1;
}
int expectTypeArgumentsSlot(
Map slotJson, {
required int offsetReferences,
required int offsetBytes,
}) {
expectSlot(
slotJson,
offsetReferences: offsetReferences,
offsetBytes: offsetBytes,
type: 'type_arguments_field',
);
return 1;
}
int expectUnknown8Bytes(
Iterable<Map> slotJson, {
required int offsetReferences,
required int offsetBytes,
}) {
final it = slotJson.iterator;
Expect.isTrue(it.moveNext());
expectSlot(
it.current,
isReference: false,
offsetReferences: offsetReferences,
offsetBytes: offsetBytes,
type: 'unknown_slot',
);
if (compressedWordSize == 8) {
return 1;
}
Expect.isTrue(it.moveNext());
expectSlot(
it.current,
isReference: false,
offsetReferences: offsetReferences,
offsetBytes: offsetBytes + compressedWordSize,
type: 'unknown_slot',
);
return 2;
}
int expectUnknown16Bytes(
Iterable<Map> slotJson, {
required int offsetReferences,
required int offsetBytes,
}) {
int slots = 0;
slots += expectUnknown8Bytes(
slotJson,
offsetReferences: offsetReferences,
offsetBytes: offsetBytes,
);
slots += expectUnknown8Bytes(
slotJson.skip(slots),
offsetReferences: offsetReferences,
offsetBytes: offsetBytes + 8,
);
return slots;
}
main() async {
void printSkip(String description) => print(
'Skipping $description for ${path.basename(buildDir)} '
'on ${Platform.operatingSystem}' +
(clangBuildToolsDir == null ? ' without //buildtools' : ''),
);
// We don't have access to the SDK on Android.
if (Platform.isAndroid) {
printSkip('all tests');
return;
}
await withTempDir('analyze_snapshot_binary', (String tempDir) async {
// We only need to generate the dill file once for all JIT tests.
final thisTestPath = path.join(
sdkDir,
'runtime',
'tests',
'vm',
'dart',
'analyze_snapshot_program.dart',
);
// We only need to generate the dill file once for all AOT tests.
final aotDillPath = path.join(tempDir, 'aot_test.dill');
await run(genKernel, <String>[
'--aot',
'--platform',
platformDill,
...Platform.executableArguments.where(
(arg) =>
arg.startsWith('--enable-experiment=') ||
arg == '--sound-null-safety' ||
arg == '--no-sound-null-safety',
),
'-o',
aotDillPath,
thisTestPath,
]);
// Just as a reminder for AOT tests:
// * If useAsm is true, then stripUtil is forced (as the assembler may add
// extra information that needs stripping), so no need to specify
// stripUtil for useAsm tests.
await Future.wait([
// Test unstripped ELF generation directly.
testAOT(aotDillPath),
testAOT(aotDillPath, forceDrops: true),
// Test flag-stripped ELF generation.
testAOT(aotDillPath, stripFlag: true),
]);
// Since we can't force disassembler support after the fact when running
// in PRODUCT mode, skip any --disassemble tests. Do these tests last as
// they have lots of output and so the log will be truncated.
if (!const bool.fromEnvironment('dart.vm.product')) {
// Regression test for dartbug.com/41149.
await Future.wait([testAOT(aotDillPath, disassemble: true)]);
}
// Test unstripped ELF generation that is then externally stripped.
await Future.wait([testAOT(aotDillPath, stripUtil: true)]);
// Dont test assembled snapshot for simulated platforms or macos
if (!buildDir.endsWith("SIMARM64") &&
!buildDir.endsWith("SIMARM64C") &&
!Platform.isMacOS) {
await Future.wait([
// Test unstripped assembly generation that is then externally stripped.
testAOT(aotDillPath, useAsm: true),
// Test stripped assembly generation that is then externally stripped.
testAOT(aotDillPath, useAsm: true, stripFlag: true),
]);
}
});
}
Future<String> readFile(String file) {
return new File(file).readAsString();
}