| // 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 'package:expect/expect.dart'; |
| import 'package:native_stack_traces/elf.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:vm_snapshot_analysis/v8_profile.dart'; |
| |
| import 'use_flag_test_helper.dart'; |
| |
| // Used to ensure we don't have multiple equivalent calls to test. |
| final _seenDescriptions = <String>{}; |
| |
| Snapshot testProfile(String profilePath) { |
| final profile = |
| Snapshot.fromJson(jsonDecode(File(profilePath).readAsStringSync())); |
| |
| // Verify that there are no "unknown" nodes. These are emitted when we see a |
| // reference to an some object but no other metadata about the object was |
| // recorded. We should at least record the type for every object in the |
| // graph (in some cases the shallow size can legitimately be 0, e.g. for |
| // "base objects" not written to the snapshot or artificial nodes). |
| for (final node in profile.nodes) { |
| Expect.notEquals("Unknown", node.type, "unknown node ${node}"); |
| } |
| |
| final root = profile.nodeAt(0); |
| final reachable = <Node>{}; |
| |
| // HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js) |
| // assumes that the graph root has at most one edge to any other node |
| // (most likely an oversight). |
| for (final edge in root.edges) { |
| Expect.isTrue( |
| reachable.add(edge.target), |
| "root\n\n$root\n\nhas multiple edges to node\n\n${edge.target}:\n\n" |
| "${root.edges.where((e) => e.target == edge.target).toList()}"); |
| } |
| |
| // Check that all other nodes are reachable from the root. |
| final stack = <Node>[...reachable]; |
| while (!stack.isEmpty) { |
| final next = stack.removeLast(); |
| for (final edge in next.edges) { |
| if (reachable.add(edge.target)) { |
| stack.add(edge.target); |
| } |
| } |
| } |
| |
| final unreachable = |
| profile.nodes.skip(1).where((Node n) => !reachable.contains(n)).toSet(); |
| Expect.isEmpty(unreachable); |
| |
| return profile; |
| } |
| |
| Future<void> testJIT(String dillPath) async { |
| final description = 'jit'; |
| Expect.isTrue(_seenDescriptions.add(description), |
| "test configuration $description would be run multiple times"); |
| |
| await withTempDir('v8-snapshot-profile-$description', (String tempDir) async { |
| // Generate the snapshot profile. |
| final profilePath = path.join(tempDir, 'profile.heapsnapshot'); |
| final vmTextPath = path.join(tempDir, 'vm_instructions.bin'); |
| final isolateTextPath = path.join(tempDir, 'isolate_instructions.bin'); |
| final vmDataPath = path.join(tempDir, 'vm_data.bin'); |
| final isolateDataPath = path.join(tempDir, 'isolate_data.bin'); |
| |
| await run(genSnapshot, <String>[ |
| '--snapshot-kind=core-jit', |
| '--vm_snapshot_instructions=$vmTextPath', |
| '--isolate_snapshot_instructions=$isolateTextPath', |
| '--vm_snapshot_data=$vmDataPath', |
| '--isolate_snapshot_data=$isolateDataPath', |
| "--write-v8-snapshot-profile-to=$profilePath", |
| dillPath, |
| ]); |
| |
| print("Snapshot profile generated at $profilePath."); |
| |
| final profile = testProfile(profilePath); |
| |
| // Verify that the total size of the snapshot text and data sections is |
| // the same as the sum of the shallow sizes of all objects in the profile. |
| // This ensures that all bytes are accounted for in some way. |
| final actualSize = await File(vmTextPath).length() + |
| await File(isolateTextPath).length() + |
| await File(vmDataPath).length() + |
| await File(isolateDataPath).length(); |
| final expectedSize = |
| profile.nodes.fold<int>(0, (size, n) => size + n.selfSize); |
| |
| Expect.equals(expectedSize, actualSize, "failed on $description snapshot"); |
| }); |
| } |
| |
| Future<void> testAOT(String dillPath, |
| {bool useAsm = false, |
| bool useBare = true, |
| bool forceDrops = false, |
| bool useDispatch = true, |
| bool stripUtil = false, // Note: forced true if useAsm. |
| bool stripFlag = false, |
| bool disassemble = false}) async { |
| if (const bool.fromEnvironment('dart.vm.product') && disassemble) { |
| Expect.isFalse(disassemble, 'no use of disassembler in PRODUCT mode'); |
| } |
| |
| // 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 (!useBare) { |
| descriptionBuilder.write('-nonbare'); |
| } |
| if (forceDrops) { |
| descriptionBuilder.write('-dropped'); |
| } |
| if (!useDispatch) { |
| descriptionBuilder.write('-nodispatch'); |
| } |
| 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('v8-snapshot-profile-$description', (String tempDir) async { |
| // Generate the snapshot profile. |
| final profilePath = path.join(tempDir, 'profile.heapsnapshot'); |
| final snapshotPath = path.join(tempDir, 'test.snap'); |
| final commonSnapshotArgs = [ |
| if (stripFlag) '--strip', // gen_snapshot specific and not a VM flag. |
| useBare ? '--use-bare-instructions' : '--no-use-bare-instructions', |
| "--write-v8-snapshot-profile-to=$profilePath", |
| if (forceDrops) ...[ |
| '--dwarf-stack-traces', |
| '--no-retain-function-objects', |
| '--no-retain-code-objects' |
| ], |
| useDispatch ? '--use-table-dispatch' : '--no-use-table-dispatch', |
| if (disassemble) '--disassemble', // Not defined in PRODUCT mode. |
| dillPath, |
| ]; |
| |
| if (useAsm) { |
| final assemblyPath = path.join(tempDir, 'test.S'); |
| |
| await run(genSnapshot, <String>[ |
| '--snapshot-kind=app-aot-assembly', |
| '--assembly=$assemblyPath', |
| ...commonSnapshotArgs, |
| ]); |
| |
| await assembleSnapshot(assemblyPath, snapshotPath); |
| } else { |
| await run(genSnapshot, <String>[ |
| '--snapshot-kind=app-aot-elf', |
| '--elf=$snapshotPath', |
| ...commonSnapshotArgs, |
| ]); |
| } |
| |
| String strippedPath; |
| if (stripUtil) { |
| strippedPath = snapshotPath + '.stripped'; |
| await stripSnapshot(snapshotPath, strippedPath, forceElf: !useAsm); |
| } else { |
| strippedPath = snapshotPath; |
| } |
| |
| print("Snapshot generated at $snapshotPath."); |
| print("Snapshot profile generated at $profilePath."); |
| |
| final profile = testProfile(profilePath); |
| |
| final expectedSize = |
| profile.nodes.fold<int>(0, (size, n) => size + n.selfSize); |
| |
| var checkedSize = false; |
| if (!useAsm) { |
| // Verify that the total size of the snapshot text and data sections is |
| // the same as the sum of the shallow sizes of all objects in the profile. |
| // This ensures that all bytes are accounted for in some way. |
| final elf = Elf.fromFile(snapshotPath); |
| Expect.isNotNull(elf); |
| |
| final vmTextSectionSymbol = elf.dynamicSymbolFor(vmSymbolName); |
| Expect.isNotNull(vmTextSectionSymbol); |
| final vmDataSectionSymbol = elf.dynamicSymbolFor(vmDataSymbolName); |
| Expect.isNotNull(vmDataSectionSymbol); |
| final isolateTextSectionSymbol = elf.dynamicSymbolFor(isolateSymbolName); |
| Expect.isNotNull(isolateTextSectionSymbol); |
| final isolateDataSectionSymbol = |
| elf.dynamicSymbolFor(isolateDataSymbolName); |
| Expect.isNotNull(isolateDataSectionSymbol); |
| |
| final actualSize = vmTextSectionSymbol.size + |
| vmDataSectionSymbol.size + |
| isolateTextSectionSymbol.size + |
| isolateDataSectionSymbol.size; |
| |
| Expect.equals( |
| expectedSize, actualSize, "failed on $description snapshot"); |
| checkedSize = true; |
| } |
| |
| if (stripUtil || stripFlag) { |
| var strippedSnapshotPath = snapshotPath; |
| if (stripUtil) { |
| strippedSnapshotPath = snapshotPath + '.stripped'; |
| await stripSnapshot(snapshotPath, strippedSnapshotPath, |
| forceElf: !useAsm); |
| print("Stripped snapshot generated at $strippedSnapshotPath."); |
| } |
| |
| // Verify that the actual size of the stripped snapshot is close to the |
| // sum of the shallow sizes of all objects in the profile. They will not |
| // be exactly equal because of global headers and padding. |
| final actualSize = await File(strippedSnapshotPath).length(); |
| |
| // See Elf::kPages in runtime/vm/elf.h, which is also used for assembly |
| // padding. |
| final segmentAlignment = 16 * 1024; |
| // Not every byte is accounted for by the snapshot profile, and data and |
| // instruction segments are padded to an alignment boundary. |
| final tolerance = 0.03 * actualSize + 2 * segmentAlignment; |
| |
| Expect.approxEquals(expectedSize, actualSize, tolerance, |
| "failed on $description snapshot"); |
| checkedSize = true; |
| } |
| |
| Expect.isTrue(checkedSize, "no snapshot size checks were performed"); |
| }); |
| } |
| |
| Match matchComplete(RegExp regexp, String line) { |
| Match match = regexp.firstMatch(line); |
| if (match == null) return match; |
| if (match.start != 0 || match.end != line.length) return null; |
| return match; |
| } |
| |
| // All fields of "Raw..." classes defined in "raw_object.h" must be included in |
| // the giant macro in "raw_object_fields.cc". This function attempts to check |
| // that with some basic regexes. |
| testMacros() async { |
| const String className = "([a-z0-9A-Z]+)"; |
| const String rawClass = "Raw$className"; |
| const String fieldName = "([a-z0-9A-Z_]+)"; |
| |
| final Map<String, Set<String>> fields = {}; |
| |
| final String rawObjectFieldsPath = |
| path.join(sdkDir, 'runtime', 'vm', 'raw_object_fields.cc'); |
| final RegExp fieldEntry = RegExp(" *F\\($className, $fieldName\\) *\\\\?"); |
| |
| await for (String line in File(rawObjectFieldsPath) |
| .openRead() |
| .cast<List<int>>() |
| .transform(utf8.decoder) |
| .transform(LineSplitter())) { |
| Match match = matchComplete(fieldEntry, line); |
| if (match != null) { |
| fields |
| .putIfAbsent(match.group(1), () => Set<String>()) |
| .add(match.group(2)); |
| } |
| } |
| |
| final RegExp classStart = RegExp("class $rawClass : public $rawClass {"); |
| final RegExp classEnd = RegExp("}"); |
| final RegExp field = RegExp(" $rawClass. +$fieldName;.*"); |
| |
| final String rawObjectPath = |
| path.join(sdkDir, 'runtime', 'vm', 'raw_object.h'); |
| |
| String currentClass; |
| bool hasMissingFields = false; |
| await for (String line in File(rawObjectPath) |
| .openRead() |
| .cast<List<int>>() |
| .transform(utf8.decoder) |
| .transform(LineSplitter())) { |
| Match match = matchComplete(classStart, line); |
| if (match != null) { |
| currentClass = match.group(1); |
| continue; |
| } |
| |
| match = matchComplete(classEnd, line); |
| if (match != null) { |
| currentClass = null; |
| continue; |
| } |
| |
| match = matchComplete(field, line); |
| if (match != null && currentClass != null) { |
| if (fields[currentClass] == null) { |
| hasMissingFields = true; |
| print("$currentClass is missing entirely."); |
| continue; |
| } |
| if (!fields[currentClass].contains(match.group(2))) { |
| hasMissingFields = true; |
| print("$currentClass is missing ${match.group(2)}."); |
| } |
| } |
| } |
| |
| if (hasMissingFields) { |
| Expect.fail("$rawObjectFieldsPath is missing some fields. " |
| "Please update it to match $rawObjectPath."); |
| } |
| } |
| |
| 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 testMacros(); |
| |
| await withTempDir('v8-snapshot-profile-writer', (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', |
| 'v8_snapshot_profile_writer_test.dart'); |
| final jitDillPath = path.join(tempDir, 'jit_test.dill'); |
| await run(genKernel, |
| <String>['--platform', platformDill, '-o', jitDillPath, _thisTestPath]); |
| |
| // 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, |
| '-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. |
| |
| // Test profile generation with a core JIT snapshot. |
| await testJIT(jitDillPath); |
| |
| // Test unstripped ELF generation directly. |
| await testAOT(aotDillPath); |
| await testAOT(aotDillPath, useBare: false); |
| await testAOT(aotDillPath, forceDrops: true); |
| await testAOT(aotDillPath, forceDrops: true, useBare: false); |
| await testAOT(aotDillPath, forceDrops: true, useDispatch: false); |
| await testAOT(aotDillPath, |
| forceDrops: true, useDispatch: false, useBare: false); |
| |
| // Test flag-stripped ELF generation. |
| await testAOT(aotDillPath, stripFlag: true); |
| await testAOT(aotDillPath, useBare: false, 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 testAOT(aotDillPath, useBare: false, disassemble: true); |
| } |
| |
| // We neither generate assembly nor have a stripping utility on Windows. |
| if (Platform.isWindows) { |
| printSkip('external stripping and assembly tests'); |
| return; |
| } |
| |
| // The native strip utility on Mac OS X doesn't recognize ELF files. |
| if (Platform.isMacOS && clangBuildToolsDir == null) { |
| printSkip('ELF external stripping test'); |
| } else { |
| // Test unstripped ELF generation that is then externally stripped. |
| await testAOT(aotDillPath, stripUtil: true); |
| await testAOT(aotDillPath, stripUtil: true, useBare: false); |
| } |
| |
| // TODO(sstrickl): Currently we can't assemble for SIMARM64 on MacOSX. |
| // For example, the test runner still uses blobs for |
| // dartkp-mac-*-simarm64. Change assembleSnapshot and remove this check |
| // when we can. |
| if (Platform.isMacOS && buildDir.endsWith('SIMARM64')) { |
| printSkip('assembly tests'); |
| return; |
| } |
| // Test unstripped assembly generation that is then externally stripped. |
| await testAOT(aotDillPath, useAsm: true); |
| await testAOT(aotDillPath, useAsm: true, useBare: false); |
| // Test stripped assembly generation that is then externally stripped. |
| await testAOT(aotDillPath, useAsm: true, stripFlag: true); |
| await testAOT(aotDillPath, useAsm: true, stripFlag: true, useBare: false); |
| }); |
| } |