blob: bed8cfabcef17424798e26ae91c23a6834ea482c [file] [log] [blame] [edit]
// 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 dartfmt
"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<CallInfo>> expectedCallsInfo) async {
final expectedAllCallsInfo = expectedCallsInfo;
final expectedExternalCallInfo = removeInternalCalls(expectedCallsInfo);
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 >= expectedAllCallsInfo.length);
final virtualAddresses =
pcOffsets.map((o) => dwarf.virtualAddressOf(o)).toList();
final externalFramesInfo = <List<CallInfo>>[];
final allFramesInfo = <List<CallInfo>>[];
for (final addr in virtualAddresses) {
externalFramesInfo.add(dwarf.callInfoFor(addr)?.toList());
allFramesInfo
.add(dwarf.callInfoFor(addr, includeInternalFrames: true)?.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:");
externalFramesInfo[i]?.forEach((frame) => print(" ${frame}"));
print(" All calls:");
allFramesInfo[i]?.forEach((frame) => print(" ${frame}"));
}
// Check that our results are also consistent.
checkConsistency(externalFramesInfo, allFramesInfo);
checkFrames(externalFramesInfo, expectedExternalCallInfo);
checkFrames(allFramesInfo, expectedAllCallsInfo);
final externalSymbolizedLines = await Stream.fromIterable(rawLines)
.transform(DwarfStackTraceDecoder(dwarf))
.toList();
final externalSymbolizedCalls =
externalSymbolizedLines.where((s) => s.startsWith('#')).toList();
print("");
print("Symbolized external-only stack trace:");
externalSymbolizedLines.forEach(print);
print("");
print("Extracted calls:");
externalSymbolizedCalls.forEach(print);
final allSymbolizedLines = await Stream.fromIterable(rawLines)
.transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: true))
.toList();
final allSymbolizedCalls =
allSymbolizedLines.where((s) => s.startsWith('#')).toList();
print("");
print("Symbolized full stack trace:");
allSymbolizedLines.forEach(print);
print("");
print("Extracted calls:");
allSymbolizedCalls.forEach(print);
final expectedExternalStrings = extractCallStrings(expectedExternalCallInfo);
// There are two strings in the list for each line in the output.
final expectedExternalCallCount = expectedExternalStrings.length ~/ 2;
final expectedStrings = extractCallStrings(expectedAllCallsInfo);
final expectedCallCount = expectedStrings.length ~/ 2;
Expect.isTrue(externalSymbolizedCalls.length >= expectedExternalCallCount);
Expect.isTrue(allSymbolizedCalls.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 externalCallsTrace =
externalSymbolizedCalls.sublist(0, expectedExternalCallCount).join('\n');
final allCallsTrace =
allSymbolizedCalls.sublist(0, expectedCallCount).join('\n');
Expect.stringContainsInOrder(externalCallsTrace, expectedExternalStrings);
Expect.stringContainsInOrder(allCallsTrace, expectedStrings);
}
final expectedCallsInfo = <List<CallInfo>>[
// 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,
inlined: true),
DartCallInfo(
function: "foo",
filename: "dwarf_stack_trace_test.dart",
line: 23,
inlined: false)
],
// The second frame corresponds to call to foo in main.
[
DartCallInfo(
function: "main",
filename: "dwarf_stack_trace_test.dart",
line: 29,
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.
];
List<List<CallInfo>> removeInternalCalls(List<List<CallInfo>> original) =>
original
.map((frame) => frame.where((call) => !call.isInternal).toList())
.toList();
void checkConsistency(
List<List<CallInfo>> externalFrames, List<List<CallInfo>> allFrames) {
// We should have the same number of frames for both external-only
// and combined call information.
Expect.equals(allFrames.length, externalFrames.length);
for (var frame in externalFrames) {
// There should be no frames in either version where we failed to look up
// call information.
Expect.isNotNull(frame);
// External-only call information should only include call information with
// positive line numbers.
for (var call in frame) {
Expect.isTrue(!call.isInternal);
}
}
for (var frame in allFrames) {
// There should be no frames in either version where we failed to look up
// call information.
Expect.isNotNull(frame);
// All frames in the internal-including version should have at least one
// piece of call information.
Expect.isTrue(frame.isNotEmpty);
}
// The information in the external-only and combined call information should
// be consistent for externally visible calls.
final allFramesStripped = removeInternalCalls(allFrames);
for (var i = 0; i < allFramesStripped.length; i++) {
Expect.listEquals(allFramesStripped[i], externalFrames[i]);
}
}
void checkFrames(
List<List<CallInfo>> framesInfo, List<List<CallInfo>> expectedInfo) {
// There may be frames below those we check.
Expect.isTrue(framesInfo.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 DartCallInfo got = framesInfo[i][j];
final DartCallInfo 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;
}