diff --git a/pkg/native_stack_traces/CHANGELOG.md b/pkg/native_stack_traces/CHANGELOG.md index afdcbabc1ace..01afccdd5964 100644 --- a/pkg/native_stack_traces/CHANGELOG.md +++ b/pkg/native_stack_traces/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.6.0 +- Make return type of DwarfContainer reader methods nullable so null + can be returned if there is no relevant DWARF information. +- Allow DwarfSnapshot.fromDwarfContainer to return null if the DwarfContainer + contents is missing expected DWARF information (that is, if any of the + used DwarfContainer reader methods return null). + ## 0.5.7 - Translates non-symbolic stack traces that include information for diff --git a/pkg/native_stack_traces/lib/src/dwarf.dart b/pkg/native_stack_traces/lib/src/dwarf.dart index fc3455739232..29b45070cd44 100644 --- a/pkg/native_stack_traces/lib/src/dwarf.dart +++ b/pkg/native_stack_traces/lib/src/dwarf.dart @@ -2047,20 +2047,24 @@ class DwarfSnapshot extends Dwarf { DwarfSnapshot._(this._container, this._abbreviationsTables, this._debugInfo, this._lineNumberInfo); - static DwarfSnapshot fromDwarfContainer( + static DwarfSnapshot? fromDwarfContainer( Reader reader, DwarfContainer container) => // We use Zone values to pass around the string tables that may be used // when parsing different sections. runZoned(() { final abbrevReader = container.abbreviationsTableReader(reader); + if (abbrevReader == null) return null; final abbreviationsTables = Map.fromEntries(abbrevReader .readRepeatedWithOffsets(_AbbreviationsTable.fromReader)); - final debugInfo = DebugInfo.fromReader( - container.debugInfoReader(reader), abbreviationsTables); + final debugInfoReader = container.debugInfoReader(reader); + if (debugInfoReader == null) return null; + final debugInfo = + DebugInfo.fromReader(debugInfoReader, abbreviationsTables); - final lineNumberInfo = - LineNumberInfo.fromReader(container.lineNumberInfoReader(reader)); + final lineNumberInfoReader = container.lineNumberInfoReader(reader); + if (lineNumberInfoReader == null) return null; + final lineNumberInfo = LineNumberInfo.fromReader(lineNumberInfoReader); return DwarfSnapshot._( container, abbreviationsTables, debugInfo, lineNumberInfo); @@ -2190,8 +2194,10 @@ class DwarfUniversalBinary extends Dwarf { final container = binary.containerForCpuType(cpuType)!; final reader = binary.readerForCpuType(originalReader, cpuType)!; final dwarf = DwarfSnapshot.fromDwarfContainer(reader, container); + if (dwarf == null) continue; dwarfs[cpuType] = dwarf; } + if (dwarfs.isEmpty) return null; return DwarfUniversalBinary._(binary, dwarfs); } diff --git a/pkg/native_stack_traces/lib/src/dwarf_container.dart b/pkg/native_stack_traces/lib/src/dwarf_container.dart index 3ac359ce2b24..4ad6de581681 100644 --- a/pkg/native_stack_traces/lib/src/dwarf_container.dart +++ b/pkg/native_stack_traces/lib/src/dwarf_container.dart @@ -19,9 +19,9 @@ abstract class DwarfContainer { /// not match any expected Dart architecture. String? get architecture; - Reader debugInfoReader(Reader containerReader); - Reader lineNumberInfoReader(Reader containerReader); - Reader abbreviationsTableReader(Reader containerReader); + Reader? debugInfoReader(Reader containerReader); + Reader? lineNumberInfoReader(Reader containerReader); + Reader? abbreviationsTableReader(Reader containerReader); DwarfContainerSymbol? staticSymbolAt(int address); int? get vmStartAddress; diff --git a/pkg/native_stack_traces/lib/src/elf.dart b/pkg/native_stack_traces/lib/src/elf.dart index 05fe4d5b08d6..25e4c51bc704 100644 --- a/pkg/native_stack_traces/lib/src/elf.dart +++ b/pkg/native_stack_traces/lib/src/elf.dart @@ -1262,16 +1262,25 @@ class Elf extends DwarfContainer { String? get architecture => _header.architecture; @override - Reader abbreviationsTableReader(Reader containerReader) => - namedSections('.debug_abbrev').single.shrink(containerReader); + Reader? abbreviationsTableReader(Reader containerReader) { + final sections = namedSections('.debug_abbrev'); + if (sections.length != 1) return null; + return sections.single.shrink(containerReader); + } @override - Reader lineNumberInfoReader(Reader containerReader) => - namedSections('.debug_line').single.shrink(containerReader); + Reader? lineNumberInfoReader(Reader containerReader) { + final sections = namedSections('.debug_line'); + if (sections.length != 1) return null; + return sections.single.shrink(containerReader); + } @override - Reader debugInfoReader(Reader containerReader) => - namedSections('.debug_info').single.shrink(containerReader); + Reader? debugInfoReader(Reader containerReader) { + final sections = namedSections('.debug_info'); + if (sections.length != 1) return null; + return sections.single.shrink(containerReader); + } @override int? get vmStartAddress => dynamicSymbolFor(constants.vmSymbolName)?.value; diff --git a/pkg/native_stack_traces/lib/src/macho.dart b/pkg/native_stack_traces/lib/src/macho.dart index 7d6f5a68685d..c8d7e301deb5 100644 --- a/pkg/native_stack_traces/lib/src/macho.dart +++ b/pkg/native_stack_traces/lib/src/macho.dart @@ -506,14 +506,14 @@ class MachO extends DwarfContainer { String? get architecture => CpuType.fromCode(_header.cputype)?.dartName; @override - Reader abbreviationsTableReader(Reader containerReader) => - _dwarfSegment!.sections['__debug_abbrev']!.shrink(containerReader); + Reader? abbreviationsTableReader(Reader containerReader) => + _dwarfSegment?.sections['__debug_abbrev']?.shrink(containerReader); @override - Reader lineNumberInfoReader(Reader containerReader) => - _dwarfSegment!.sections['__debug_line']!.shrink(containerReader); + Reader? lineNumberInfoReader(Reader containerReader) => + _dwarfSegment?.sections['__debug_line']?.shrink(containerReader); @override - Reader debugInfoReader(Reader containerReader) => - _dwarfSegment!.sections['__debug_info']!.shrink(containerReader); + Reader? debugInfoReader(Reader containerReader) => + _dwarfSegment?.sections['__debug_info']?.shrink(containerReader); @override int? get vmStartAddress => _symbolTable[constants.vmSymbolName]?.value; diff --git a/pkg/native_stack_traces/pubspec.yaml b/pkg/native_stack_traces/pubspec.yaml index 51d11189797b..8b75fa9da71a 100644 --- a/pkg/native_stack_traces/pubspec.yaml +++ b/pkg/native_stack_traces/pubspec.yaml @@ -1,5 +1,5 @@ name: native_stack_traces -version: 0.5.7 +version: 0.6.0 description: Utilities for working with non-symbolic stack traces. repository: https://github.com/dart-lang/sdk/tree/main/pkg/native_stack_traces diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart index dd9c903b5ab3..06b112543ad1 100644 --- a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart +++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_deferred_test.dart @@ -14,24 +14,23 @@ import "dart:async"; import "dart:convert"; import "dart:io"; -import 'package:expect/expect.dart'; import 'package:native_stack_traces/native_stack_traces.dart'; import 'package:native_stack_traces/src/constants.dart' show rootLoadingUnitId; -import 'package:native_stack_traces/src/convert.dart' show LoadingUnit; import 'package:native_stack_traces/src/macho.dart'; import 'package:path/path.dart' as path; +import 'package:test/test.dart'; import 'use_flag_test_helper.dart'; -import 'use_dwarf_stack_traces_flag_test.dart' as original; +import 'use_dwarf_stack_traces_flag_helper.dart'; Future main() async { - await original.runTests( + await runTests( 'dwarf-flag-deferred-test', path.join(sdkDir, 'runtime', 'tests', 'vm', 'dart', 'use_dwarf_stack_traces_flag_deferred_program.dart'), - testNonDwarf, - testElf, - testAssembly); + runNonDwarf, + runElf, + runAssembly); } Manifest useSnapshotForDwarfPath(Manifest original, @@ -53,115 +52,125 @@ Manifest useSnapshotForDwarfPath(Manifest original, const _asmExt = '.S'; const _soExt = '.so'; -Future> testNonDwarf(String tempDir, String scriptDill) async { - final scriptNonDwarfUnitManifestPath = - path.join(tempDir, 'manifest_non_dwarf.json'); - final scriptNonDwarfSnapshot = path.join(tempDir, 'non_dwarf' + _soExt); +Future runNonDwarf(String tempDir, String scriptDill) async { + final manifestPath = path.join(tempDir, 'manifest_non_dwarf.json'); + final snapshotPath = path.join(tempDir, 'non_dwarf' + _soExt); await run(genSnapshot, [ '--no-dwarf-stack-traces-mode', - '--loading-unit-manifest=$scriptNonDwarfUnitManifestPath', + '--loading-unit-manifest=$manifestPath', '--snapshot-kind=app-aot-elf', - '--elf=$scriptNonDwarfSnapshot', + '--elf=$snapshotPath', scriptDill, ]); - final scriptNonDwarfUnitManifest = - Manifest.fromPath(scriptNonDwarfUnitManifestPath); - if (scriptNonDwarfUnitManifest == null) { - throw "Failure parsing manifest $scriptNonDwarfUnitManifestPath"; + final manifest = Manifest.fromPath(manifestPath); + if (manifest == null) { + throw "Failure parsing manifest $manifestPath"; } - if (!scriptNonDwarfUnitManifest.contains(rootLoadingUnitId)) { - throw "Manifest '$scriptNonDwarfUnitManifestPath' " - "does not contain root unit info"; + if (!manifest.contains(rootLoadingUnitId)) { + throw "Manifest '$manifestPath' does not contain root unit info"; + } + if (snapshotPath != manifest[rootLoadingUnitId]!.path) { + throw "Manifest '$manifestPath' does not contain expected " + "root unit path '$snapshotPath'"; } - Expect.stringEquals(scriptNonDwarfSnapshot, - scriptNonDwarfUnitManifest[rootLoadingUnitId]!.path); // Run the resulting non-Dwarf-AOT compiled script. - final nonDwarfTrace1 = - (await original.runTestProgram(dartPrecompiledRuntime, [ + final outputWithOppositeFlag = + (await runTestProgram(dartPrecompiledRuntime, [ '--dwarf-stack-traces-mode', - scriptNonDwarfSnapshot, - ])) - .trace; - final nonDwarfTrace2 = - (await original.runTestProgram(dartPrecompiledRuntime, [ + snapshotPath, + ])); + final output = (await runTestProgram(dartPrecompiledRuntime, [ '--no-dwarf-stack-traces-mode', - scriptNonDwarfSnapshot, - ])) - .trace; + snapshotPath, + ])); + + return NonDwarfState(output, outputWithOppositeFlag); +} - // Ensure the result is based off the flag passed to gen_snapshot, not - // the one passed to the runtime. - Expect.deepEquals(nonDwarfTrace1, nonDwarfTrace2); +/// Maps the id of a loading unit to the DWARF information for the unit. +typedef DwarfMap = Map; - return nonDwarfTrace1; +class DeferredElfState extends ElfState { + DeferredElfState(super.snapshot, super.debugInfo, super.output, + super.outputWithOppositeFlag); + + @override + Future check(Trace trace, DwarfMap dwarfMap) => + compareTraces(trace, output, outputWithOppositeFlag, dwarfMap); } -Future testElf( - String tempDir, String scriptDill, List nonDwarfTrace) async { - final scriptDwarfUnitManifestPath = path.join(tempDir, 'manifest_elf.json'); - final scriptDwarfSnapshot = path.join(tempDir, 'dwarf' + _soExt); - final scriptDwarfDebugInfo = path.join(tempDir, 'debug_info' + _soExt); +Future runElf(String tempDir, String scriptDill) async { + final manifestPath = path.join(tempDir, 'manifest_elf.json'); + final snapshotPath = path.join(tempDir, 'dwarf' + _soExt); + final debugInfoPath = path.join(tempDir, 'debug_info' + _soExt); await run(genSnapshot, [ // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because // the latter is a handler that sets the former and also may change // other flags. This way, we limit the difference between the two // snapshots and also directly test the flag saved as a VM global flag. '--dwarf-stack-traces-mode', - '--save-debugging-info=$scriptDwarfDebugInfo', - '--loading-unit-manifest=$scriptDwarfUnitManifestPath', + '--save-debugging-info=$debugInfoPath', + '--loading-unit-manifest=$manifestPath', '--snapshot-kind=app-aot-elf', - '--elf=$scriptDwarfSnapshot', + '--elf=$snapshotPath', scriptDill, ]); - final scriptDwarfUnitManifest = - Manifest.fromPath(scriptDwarfUnitManifestPath); - if (scriptDwarfUnitManifest == null) { - throw "Failure parsing manifest $scriptDwarfUnitManifestPath"; + final pathManifest = Manifest.fromPath(manifestPath); + if (pathManifest == null) { + throw "Failure parsing manifest $manifestPath"; + } + if (!pathManifest.contains(rootLoadingUnitId)) { + throw "Manifest '$manifestPath' does not contain root unit info"; + } + if (snapshotPath != pathManifest[rootLoadingUnitId]!.path) { + throw "Manifest '$manifestPath' does not contain expected " + "root unit path '$snapshotPath'"; } - if (!scriptDwarfUnitManifest.contains(rootLoadingUnitId)) { - throw "Manifest '$scriptDwarfUnitManifest' " - "does not contain root unit info"; + if (debugInfoPath != pathManifest[rootLoadingUnitId]!.dwarfPath) { + throw "Manifest '$manifestPath' does not contain expected " + "root unit debugging info path '$debugInfoPath'"; } - Expect.stringEquals( - scriptDwarfSnapshot, scriptDwarfUnitManifest[rootLoadingUnitId]!.path); // Run the resulting Dwarf-AOT compiled script. - final output1 = - await original.runTestProgram(dartPrecompiledRuntime, [ + final output = await runTestProgram(dartPrecompiledRuntime, [ '--dwarf-stack-traces-mode', - scriptDwarfSnapshot, + snapshotPath, ]); - final output2 = - await original.runTestProgram(dartPrecompiledRuntime, [ + final outputWithOppositeFlag = + await runTestProgram(dartPrecompiledRuntime, [ '--no-dwarf-stack-traces-mode', - scriptDwarfSnapshot, + snapshotPath, ]); - // Check with DWARF from separate debugging information. - await compareTraces(nonDwarfTrace, output1, output2, scriptDwarfUnitManifest); - // Check with DWARF in generated snapshot (e.g., replacing the Dwarf paths - // in the dwarf-stack-traces manifest, which point at the separate - // debugging information, with the output snapshot paths.) - final manifest = useSnapshotForDwarfPath(scriptDwarfUnitManifest); - await compareTraces(nonDwarfTrace, output1, output2, manifest); + final debugInfoDwarfMap = pathManifest.dwarfMap; + final snapshotDwarfMap = useSnapshotForDwarfPath(pathManifest).dwarfMap; + + return DeferredElfState( + snapshotDwarfMap, debugInfoDwarfMap, output, outputWithOppositeFlag); +} + +class DeferredAssemblyState extends AssemblyState { + DeferredAssemblyState(super.snapshot, super.debugInfo, super.output, + super.outputWithOppositeFlag, + [super.singleArch, super.multiArch]); + + @override + Future check(Trace trace, DwarfMap dwarfMap) => + compareTraces(trace, output, outputWithOppositeFlag, dwarfMap, + fromAssembly: true); } -Future testAssembly( - String tempDir, String scriptDill, List nonDwarfTrace) async { - // Currently there are no appropriate buildtools on the simulator trybots as - // normally they compile to ELF and don't need them for compiling assembly - // snapshots. - if (isSimulator || (!Platform.isLinux && !Platform.isMacOS)) return; +Future runAssembly( + String tempDir, String scriptDill) async { + if (skipAssembly != false) return null; - final scriptAssembly = path.join(tempDir, 'dwarf_assembly' + _asmExt); - final scriptDwarfAssemblyDebugInfo = - path.join(tempDir, 'dwarf_assembly_info' + _soExt); - final scriptDwarfAssemblyUnitManifestPath = - path.join(tempDir, 'manifest_assembly.json'); + final assemblyPath = path.join(tempDir, 'dwarf_assembly' + _asmExt); + final debugInfoPath = path.join(tempDir, 'dwarf_assembly_info' + _soExt); + final manifestPath = path.join(tempDir, 'manifest_assembly.json'); await run(genSnapshot, [ // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because @@ -169,107 +178,107 @@ Future testAssembly( // other flags. This way, we limit the difference between the two // snapshots and also directly test the flag saved as a VM global flag. '--dwarf-stack-traces-mode', - '--save-debugging-info=$scriptDwarfAssemblyDebugInfo', - '--loading-unit-manifest=$scriptDwarfAssemblyUnitManifestPath', + '--save-debugging-info=$debugInfoPath', + '--loading-unit-manifest=$manifestPath', '--snapshot-kind=app-aot-assembly', - '--assembly=$scriptAssembly', + '--assembly=$assemblyPath', scriptDill, ]); - final scriptDwarfAssemblyUnitManifest = - Manifest.fromPath(scriptDwarfAssemblyUnitManifestPath); - if (scriptDwarfAssemblyUnitManifest == null) { - throw "Failure parsing manifest $scriptDwarfAssemblyUnitManifest"; + final manifest = Manifest.fromPath(manifestPath); + if (manifest == null) { + throw "Failure parsing manifest $manifestPath"; + } + if (!manifest.contains(rootLoadingUnitId)) { + throw "Manifest '$manifestPath' does not contain root unit info"; + } + if (assemblyPath != manifest[rootLoadingUnitId]!.path) { + throw "Manifest '$manifestPath' does not contain expected " + "root unit path '$assemblyPath'"; } - if (!scriptDwarfAssemblyUnitManifest.contains(rootLoadingUnitId)) { - throw "Manifest '$scriptDwarfAssemblyUnitManifest' " - "does not contain root unit info"; + if (debugInfoPath != manifest[rootLoadingUnitId]!.dwarfPath) { + throw "Manifest '$manifestPath' does not contain expected " + "root unit debugging info path '$debugInfoPath'"; } - Expect.stringEquals( - scriptAssembly, scriptDwarfAssemblyUnitManifest[rootLoadingUnitId]!.path); - Expect.stringEquals(scriptDwarfAssemblyDebugInfo, - scriptDwarfAssemblyUnitManifest[rootLoadingUnitId]!.dwarfPath!); - for (final entry in scriptDwarfAssemblyUnitManifest.entries) { - Expect.isNotNull(entry.snapshotBasename); + for (final entry in manifest.entries) { final outputPath = path.join(tempDir, entry.snapshotBasename!); await assembleSnapshot(entry.path, outputPath, debug: true); } - final scriptDwarfAssemblySnapshot = path.join(tempDir, - scriptDwarfAssemblyUnitManifest[rootLoadingUnitId]!.snapshotBasename!); + final snapshotPath = + path.join(tempDir, manifest[rootLoadingUnitId]!.snapshotBasename!); // Run the resulting Dwarf-AOT compiled script. - final assemblyOutput1 = - await original.runTestProgram(dartPrecompiledRuntime, [ + final output = await runTestProgram(dartPrecompiledRuntime, [ '--dwarf-stack-traces-mode', - scriptDwarfAssemblySnapshot, + snapshotPath, scriptDill, ]); - final assemblyOutput2 = - await original.runTestProgram(dartPrecompiledRuntime, [ + final outputWithOppositeFlag = + await runTestProgram(dartPrecompiledRuntime, [ '--no-dwarf-stack-traces-mode', - scriptDwarfAssemblySnapshot, + snapshotPath, scriptDill, ]); - // Check with DWARF from separate debugging information. - await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2, - scriptDwarfAssemblyUnitManifest, - fromAssembly: true); - // Check with DWARF in assembled snapshot. Note that we get a separate .dSYM - // bundle on MacOS, so we need to add a '.dSYM' suffix there. - final manifest = useSnapshotForDwarfPath(scriptDwarfAssemblyUnitManifest, + final debugInfoDwarfMap = manifest.dwarfMap; + final debugManifest = useSnapshotForDwarfPath(manifest, outputDir: tempDir, suffix: Platform.isMacOS ? '.dSYM' : ''); - await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2, manifest, - fromAssembly: true); - - // Next comes tests for MacOS universal binaries. - if (!Platform.isMacOS) return; - - // Create empty MachO files (just a header) for each of the possible - // architectures. - final emptyFiles = {}; - for (final arch in original.machOArchNames.values) { - // Don't create an empty file for the current architecture. - if (arch == original.dartNameForCurrentArchitecture) continue; - final contents = emptyMachOForArchitecture(arch); - Expect.isNotNull(contents); - final emptyPath = path.join(tempDir, "empty_$arch.so"); - await File(emptyPath).writeAsBytes(contents!, flush: true); - emptyFiles[arch] = emptyPath; - } + final snapshotDwarfMap = debugManifest.dwarfMap; + + DwarfMap? singleArchSnapshotDwarfMap; + DwarfMap? multiArchSnapshotDwarfMap; + if (skipUniversalBinary == false) { + // Create empty MachO files (just a header) for each of the possible + // architectures. + final emptyFiles = {}; + for (final arch in machOArchNames.values) { + // Don't create an empty file for the current architecture. + if (arch == dartNameForCurrentArchitecture) continue; + final contents = emptyMachOForArchitecture(arch); + final emptyPath = path.join(tempDir, "empty_$arch.so"); + await File(emptyPath).writeAsBytes(contents!, flush: true); + emptyFiles[arch] = emptyPath; + } - Future testUniversalBinary( - String binaryPath, List machoFiles) async { - await run(lipo, [...machoFiles, '-create', '-output', binaryPath]); - final entries = {}; - for (final id in scriptDwarfAssemblyUnitManifest.ids) { - entries[id] = scriptDwarfAssemblyUnitManifest[id]!; - if (id == rootLoadingUnitId) { - entries[id] = entries[id]!.replaceDwarf(binaryPath); - } + final singleDir = await Directory(path.join(tempDir, 'ub-single')).create(); + final multiDir = await Directory(path.join(tempDir, 'ub-multi')).create(); + var singleManifest = Manifest.of(debugManifest); + var multiManifest = Manifest.of(debugManifest); + for (final id in debugManifest.ids) { + final entry = debugManifest[id]!; + final snapshotPath = MachO.handleDSYM(debugManifest[id]!.dwarfPath!); + final singlePath = path.join(singleDir.path, path.basename(snapshotPath)); + await run(lipo, [snapshotPath, '-create', '-output', singlePath]); + final multiPath = path.join(multiDir.path, path.basename(snapshotPath)); + await run(lipo, [ + ...emptyFiles.values, + snapshotPath, + '-create', + '-output', + multiPath + ]); + singleManifest[id] = entry.replaceDwarf(singlePath); + multiManifest[id] = entry.replaceDwarf(multiPath); } - final manifest = Manifest._(entries); - await compareTraces( - nonDwarfTrace, assemblyOutput1, assemblyOutput2, manifest, - fromAssembly: true); + + singleArchSnapshotDwarfMap = await singleManifest.dwarfMap; + multiArchSnapshotDwarfMap = await multiManifest.dwarfMap; } - final scriptDwarfAssemblyDebugSnapshotFile = - MachO.handleDSYM(manifest[rootLoadingUnitId]!.dwarfPath!); - await testUniversalBinary(path.join(tempDir, "ub-single"), - [scriptDwarfAssemblyDebugSnapshotFile]); - await testUniversalBinary(path.join(tempDir, "ub-multiple"), - [...emptyFiles.values, scriptDwarfAssemblyDebugSnapshotFile]); + return DeferredAssemblyState( + snapshotDwarfMap, + debugInfoDwarfMap, + output, + outputWithOppositeFlag, + singleArchSnapshotDwarfMap, + multiArchSnapshotDwarfMap); } -Future compareTraces( - List nonDwarfTrace, - original.DwarfTestOutput output1, - original.DwarfTestOutput output2, - Manifest manifest, +Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, + DwarfTestOutput output2, DwarfMap dwarfMap, {bool fromAssembly = false}) async { - Expect.isNotNull(manifest[rootLoadingUnitId]); + expect(dwarfMap, contains(rootLoadingUnitId)); final header1 = StackTraceHeader.fromLines(output1.trace); print('Header1 = $header1'); @@ -288,31 +297,26 @@ Future compareTraces( print("PCOffsets from trace 2:"); printByUnit(tracePCOffsets2); - Expect.deepEquals(tracePCOffsets1, tracePCOffsets2); - - Expect.isNotNull(tracePCOffsets1[rootLoadingUnitId]); - Expect.isNotEmpty(tracePCOffsets1[rootLoadingUnitId]!); + expect(tracePCOffsets2, equals(tracePCOffsets1)); + expect(tracePCOffsets1, contains(rootLoadingUnitId)); + expect(tracePCOffsets1[rootLoadingUnitId]!, isNotEmpty); final sampleOffset = tracePCOffsets1[rootLoadingUnitId]!.first; // Only retrieve the DWARF objects that we need to decode the stack traces. final dwarfByUnitId = {}; for (final id in tracePCOffsets1.keys.toSet()) { - Expect.isTrue(header2.units!.containsKey(id)); - final dwarfPath = manifest[id]!.dwarfPath; - Expect.isNotNull(dwarfPath); - print("Reading dwarf for unit $id from $dwarfPath}"); - final dwarf = Dwarf.fromFile(dwarfPath!); - Expect.isNotNull(dwarf); - dwarfByUnitId[id] = dwarf!; + expect(header2.units!, contains(id)); + expect(dwarfMap, contains(id)); + dwarfByUnitId[id] = dwarfMap[id]!; } // The first non-root loading unit is not loaded and so shouldn't appear in // the stack trace at all, but the root and second non-root loading units do. - Expect.isTrue(dwarfByUnitId.containsKey(rootLoadingUnitId)); - Expect.isFalse(dwarfByUnitId.containsKey(rootLoadingUnitId + 1)); - Expect.isTrue(dwarfByUnitId.containsKey(rootLoadingUnitId + 2)); + expect(dwarfByUnitId, contains(rootLoadingUnitId)); + expect(dwarfByUnitId[rootLoadingUnitId + 1], isNull); + expect(dwarfByUnitId, contains(rootLoadingUnitId + 2)); final rootDwarf = dwarfByUnitId[rootLoadingUnitId]!; - original.checkRootUnitAssumptions(output1, output2, rootDwarf, + checkRootUnitAssumptions(output1, output2, rootDwarf, sampleOffset: sampleOffset, matchingBuildIds: !fromAssembly); // The offsets of absolute addresses from their respective DSO base @@ -330,18 +334,16 @@ Future compareTraces( dsoBase1[unit.id] = unit.dsoBase; } print("DSO bases for trace 1:"); - for (final unitId in dsoBase1.keys) { - print(" $unitId => 0x${dsoBase1[unitId]!.toRadixString(16)}"); - } + printByUnit(dsoBase1.map((id, dso) => MapEntry(id, [dso])), + toString: addressString); final dsoBase2 = {}; for (final unit in header2.units!.values) { dsoBase2[unit.id] = unit.dsoBase; } print("DSO bases for trace 2:"); - for (final unitId in dsoBase2.keys) { - print(" $unitId => 0x${dsoBase2[unitId]!.toRadixString(16)}"); - } + printByUnit(dsoBase2.map((id, dso) => MapEntry(id, [dso])), + toString: addressString); final relocatedFromDso1 = Map.fromEntries(absTrace1.keys.map((unitId) => MapEntry(unitId, absTrace1[unitId]!.map((a) => a - dsoBase1[unitId]!)))); @@ -353,7 +355,7 @@ Future compareTraces( print("Relocated addresses from trace 2:"); printByUnit(relocatedFromDso2, toString: addressString); - Expect.deepEquals(relocatedFromDso1, relocatedFromDso2); + expect(relocatedFromDso2, equals(relocatedFromDso1)); // We don't print 'virt' relocated addresses when running assembled snapshots. if (fromAssembly) return; @@ -374,28 +376,28 @@ Future compareTraces( final fromTracePCOffsets1 = >{}; for (final unitId in tracePCOffsets1.keys) { - final dwarf = dwarfByUnitId[unitId]; - Expect.isNotNull(dwarf); + expect(dwarfByUnitId, contains(unitId)); + final dwarf = dwarfByUnitId[unitId]!; fromTracePCOffsets1[unitId] = - tracePCOffsets1[unitId]!.map((o) => o.virtualAddressIn(dwarf!)); + tracePCOffsets1[unitId]!.map((o) => o.virtualAddressIn(dwarf)); } print("Virtual addresses calculated from PCOffsets in trace 1:"); printByUnit(fromTracePCOffsets1, toString: addressString); final fromTracePCOffsets2 = >{}; for (final unitId in tracePCOffsets2.keys) { - final dwarf = dwarfByUnitId[unitId]; - Expect.isNotNull(dwarf); + expect(dwarfByUnitId, contains(unitId)); + final dwarf = dwarfByUnitId[unitId]!; fromTracePCOffsets2[unitId] = - tracePCOffsets2[unitId]!.map((o) => o.virtualAddressIn(dwarf!)); + tracePCOffsets2[unitId]!.map((o) => o.virtualAddressIn(dwarf)); } print("Virtual addresses calculated from PCOffsets in trace 2:"); printByUnit(fromTracePCOffsets2, toString: addressString); - Expect.deepEquals(virtTrace1, virtTrace2); - Expect.deepEquals(virtTrace1, fromTracePCOffsets1); - Expect.deepEquals(virtTrace2, fromTracePCOffsets2); - Expect.deepEquals(virtTrace1, relocatedFromDso1); - Expect.deepEquals(virtTrace2, relocatedFromDso2); + expect(virtTrace2, equals(virtTrace1)); + expect(fromTracePCOffsets1, equals(virtTrace1)); + expect(fromTracePCOffsets2, equals(virtTrace2)); + expect(relocatedFromDso1, equals(virtTrace1)); + expect(relocatedFromDso2, equals(virtTrace2)); // Check that translating the DWARF stack trace (without internal frames) // matches the symbolic stack trace, and that for ELF outputs, we can also @@ -409,39 +411,38 @@ Future compareTraces( final decoder2 = DwarfStackTraceDecoder(rootDwarf, unitDwarfs: unitDwarfs); final translatedDwarfTrace2 = await Stream.fromIterable(output1.trace).transform(decoder2).toList(); - Expect.deepEquals(translatedDwarfTrace1, translatedDwarfTrace2); + expect(translatedDwarfTrace2, equals(translatedDwarfTrace1)); } - original.checkTranslatedTrace(nonDwarfTrace, translatedDwarfTrace1); + checkTranslatedTrace(nonDwarfTrace, translatedDwarfTrace1); } void checkHeaderWithUnits(StackTraceHeader header, {bool fromAssembly = false}) { - original.checkHeader(header); + checkHeader(header); // Additional requirements for the deferred test. - Expect.isNotNull(header.units); + expect(header.units, isNotNull); // There should be an entry included for the root loading unit. - Expect.isNotNull(header.units![rootLoadingUnitId]); + expect(header.units!, contains(rootLoadingUnitId)); // The first non-root loading unit is never loaded by the test program. // Verify that it is not listed for direct-to-ELF snapshots. (It may be // eagerly loaded in assembly snapshots.) if (!fromAssembly) { - Expect.isNull(header.units![rootLoadingUnitId + 1]); + expect(header.units![rootLoadingUnitId + 1], isNull); } // There should be an entry included for the second non-root loading unit. - Expect.isNotNull(header.units![rootLoadingUnitId + 2]); + expect(header.units!, contains(rootLoadingUnitId + 2)); for (final unitId in header.units!.keys) { final unit = header.units![unitId]!; - Expect.equals(unitId, unit.id); - Expect.isNotNull(unit.buildId); + expect(unit.id, equals(unitId)); + expect(unit.buildId, isNotNull); } // The information for the root loading unit should match the non-loading // unit information in the header. - Expect.equals(header.isolateStart!, header.units![rootLoadingUnitId]!.start!); - Expect.equals( - header.isolateDsoBase!, header.units![rootLoadingUnitId]!.dsoBase!); - Expect.stringEquals( - header.buildId!, header.units![rootLoadingUnitId]!.buildId!); + expect(header.units![rootLoadingUnitId]!.start, equals(header.isolateStart!)); + expect(header.units![rootLoadingUnitId]!.dsoBase, + equals(header.isolateDsoBase!)); + expect(header.units![rootLoadingUnitId]!.buildId!, equals(header.buildId!)); } Map> collectPCOffsetsByUnit(Iterable lines) { @@ -489,12 +490,14 @@ Map> explicitVirtualAddresses(Iterable lines) => void printByUnit(Map> valuesByUnit, {String Function(X) toString = objectString}) { + final buffer = StringBuffer(); for (final unitId in valuesByUnit.keys) { - print(" For unit $unitId:"); + buffer.writeln(" For unit $unitId:"); for (final value in valuesByUnit[unitId]!) { - print(" * ${toString(value)}"); + buffer.writeln(" * ${toString(value)}"); } } + print(buffer.toString()); } String objectString(dynamic object) => object.toString(); @@ -558,7 +561,6 @@ class Manifest { entryMap[rootLoadingUnitId] = rootEntry.replaceSnapshotBasename(basename); for (final id in entryMap.keys) { if (id == rootLoadingUnitId) continue; - final entry = entryMap[id]!; // Note that this must match the suffix added to the snapshot URI // in Loader::DeferredLoadHandler. entryMap[id] = entryMap[id]! @@ -574,10 +576,25 @@ class Manifest { return fromJson(json.decode(file.readAsStringSync())); } + static Manifest of(Manifest original) => Manifest._(Map.of(original._map)); + + DwarfMap get dwarfMap { + final map = {}; + for (final id in ids) { + final path = _map[id]!.dwarfPath; + if (path == null) continue; + final dwarf = Dwarf.fromFile(path); + if (dwarf == null) continue; + map[id] = dwarf; + } + return map; + } + int get length => _map.length; bool contains(int i) => _map.containsKey(i); ManifestEntry? operator [](int i) => _map[i]; + void operator []=(int i, ManifestEntry e) => _map[i] = e; Iterable get ids => _map.keys; Iterable get entries => _map.values; diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_helper.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_helper.dart new file mode 100644 index 000000000000..f79da44ada24 --- /dev/null +++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_helper.dart @@ -0,0 +1,383 @@ +// Copyright (c) 2024, 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. + +// Helper methods and definitions used in the use_dwarf_stack_traces_flag tests. + +import "dart:async"; +import "dart:convert"; +import "dart:io"; + +import 'package:native_stack_traces/native_stack_traces.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'use_flag_test_helper.dart'; + +/// Returns false if tests involving assembly snapshots should be run +/// and a String describing why the tests should be skipped otherwise. +Object get skipAssembly { + // Currently there are no appropriate buildtools on the simulator trybots as + // normally they compile to ELF and don't need them for compiling assembly + // snapshots. + if (isSimulator) { + return "running on a simulated architecture"; + } + return (Platform.isLinux || Platform.isMacOS) + ? false + : "no process for assembling snapshots on this platform"; +} + +/// Returns false if tests involving MacOS universal binaries should be run +/// and a String describing why the tests should be skipped otherwise. +Object get skipUniversalBinary { + final assemblySkipped = skipAssembly; + if (assemblySkipped != false) return assemblySkipped; + return Platform.isMacOS ? false : "only valid for MacOS"; +} + +typedef Trace = List; + +class DwarfTestOutput { + final Trace trace; + final int allocateObjectStart; + final int allocateObjectEnd; + + DwarfTestOutput(this.trace, this.allocateObjectStart, this.allocateObjectEnd); +} + +class NonDwarfState { + final DwarfTestOutput output; + final DwarfTestOutput outputWithOppositeFlag; + + NonDwarfState(this.output, this.outputWithOppositeFlag); + + void check() => expect(outputWithOppositeFlag.trace, equals(output.trace)); +} + +abstract class ElfState { + final T snapshot; + final T debugInfo; + final DwarfTestOutput output; + final DwarfTestOutput outputWithOppositeFlag; + + ElfState( + this.snapshot, this.debugInfo, this.output, this.outputWithOppositeFlag); + + Future check(Trace trace, T t); +} + +abstract class AssemblyState { + final T snapshot; + final T debugInfo; + final DwarfTestOutput output; + final DwarfTestOutput outputWithOppositeFlag; + final T? singleArch; + final T? multiArch; + + AssemblyState( + this.snapshot, this.debugInfo, this.output, this.outputWithOppositeFlag, + [this.singleArch, this.multiArch]); + + Future check(Trace trace, T t); +} + +abstract class UniversalBinaryState { + final T singleArch; + final T multiArch; + + UniversalBinaryState(this.singleArch, this.multiArch); + + Future checkSingleArch(Trace trace, AssemblyState assemblyState); + Future checkMultiArch(Trace trace, AssemblyState assemblyState); +} + +Future runTests( + String tempPrefix, + String scriptPath, + Future Function(String, String) runNonDwarf, + Future> Function(String, String) runElf, + Future?> Function(String, String) runAssembly) async { + if (!isAOTRuntime) { + return; // Running in JIT: AOT binaries not available. + } + + if (Platform.isAndroid) { + return; // SDK tree and dart_bootstrap not available on the test device. + } + + // These are the tools we need to be available to run on a given platform: + if (!await testExecutable(genSnapshot)) { + throw "Cannot run test as $genSnapshot not available"; + } + if (!await testExecutable(dartPrecompiledRuntime)) { + throw "Cannot run test as $dartPrecompiledRuntime not available"; + } + if (!File(platformDill).existsSync()) { + throw "Cannot run test as $platformDill does not exist"; + } + + await withTempDir(tempPrefix, (String tempDir) async { + // We have to use the program in its original location so it can use + // the dart:_internal library (as opposed to adding it as an OtherResources + // option to the test). + final scriptDill = path.join(tempDir, 'flag_program.dill'); + + // Compile script to Kernel IR. + await run(genKernel, [ + '--aot', + '--platform=$platformDill', + '-o', + scriptDill, + scriptPath, + ]); + + final nonDwarfState = await runNonDwarf(tempDir, scriptDill); + final elfState = await runElf(tempDir, scriptDill); + final assemblyState = await runAssembly(tempDir, scriptDill); + + test('Testing symbolic traces', nonDwarfState.check); + + final nonDwarfTrace = nonDwarfState.output.trace; + + test('Testing ELF traces with separate debugging info', + () async => await elfState.check(nonDwarfTrace, elfState.debugInfo)); + + test('Testing ELF traces with original snapshot', + () async => await elfState.check(nonDwarfTrace, elfState.snapshot)); + + test('Testing assembly traces with separate debugging info', () async { + expect(assemblyState, isNotNull); + await assemblyState!.check(nonDwarfTrace, assemblyState.debugInfo); + }, skip: skipAssembly); + + test('Testing assembly traces with debug snapshot ', () async { + expect(assemblyState, isNotNull); + await assemblyState!.check(nonDwarfTrace, assemblyState.snapshot); + }, skip: skipAssembly); + + test('Testing single-architecture universal binary', () async { + expect(assemblyState, isNotNull); + expect(assemblyState!.singleArch, isNotNull); + await assemblyState.check(nonDwarfTrace, assemblyState.singleArch!); + }, skip: skipUniversalBinary); + + test('Testing multi-architecture universal binary', () async { + expect(assemblyState, isNotNull); + expect(assemblyState!.multiArch, isNotNull); + await assemblyState.check(nonDwarfTrace, assemblyState.multiArch!); + }, skip: skipUniversalBinary); + }); +} + +void checkHeader(StackTraceHeader header) { + // These should be all available. + expect(header.vmStart, isNotNull); + expect(header.isolateStart, isNotNull); + expect(header.isolateDsoBase, isNotNull); + expect(header.buildId, isNotNull); + expect(header.os, isNotNull); + expect(header.architecture, isNotNull); + expect(header.usingSimulator, isNotNull); + expect(header.compressedPointers, isNotNull); +} + +void checkRootUnitAssumptions( + DwarfTestOutput output1, DwarfTestOutput output2, Dwarf rootDwarf, + {required PCOffset sampleOffset, bool matchingBuildIds = true}) { + // We run the test program on the same host OS as the test, so any + // PCOffset from the trace should have this information. + expect(sampleOffset.os, isNotNull); + expect(sampleOffset.architecture, isNotNull); + expect(sampleOffset.usingSimulator, isNotNull); + expect(sampleOffset.compressedPointers, isNotNull); + + expect(sampleOffset.os, equals(Platform.operatingSystem)); + final archString = '${sampleOffset.usingSimulator! ? 'SIM' : ''}' + '${sampleOffset.architecture!.toUpperCase()}' + '${sampleOffset.compressedPointers! ? 'C' : ''}'; + final baseBuildDir = path.basename(buildDir); + expect(baseBuildDir, endsWith(archString)); + + // Check that the build IDs exist in the traces and are the same. + final buildId1 = buildId(output1.trace); + expect(buildId1, isNotEmpty); + print('Trace 1 build ID: "${buildId1}"'); + final buildId2 = buildId(output2.trace); + expect(buildId2, isNotEmpty); + print('Trace 2 build ID: "${buildId2}"'); + expect(buildId2, equals(buildId1)); + + if (matchingBuildIds) { + // The build ID in the traces should be the same as the DWARF build ID + // when the ELF was generated by gen_snapshot. + final dwarfBuildId = rootDwarf.buildId(); + expect(dwarfBuildId, isNotNull); + print('Dwarf build ID: "${dwarfBuildId!}"'); + // We should never generate an all-zero build ID. + expect(dwarfBuildId, isNot("00000000000000000000000000000000")); + // This is a common failure case as well, when HashBitsContainer ends up + // hashing over seemingly empty sections. + expect(dwarfBuildId, isNot("01000000010000000100000001000000")); + expect(buildId1, equals(dwarfBuildId)); + expect(buildId2, equals(dwarfBuildId)); + } + + final allocateObjectStart = output1.allocateObjectStart; + final allocateObjectEnd = output1.allocateObjectEnd; + expect(output2.allocateObjectStart, equals(allocateObjectStart)); + expect(output2.allocateObjectEnd, equals(allocateObjectEnd)); + + checkAllocateObjectOffset(rootDwarf, allocateObjectStart); + // The end of the bare instructions payload may be padded up to word size, + // so check the maximum possible word size (64 bits) before the end. + checkAllocateObjectOffset(rootDwarf, allocateObjectEnd - 8); + // The end should be either in a different stub or not a stub altogether. + checkAllocateObjectOffset(rootDwarf, allocateObjectEnd, expectedValue: false); + // The byte before the start should also be in either a different stub or + // not in a stub altogether. + checkAllocateObjectOffset(rootDwarf, allocateObjectStart - 1, + expectedValue: false); + // Check the midpoint of the stub, as the stub should be large enough that the + // midpoint won't be in any possible padding. + expect(allocateObjectEnd - allocateObjectStart, greaterThanOrEqualTo(16), + reason: 'midpoint of stub may be in bare payload padding'); + checkAllocateObjectOffset( + rootDwarf, (allocateObjectStart + allocateObjectEnd) ~/ 2); + + print("Successfully matched AllocateObject stub addresses"); +} + +void checkAllocateObjectOffset(Dwarf dwarf, int offset, + {bool expectedValue = true}) { + final pcOffset = PCOffset(offset, InstructionsSection.isolate); + print('Offset of tested stub address is $pcOffset'); + final callInfo = + dwarf.callInfoForPCOffset(pcOffset, includeInternalFrames: true); + print('Call info for tested stub address is $callInfo'); + final got = callInfo != null && + callInfo.length == 1 && + callInfo.single is StubCallInfo && + (callInfo.single as StubCallInfo).name.endsWith('AllocateObjectStub'); + expect(got, equals(expectedValue), + reason: 'address is ${expectedValue ? 'not within' : 'within'} ' + 'the AllocateObject stub'); +} + +void checkTranslatedTrace(List nonDwarfTrace, List dwarfTrace) { + final translatedStackFrames = onlySymbolicFrameLines(dwarfTrace); + final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace); + + print('Stack frames from translated non-symbolic stack trace:'); + print(translatedStackFrames.join('\n')); + + print('Stack frames from original symbolic stack trace:'); + print(originalStackFrames.join('\n')); + + expect(translatedStackFrames, isNotEmpty); + expect(originalStackFrames, isNotEmpty); + + // In symbolic mode, we don't store column information to avoid an increase + // in size of CodeStackMaps. Thus, we need to strip any columns from the + // translated non-symbolic stack to compare them via equality. + final columnStrippedTranslated = removeColumns(translatedStackFrames); + + print('Stack frames from translated non-symbolic stack trace, no columns:'); + print(columnStrippedTranslated.join('\n')); + + expect(columnStrippedTranslated, equals(originalStackFrames)); +} + +Future runTestProgram( + String executable, List args) async { + final result = await runHelper(executable, args); + + if (result.exitCode == 0) { + throw 'Command did not fail with non-zero exit code'; + } + if (result.stdout.isEmpty) { + throw 'Command did not print a stacktrace'; + } + + final stdoutLines = LineSplitter.split(result.stdout).toList(); + if (result.stdout.length < 2) { + throw 'Command did not print both absolute addresses for stub range'; + } + final start = int.parse(stdoutLines[0]); + final end = int.parse(stdoutLines[1]); + + return DwarfTestOutput( + LineSplitter.split(result.stderr).toList(), start, end); +} + +final _buildIdRE = RegExp(r"build_id: '([a-f\d]+)'"); +String buildId(Iterable lines) { + for (final line in lines) { + final match = _buildIdRE.firstMatch(line); + if (match != null) { + return match.group(1)!; + } + } + return ''; +} + +final _symbolicFrameRE = RegExp(r'^#\d+\s+'); + +Iterable onlySymbolicFrameLines(Iterable lines) { + return lines.where((line) => _symbolicFrameRE.hasMatch(line)); +} + +final _columnsRE = RegExp(r'[(](.*:\d+):\d+[)]'); + +Iterable removeColumns(Iterable lines) sync* { + for (final line in lines) { + final match = _columnsRE.firstMatch(line); + if (match != null) { + yield line.replaceRange(match.start, match.end, '(${match.group(1)!})'); + } else { + yield line; + } + } +} + +Iterable parseUsingAddressRegExp(RegExp re, Iterable lines) sync* { + for (final line in lines) { + final match = re.firstMatch(line); + if (match != null) { + yield int.parse(match.group(1)!, radix: 16); + } + } +} + +final _absRE = RegExp(r'abs ([a-f\d]+)'); + +Iterable absoluteAddresses(Iterable lines) => + parseUsingAddressRegExp(_absRE, lines); + +final _virtRE = RegExp(r'virt ([a-f\d]+)'); + +Iterable explicitVirtualAddresses(Iterable lines) => + parseUsingAddressRegExp(_virtRE, lines); + +final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)'); + +Iterable dsoBaseAddresses(Iterable lines) => + parseUsingAddressRegExp(_dsoBaseRE, lines); + +// We only list architectures supported by the current CpuType enum in +// pkg:native_stack_traces/src/macho.dart. +const machOArchNames = { + "ARM": "arm", + "ARM64": "arm64", + "IA32": "ia32", + "X64": "x64", +}; + +String? get dartNameForCurrentArchitecture { + for (final entry in machOArchNames.entries) { + if (buildDir.endsWith(entry.key)) { + return entry.value; + } + } + return null; +} diff --git a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart index b1dab8b51bc5..04a974b6f483 100644 --- a/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart +++ b/runtime/tests/vm/dart/use_dwarf_stack_traces_flag_test.dart @@ -7,78 +7,34 @@ // passed to the runtime). import "dart:async"; -import "dart:convert"; import "dart:io"; -import 'package:expect/expect.dart'; import 'package:native_stack_traces/native_stack_traces.dart'; import 'package:native_stack_traces/src/macho.dart'; import 'package:path/path.dart' as path; +import 'package:test/test.dart'; import 'use_flag_test_helper.dart'; +import 'use_dwarf_stack_traces_flag_helper.dart'; Future main() async { await runTests( 'dwarf-flag-test', path.join(sdkDir, 'runtime', 'tests', 'vm', 'dart', 'use_dwarf_stack_traces_flag_program.dart'), - testNonDwarf, - testElf, - testAssembly); + runNonDwarf, + runElf, + runAssembly); } -Future runTests( - String tempPrefix, - String scriptPath, - Future> Function(String, String) testNonDwarf, - Future Function(String, String, List) testElf, - Future Function(String, String, List) testAssembly) async { - if (!isAOTRuntime) { - return; // Running in JIT: AOT binaries not available. - } - - if (Platform.isAndroid) { - return; // SDK tree and dart_bootstrap not available on the test device. - } - - // These are the tools we need to be available to run on a given platform: - if (!await testExecutable(genSnapshot)) { - throw "Cannot run test as $genSnapshot not available"; - } - if (!await testExecutable(dartPrecompiledRuntime)) { - throw "Cannot run test as $dartPrecompiledRuntime not available"; - } - if (!File(platformDill).existsSync()) { - throw "Cannot run test as $platformDill does not exist"; - } - - await withTempDir(tempPrefix, (String tempDir) async { - // We have to use the program in its original location so it can use - // the dart:_internal library (as opposed to adding it as an OtherResources - // option to the test). - final scriptDill = path.join(tempDir, 'flag_program.dill'); - - // Compile script to Kernel IR. - await run(genKernel, [ - '--aot', - '--platform=$platformDill', - '-o', - scriptDill, - scriptPath, - ]); - - final nonDwarfTrace = await testNonDwarf(tempDir, scriptDill); - - await testElf(tempDir, scriptDill, nonDwarfTrace); - - await testAssembly(tempDir, scriptDill, nonDwarfTrace); - }); -} - -Future> testNonDwarf(String tempDir, String scriptDill) async { +Future runNonDwarf(String tempDir, String scriptDill) async { final scriptNonDwarfSnapshot = path.join(tempDir, 'non_dwarf.so'); await run(genSnapshot, [ + // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because + // the latter is a handler that sets the former and also may change + // other flags. This way, we limit the difference between the two + // snapshots and also directly test the flag saved as a VM global flag. '--no-dwarf-stack-traces-mode', '--snapshot-kind=app-aot-elf', '--elf=$scriptNonDwarfSnapshot', @@ -86,72 +42,74 @@ Future> testNonDwarf(String tempDir, String scriptDill) async { ]); // Run the resulting non-Dwarf-AOT compiled script. - final nonDwarfTrace1 = (await runTestProgram(dartPrecompiledRuntime, [ + final outputWithOppositeFlag = + (await runTestProgram(dartPrecompiledRuntime, [ '--dwarf-stack-traces-mode', scriptNonDwarfSnapshot, scriptDill, - ])) - .trace; - final nonDwarfTrace2 = (await runTestProgram(dartPrecompiledRuntime, [ + ])); + final output = (await runTestProgram(dartPrecompiledRuntime, [ '--no-dwarf-stack-traces-mode', scriptNonDwarfSnapshot, scriptDill, - ])) - .trace; + ])); - // Ensure the result is based off the flag passed to gen_snapshot, not - // the one passed to the runtime. - Expect.deepEquals(nonDwarfTrace1, nonDwarfTrace2); + return NonDwarfState(output, outputWithOppositeFlag); +} + +class DwarfElfState extends ElfState { + DwarfElfState(super.snapshot, super.debugInfo, super.output, + super.outputWithOppositeFlag); - return nonDwarfTrace1; + @override + Future check(Trace trace, Dwarf dwarf) => + compareTraces(trace, output, outputWithOppositeFlag, dwarf); } -Future testElf( - String tempDir, String scriptDill, List nonDwarfTrace) async { - final scriptDwarfSnapshot = path.join(tempDir, 'dwarf.so'); - final scriptDwarfDebugInfo = path.join(tempDir, 'debug_info.so'); +Future runElf(String tempDir, String scriptDill) async { + final snapshotPath = path.join(tempDir, 'dwarf.so'); + final debugInfoPath = path.join(tempDir, 'debug_info.so'); await run(genSnapshot, [ - // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because - // the latter is a handler that sets the former and also may change - // other flags. This way, we limit the difference between the two - // snapshots and also directly test the flag saved as a VM global flag. '--dwarf-stack-traces-mode', - '--save-debugging-info=$scriptDwarfDebugInfo', + '--save-debugging-info=$debugInfoPath', '--snapshot-kind=app-aot-elf', - '--elf=$scriptDwarfSnapshot', + '--elf=$snapshotPath', scriptDill, ]); + final snapshot = Dwarf.fromFile(snapshotPath)!; + final debugInfo = Dwarf.fromFile(debugInfoPath)!; + // Run the resulting Dwarf-AOT compiled script. - final output1 = await runTestProgram(dartPrecompiledRuntime, - ['--dwarf-stack-traces-mode', scriptDwarfSnapshot, scriptDill]); - final output2 = await runTestProgram(dartPrecompiledRuntime, [ - '--no-dwarf-stack-traces-mode', - scriptDwarfSnapshot, - scriptDill - ]); + final output = await runTestProgram(dartPrecompiledRuntime, + ['--dwarf-stack-traces-mode', snapshotPath, scriptDill]); + final outputWithOppositeFlag = await runTestProgram(dartPrecompiledRuntime, + ['--no-dwarf-stack-traces-mode', snapshotPath, scriptDill]); + + return DwarfElfState(snapshot, debugInfo, output, outputWithOppositeFlag); +} - // Check with DWARF from separate debugging information. - await compareTraces(nonDwarfTrace, output1, output2, scriptDwarfDebugInfo); - // Check with DWARF in generated snapshot. - await compareTraces(nonDwarfTrace, output1, output2, scriptDwarfSnapshot); +class DwarfAssemblyState extends AssemblyState { + DwarfAssemblyState(super.snapshot, super.debugInfo, super.output, + super.outputWithOppositeFlag, + [super.singleArch, super.multiArch]); + + @override + Future check(Trace trace, Dwarf dwarf) => + compareTraces(trace, output, outputWithOppositeFlag, dwarf, + fromAssembly: true); } -Future testAssembly( - String tempDir, String scriptDill, List nonDwarfTrace) async { - // Currently there are no appropriate buildtools on the simulator trybots as - // normally they compile to ELF and don't need them for compiling assembly - // snapshots. - if (isSimulator || (!Platform.isLinux && !Platform.isMacOS)) return; - - final scriptAssembly = path.join(tempDir, 'dwarf_assembly.S'); - final scriptDwarfAssemblyDebugInfo = - path.join(tempDir, 'dwarf_assembly_info.so'); - final scriptDwarfAssemblySnapshot = path.join(tempDir, 'dwarf_assembly.so'); +Future runAssembly( + String tempDir, String scriptDill) async { + if (skipAssembly != false) return null; + + final asmPath = path.join(tempDir, 'dwarf_assembly.S'); + final debugInfoPath = path.join(tempDir, 'dwarf_assembly_info.so'); + final snapshotPath = path.join(tempDir, 'dwarf_assembly.so'); // We get a separate .dSYM bundle on MacOS. - final scriptDwarfAssemblyDebugSnapshot = - scriptDwarfAssemblySnapshot + (Platform.isMacOS ? '.dSYM' : ''); + var debugSnapshotPath = snapshotPath + (Platform.isMacOS ? '.dSYM' : ''); await run(genSnapshot, [ // We test --dwarf-stack-traces-mode, not --dwarf-stack-traces, because @@ -159,78 +117,74 @@ Future testAssembly( // other flags. This way, we limit the difference between the two // snapshots and also directly test the flag saved as a VM global flag. '--dwarf-stack-traces-mode', - '--save-debugging-info=$scriptDwarfAssemblyDebugInfo', + '--save-debugging-info=$debugInfoPath', '--snapshot-kind=app-aot-assembly', - '--assembly=$scriptAssembly', + '--assembly=$asmPath', scriptDill, ]); - await assembleSnapshot(scriptAssembly, scriptDwarfAssemblySnapshot, - debug: true); + final debugInfo = Dwarf.fromFile(debugInfoPath)!; + + await assembleSnapshot(asmPath, snapshotPath, debug: true); // Run the resulting Dwarf-AOT compiled script. - final assemblyOutput1 = await runTestProgram(dartPrecompiledRuntime, [ + final output = await runTestProgram(dartPrecompiledRuntime, [ '--dwarf-stack-traces-mode', - scriptDwarfAssemblySnapshot, + snapshotPath, scriptDill, ]); - final assemblyOutput2 = await runTestProgram(dartPrecompiledRuntime, [ + final outputWithOppositeFlag = + await runTestProgram(dartPrecompiledRuntime, [ '--no-dwarf-stack-traces-mode', - scriptDwarfAssemblySnapshot, + snapshotPath, scriptDill, ]); - // Check with DWARF in assembled snapshot. - await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2, - scriptDwarfAssemblyDebugSnapshot, - fromAssembly: true); - // Check with DWARF from separate debugging information. - await compareTraces(nonDwarfTrace, assemblyOutput1, assemblyOutput2, - scriptDwarfAssemblyDebugInfo, - fromAssembly: true); - - // Next comes tests for MacOS universal binaries. - if (!Platform.isMacOS) return; - - // Create empty MachO files (just a header) for each of the possible - // architectures. - final emptyFiles = {}; - for (final arch in machOArchNames.values) { - // Don't create an empty file for the current architecture. - if (arch == dartNameForCurrentArchitecture) continue; - final contents = emptyMachOForArchitecture(arch); - Expect.isNotNull(contents); - final emptyPath = path.join(tempDir, "empty_$arch.so"); - await File(emptyPath).writeAsBytes(contents!, flush: true); - emptyFiles[arch] = emptyPath; - } + // Get the shared object path inside the .dSYM after compilation on MacOS. + debugSnapshotPath = MachO.handleDSYM(debugSnapshotPath); + final snapshot = Dwarf.fromFile(debugSnapshotPath)!; + + Dwarf? singleArchSnapshot; + Dwarf? multiArchSnapshot; + if (skipUniversalBinary == false) { + // Create empty MachO files (just a header) for each of the possible + // architectures. + final emptyFiles = {}; + for (final arch in machOArchNames.values) { + // Don't create an empty file for the current architecture. + if (arch == dartNameForCurrentArchitecture) continue; + final contents = emptyMachOForArchitecture(arch)!; + final emptyPath = path.join(tempDir, "empty_$arch.so"); + await File(emptyPath).writeAsBytes(contents, flush: true); + emptyFiles[arch] = emptyPath; + } - Future testUniversalBinary( - String binaryPath, List machoFiles) async { - await run(lipo, [...machoFiles, '-create', '-output', binaryPath]); - await compareTraces( - nonDwarfTrace, assemblyOutput1, assemblyOutput2, binaryPath, - fromAssembly: true); + final singleArchSnapshotPath = path.join(tempDir, "ub-single"); + await run(lipo, [ + debugSnapshotPath, + '-create', + '-output', + singleArchSnapshotPath, + ]); + singleArchSnapshot = Dwarf.fromFile(singleArchSnapshotPath)!; + + final multiArchSnapshotPath = path.join(tempDir, "ub-multiple"); + await run(lipo, [ + ...emptyFiles.values, + debugSnapshotPath, + '-create', + '-output', + multiArchSnapshotPath, + ]); + multiArchSnapshot = Dwarf.fromFile(multiArchSnapshotPath)!; } - final scriptDwarfAssemblyDebugSnapshotFile = - MachO.handleDSYM(scriptDwarfAssemblyDebugSnapshot); - await testUniversalBinary(path.join(tempDir, "ub-single"), - [scriptDwarfAssemblyDebugSnapshotFile]); - await testUniversalBinary(path.join(tempDir, "ub-multiple"), - [...emptyFiles.values, scriptDwarfAssemblyDebugSnapshotFile]); -} - -class DwarfTestOutput { - final List trace; - final int allocateObjectStart; - final int allocateObjectEnd; - - DwarfTestOutput(this.trace, this.allocateObjectStart, this.allocateObjectEnd); + return DwarfAssemblyState(snapshot, debugInfo, output, outputWithOppositeFlag, + singleArchSnapshot, multiArchSnapshot); } Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, - DwarfTestOutput output2, String dwarfPath, + DwarfTestOutput output2, Dwarf dwarf, {bool fromAssembly = false}) async { final header1 = StackTraceHeader.fromLines(output1.trace); print('Header1 = $header1'); @@ -239,22 +193,13 @@ Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, print('Header2 = $header1'); checkHeader(header2); - // Check that translating the DWARF stack trace (without internal frames) - // matches the symbolic stack trace. - print("Reading DWARF info from ${dwarfPath}"); - final dwarf = Dwarf.fromFile(dwarfPath); - if (dwarf == null) { - throw 'No DWARF information at $dwarfPath'; - } - // For DWARF stack traces, we can't guarantee that the stack traces are // textually equal on all platforms, but if we retrieve the PC offsets // out of the stack trace, those should be equal. final tracePCOffsets1 = collectPCOffsets(output1.trace); final tracePCOffsets2 = collectPCOffsets(output2.trace); - Expect.deepEquals(tracePCOffsets1, tracePCOffsets2); - - Expect.isNotEmpty(tracePCOffsets1); + expect(tracePCOffsets2, equals(tracePCOffsets1)); + expect(tracePCOffsets1, isNotEmpty); checkRootUnitAssumptions(output1, output2, dwarf, sampleOffset: tracePCOffsets1.first, matchingBuildIds: !fromAssembly); @@ -278,7 +223,7 @@ Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, final relocatedFromDso1 = absTrace1.map((a) => a - dsoBase1); final relocatedFromDso2 = absTrace2.map((a) => a - dsoBase2); - Expect.deepEquals(relocatedFromDso1, relocatedFromDso2); + expect(relocatedFromDso2, equals(relocatedFromDso1)); // We don't print 'virt' relocated addresses when running assembled snapshots. if (fromAssembly) return; @@ -290,229 +235,13 @@ Future compareTraces(List nonDwarfTrace, DwarfTestOutput output1, final virtTrace1 = explicitVirtualAddresses(output1.trace); final virtTrace2 = explicitVirtualAddresses(output2.trace); - Expect.deepEquals(virtTrace1, virtTrace2); - - Expect.deepEquals( - virtTrace1, tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf))); - Expect.deepEquals( - virtTrace2, tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf))); + expect(virtTrace2, equals(virtTrace1)); - Expect.deepEquals(virtTrace1, relocatedFromDso1); - Expect.deepEquals(virtTrace2, relocatedFromDso2); -} - -void checkHeader(StackTraceHeader header) { - // These should be all available. - Expect.isNotNull(header.vmStart); - Expect.isNotNull(header.isolateStart); - Expect.isNotNull(header.isolateDsoBase); - Expect.isNotNull(header.buildId); - Expect.isNotNull(header.os); - Expect.isNotNull(header.architecture); - Expect.isNotNull(header.usingSimulator); - Expect.isNotNull(header.compressedPointers); -} + expect(tracePCOffsets1.map((o) => o.virtualAddressIn(dwarf)), + equals(virtTrace1)); + expect(tracePCOffsets2.map((o) => o.virtualAddressIn(dwarf)), + equals(virtTrace2)); -void checkRootUnitAssumptions( - DwarfTestOutput output1, DwarfTestOutput output2, Dwarf rootDwarf, - {required PCOffset sampleOffset, bool matchingBuildIds = true}) { - // We run the test program on the same host OS as the test, so any - // PCOffset from the trace should have this information. - Expect.isNotNull(sampleOffset.os); - Expect.isNotNull(sampleOffset.architecture); - Expect.isNotNull(sampleOffset.usingSimulator); - Expect.isNotNull(sampleOffset.compressedPointers); - - Expect.equals(sampleOffset.os, Platform.operatingSystem); - final archString = '${sampleOffset.usingSimulator! ? 'SIM' : ''}' - '${sampleOffset.architecture!.toUpperCase()}' - '${sampleOffset.compressedPointers! ? 'C' : ''}'; - final baseBuildDir = path.basename(buildDir); - Expect.isTrue(baseBuildDir.endsWith(archString), - 'Expected $baseBuildDir to end with $archString'); - - // Check that the build IDs exist in the traces and are the same. - final buildId1 = buildId(output1.trace); - Expect.isFalse(buildId1.isEmpty, 'Could not find build ID in first trace'); - print('Trace 1 build ID: "${buildId1}"'); - final buildId2 = buildId(output2.trace); - Expect.isFalse(buildId2.isEmpty, 'Could not find build ID in second trace'); - print('Trace 2 build ID: "${buildId2}"'); - Expect.equals(buildId1, buildId2); - - if (matchingBuildIds) { - // The build ID in the traces should be the same as the DWARF build ID - // when the ELF was generated by gen_snapshot. - final dwarfBuildId = rootDwarf.buildId(); - Expect.isNotNull(dwarfBuildId); - print('Dwarf build ID: "${dwarfBuildId!}"'); - // We should never generate an all-zero build ID. - Expect.notEquals(dwarfBuildId, "00000000000000000000000000000000"); - // This is a common failure case as well, when HashBitsContainer ends up - // hashing over seemingly empty sections. - Expect.notEquals(dwarfBuildId, "01000000010000000100000001000000"); - Expect.stringEquals(dwarfBuildId, buildId1); - Expect.stringEquals(dwarfBuildId, buildId2); - } - - final allocateObjectStart = output1.allocateObjectStart; - final allocateObjectEnd = output1.allocateObjectEnd; - Expect.equals(allocateObjectStart, output2.allocateObjectStart); - Expect.equals(allocateObjectEnd, output2.allocateObjectEnd); - - checkAllocateObjectOffset(rootDwarf, allocateObjectStart); - // The end of the bare instructions payload may be padded up to word size, - // so check the maximum possible word size (64 bits) before the end. - checkAllocateObjectOffset(rootDwarf, allocateObjectEnd - 8); - // The end should be either in a different stub or not a stub altogether. - checkAllocateObjectOffset(rootDwarf, allocateObjectEnd, expectedValue: false); - // The byte before the start should also be in either a different stub or - // not in a stub altogether. - checkAllocateObjectOffset(rootDwarf, allocateObjectStart - 1, - expectedValue: false); - // Check the midpoint of the stub, as the stub should be large enough that the - // midpoint won't be in any possible padding. - Expect.isTrue(allocateObjectEnd - allocateObjectStart >= 16, - 'midpoint of stub may be in bare payload padding'); - checkAllocateObjectOffset( - rootDwarf, (allocateObjectStart + allocateObjectEnd) ~/ 2); - - print("Successfully matched AllocateObject stub addresses"); - print(""); -} - -void checkAllocateObjectOffset(Dwarf dwarf, int offset, - {bool expectedValue = true}) { - final pcOffset = PCOffset(offset, InstructionsSection.isolate); - print('Offset of tested stub address is $pcOffset'); - final callInfo = - dwarf.callInfoForPCOffset(pcOffset, includeInternalFrames: true); - print('Call info for tested stub address is $callInfo'); - final got = callInfo != null && - callInfo.length == 1 && - callInfo.single is StubCallInfo && - (callInfo.single as StubCallInfo).name.endsWith('AllocateObjectStub'); - Expect.equals( - expectedValue, - got, - 'address is ${expectedValue ? 'not within' : 'within'} ' - 'the AllocateObject stub'); -} - -void checkTranslatedTrace(List nonDwarfTrace, List dwarfTrace) { - final translatedStackFrames = onlySymbolicFrameLines(dwarfTrace); - final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace); - - print('Stack frames from translated non-symbolic stack trace:'); - translatedStackFrames.forEach(print); - print(''); - - print('Stack frames from original symbolic stack trace:'); - originalStackFrames.forEach(print); - print(''); - - Expect.isTrue(translatedStackFrames.length > 0); - Expect.isTrue(originalStackFrames.length > 0); - - // In symbolic mode, we don't store column information to avoid an increase - // in size of CodeStackMaps. Thus, we need to strip any columns from the - // translated non-symbolic stack to compare them via equality. - final columnStrippedTranslated = removeColumns(translatedStackFrames); - - print('Stack frames from translated non-symbolic stack trace, no columns:'); - columnStrippedTranslated.forEach(print); - print(''); - - Expect.deepEquals(columnStrippedTranslated, originalStackFrames); -} - -Future runTestProgram( - String executable, List args) async { - final result = await runHelper(executable, args); - - if (result.exitCode == 0) { - throw 'Command did not fail with non-zero exit code'; - } - Expect.isTrue(result.stdout.isNotEmpty); - Expect.isTrue(result.stderr.isNotEmpty); - - final stdoutLines = LineSplitter.split(result.stdout).toList(); - Expect.isTrue(stdoutLines.length >= 2); - final start = int.parse(stdoutLines[0]); - final end = int.parse(stdoutLines[1]); - - return DwarfTestOutput( - LineSplitter.split(result.stderr).toList(), start, end); -} - -final _buildIdRE = RegExp(r"build_id: '([a-f\d]+)'"); -String buildId(Iterable lines) { - for (final line in lines) { - final match = _buildIdRE.firstMatch(line); - if (match != null) { - return match.group(1)!; - } - } - return ''; -} - -final _symbolicFrameRE = RegExp(r'^#\d+\s+'); - -Iterable onlySymbolicFrameLines(Iterable lines) { - return lines.where((line) => _symbolicFrameRE.hasMatch(line)); -} - -final _columnsRE = RegExp(r'[(](.*:\d+):\d+[)]'); - -Iterable removeColumns(Iterable lines) sync* { - for (final line in lines) { - final match = _columnsRE.firstMatch(line); - if (match != null) { - yield line.replaceRange(match.start, match.end, '(${match.group(1)!})'); - } else { - yield line; - } - } -} - -Iterable parseUsingAddressRegExp(RegExp re, Iterable lines) sync* { - for (final line in lines) { - final match = re.firstMatch(line); - if (match != null) { - yield int.parse(match.group(1)!, radix: 16); - } - } -} - -final _absRE = RegExp(r'abs ([a-f\d]+)'); - -Iterable absoluteAddresses(Iterable lines) => - parseUsingAddressRegExp(_absRE, lines); - -final _virtRE = RegExp(r'virt ([a-f\d]+)'); - -Iterable explicitVirtualAddresses(Iterable lines) => - parseUsingAddressRegExp(_virtRE, lines); - -final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)'); - -Iterable dsoBaseAddresses(Iterable lines) => - parseUsingAddressRegExp(_dsoBaseRE, lines); - -// We only list architectures supported by the current CpuType enum in -// pkg:native_stack_traces/src/macho.dart. -const machOArchNames = { - "ARM": "arm", - "ARM64": "arm64", - "IA32": "ia32", - "X64": "x64", -}; - -String? get dartNameForCurrentArchitecture { - for (final entry in machOArchNames.entries) { - if (buildDir.endsWith(entry.key)) { - return entry.value; - } - } - return null; + expect(relocatedFromDso1, equals(virtTrace1)); + expect(relocatedFromDso2, equals(virtTrace2)); }