| // Copyright (c) 2017, 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. |
| |
| /// VMOptions=--dwarf-stack-traces --save-debugging-info=dwarf.so |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:native_stack_traces/native_stack_traces.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:expect/expect.dart'; |
| |
| @pragma("vm:prefer-inline") |
| bar() { |
| // Keep the 'throw' and its argument on separate lines. |
| throw // force linebreak with dart format |
| "Hello, Dwarf!"; |
| } |
| |
| @pragma("vm:never-inline") |
| foo() { |
| bar(); |
| } |
| |
| Future<void> main() async { |
| String rawStack = ""; |
| try { |
| foo(); |
| } catch (e, st) { |
| rawStack = st.toString(); |
| } |
| |
| if (path.basenameWithoutExtension(Platform.executable) != |
| "dart_precompiled_runtime") { |
| return; // Not running from an AOT compiled snapshot. |
| } |
| |
| if (Platform.isAndroid) { |
| return; // Generated dwarf.so not available on the test device. |
| } |
| |
| final dwarf = Dwarf.fromFile("dwarf.so")!; |
| |
| await checkStackTrace(rawStack, dwarf, expectedCallsInfo); |
| } |
| |
| Future<void> checkStackTrace(String rawStack, Dwarf dwarf, |
| List<List<DartCallInfo>> expectedCallsInfo) async { |
| print(""); |
| print("Raw stack trace:"); |
| print(rawStack); |
| |
| final rawLines = |
| await Stream.value(rawStack).transform(const LineSplitter()).toList(); |
| |
| final pcOffsets = collectPCOffsets(rawLines).toList(); |
| |
| // We should have at least enough PC addresses to cover the frames we'll be |
| // checking. |
| Expect.isTrue(pcOffsets.length >= expectedCallsInfo.length); |
| |
| final virtualAddresses = |
| pcOffsets.map((o) => dwarf.virtualAddressOf(o)).toList(); |
| |
| // Some double-checks using other information in the non-symbolic stack trace. |
| final dsoBase = dsoBaseAddresses(rawLines).single; |
| final absolutes = absoluteAddresses(rawLines); |
| final relocatedAddresses = absolutes.map((a) => a - dsoBase); |
| final explicits = explicitVirtualAddresses(rawLines); |
| |
| // Explicits will be empty if not generating ELF snapshots directly, which |
| // means we can't depend on virtual addresses in the snapshot lining up with |
| // those in the separate debugging information. |
| if (explicits.isNotEmpty) { |
| // Direct-to-ELF snapshots should have a build ID. |
| Expect.isNotNull(dwarf.buildId); |
| Expect.deepEquals(relocatedAddresses, virtualAddresses); |
| Expect.deepEquals(explicits, virtualAddresses); |
| } |
| |
| final gotCallsInfo = <List<DartCallInfo>>[]; |
| |
| for (final addr in virtualAddresses) { |
| final externalCallInfo = dwarf.callInfoFor(addr); |
| Expect.isNotNull(externalCallInfo); |
| final allCallInfo = dwarf.callInfoFor(addr, includeInternalFrames: true); |
| Expect.isNotNull(allCallInfo); |
| for (final call in externalCallInfo!) { |
| Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}"); |
| Expect.isFalse(call.isInternal); |
| Expect.isTrue(allCallInfo!.contains(call), |
| "External call info ${call} is not among all calls"); |
| } |
| for (final call in allCallInfo!) { |
| if (!call.isInternal) { |
| Expect.isTrue(externalCallInfo.contains(call), |
| "External call info ${call} is not among external calls"); |
| } |
| } |
| gotCallsInfo.add(externalCallInfo.cast<DartCallInfo>().toList()); |
| } |
| |
| print(""); |
| print("Call information for PC addresses:"); |
| for (var i = 0; i < virtualAddresses.length; i++) { |
| print("For PC 0x${virtualAddresses[i].toRadixString(16)}:"); |
| print(" Calls corresponding to user or library code:"); |
| gotCallsInfo[i].forEach((frame) => print(" ${frame}")); |
| } |
| |
| // Remove empty entries which correspond to skipped internal frames. |
| gotCallsInfo.removeWhere((calls) => calls.isEmpty); |
| |
| checkFrames(gotCallsInfo, expectedCallsInfo); |
| |
| final gotSymbolizedLines = await Stream.fromIterable(rawLines) |
| .transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: false)) |
| .toList(); |
| |
| final gotSymbolizedCalls = |
| gotSymbolizedLines.where((s) => s.startsWith('#')).toList(); |
| |
| print(""); |
| print("Symbolized stack trace:"); |
| gotSymbolizedLines.forEach(print); |
| print(""); |
| print("Extracted calls:"); |
| gotSymbolizedCalls.forEach(print); |
| |
| final expectedStrings = extractCallStrings(expectedCallsInfo); |
| // There are two strings in the list for each line in the output. |
| final expectedCallCount = expectedStrings.length ~/ 2; |
| |
| Expect.isTrue(gotSymbolizedCalls.length >= expectedCallCount); |
| |
| // Strip off any unexpected lines, so we can also make sure we didn't get |
| // unexpected calls prior to those calls we expect. |
| final gotCallsTrace = |
| gotSymbolizedCalls.sublist(0, expectedCallCount).join('\n'); |
| |
| Expect.stringContainsInOrder(gotCallsTrace, expectedStrings); |
| } |
| |
| final expectedCallsInfo = <List<DartCallInfo>>[ |
| // The first frame should correspond to the throw in bar, which was inlined |
| // into foo (so we'll get information for two calls for that PC address). |
| [ |
| DartCallInfo( |
| function: "bar", |
| filename: "dwarf_stack_trace_test.dart", |
| line: 17, |
| column: 3, |
| inlined: true), |
| DartCallInfo( |
| function: "foo", |
| filename: "dwarf_stack_trace_test.dart", |
| line: 23, |
| column: 3, |
| inlined: false) |
| ], |
| // The second frame corresponds to call to foo in main. |
| [ |
| DartCallInfo( |
| function: "main", |
| filename: "dwarf_stack_trace_test.dart", |
| line: 29, |
| column: 5, |
| inlined: false) |
| ], |
| // Don't assume anything about any of the frames below the call to foo |
| // in main, as this makes the test too brittle. |
| ]; |
| |
| void checkFrames( |
| List<List<DartCallInfo>> gotInfo, List<List<DartCallInfo>> expectedInfo) { |
| // There may be frames below those we check. |
| Expect.isTrue(gotInfo.length >= expectedInfo.length); |
| |
| // We can't just use deep equality, since we only have the filenames in the |
| // expected version, not the whole path, and we don't really care if |
| // non-positive line numbers match, as long as they're both non-positive. |
| for (var i = 0; i < expectedInfo.length; i++) { |
| for (var j = 0; j < expectedInfo[i].length; j++) { |
| final got = gotInfo[i][j]; |
| final expected = expectedInfo[i][j]; |
| Expect.equals(expected.function, got.function); |
| Expect.equals(expected.inlined, got.inlined); |
| Expect.equals(expected.filename, path.basename(got.filename)); |
| if (expected.isInternal) { |
| Expect.isTrue(got.isInternal); |
| } else { |
| Expect.equals(expected.line, got.line); |
| } |
| } |
| } |
| } |
| |
| List<String> extractCallStrings(List<List<CallInfo>> expectedCalls) { |
| var ret = <String>[]; |
| for (final frame in expectedCalls) { |
| for (final call in frame) { |
| if (call is DartCallInfo) { |
| ret.add(call.function); |
| if (call.isInternal) { |
| ret.add("${call.filename}:??"); |
| } else { |
| ret.add("${call.filename}:${call.line}"); |
| } |
| } |
| } |
| } |
| return ret; |
| } |
| |
| Iterable<int> parseUsingAddressRegExp(RegExp re, Iterable<String> lines) sync* { |
| for (final line in lines) { |
| final match = re.firstMatch(line); |
| if (match == null) continue; |
| final s = match.group(1); |
| if (s == null) continue; |
| yield int.parse(s, radix: 16); |
| } |
| } |
| |
| final _absRE = RegExp(r'abs ([a-f\d]+)'); |
| |
| Iterable<int> absoluteAddresses(Iterable<String> lines) => |
| parseUsingAddressRegExp(_absRE, lines); |
| |
| final _virtRE = RegExp(r'virt ([a-f\d]+)'); |
| |
| Iterable<int> explicitVirtualAddresses(Iterable<String> lines) => |
| parseUsingAddressRegExp(_virtRE, lines); |
| |
| final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)'); |
| |
| Iterable<int> dsoBaseAddresses(Iterable<String> lines) => |
| parseUsingAddressRegExp(_dsoBaseRE, lines); |