[vm/aot] Reland "Keep column information when possible for precompiled mode."

Changes:

Doing this always in precompiled mode meant increased data segment sizes
when CodeSourceMaps are stored, since encoded line/column information is
larger in the LEB-like encoding used. Now we only store column
information when we produce non-symbolic stacks, since the increased
space needed to store the columns is instead in DWARF sections and can
be stripped or elided.

Original description:

Previously, we passed line number information to the stack trace printer
and to DWARF by changing the non-special positions in the CodeSourceMap
to line numbers in precompiled mode. However, doing this lost column
information.

We get the column information back in the majority of cases by encoding
the line number and column information when neither is too large to pack
together into 30 bits. (Here, 20 bits for line and 10 bits for column.)
Otherwise, we just store the line information as before, though due to
using a bit to encode whether column info exists, it's reduced to 30
bits. If the line info is too big for that, we just return kNoSourcePos.

Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-nnbd-linux-release-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-kernel-precomp-linux-product-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-release-simarm_x64-try
Change-Id: Ia8baee71468da6100a170fa305d03059ffd17f78
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/151822
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Tess Strickland 2020-06-19 13:50:03 +00:00 committed by commit-bot@chromium.org
parent a1416976c0
commit 3e40abbc09
16 changed files with 561 additions and 171 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## 0.3.8
- Support columns when present in line number programs.
## 0.3.7
- Added buildId accessor for retrieving GNU build IDs from DWARF files that

View file

@ -313,6 +313,11 @@ class DebugInformationEntry {
int get callLine => this[_AttributeName.callLine] as int;
// We don't assume that call columns are present for backwards compatibility.
int get callColumn => containsKey(_AttributeName.callColumn)
? this[_AttributeName.callColumn] as int
: 0;
List<CallInfo> callInfo(
CompilationUnit unit, LineNumberProgram lineNumberProgram, int address) {
String callFilename(int index) =>
@ -333,7 +338,8 @@ class DebugInformationEntry {
function: unit.nameOfOrigin(abstractOrigin),
inlined: inlined,
filename: callFilename(child.callFileIndex),
line: child.callLine));
line: child.callLine,
column: child.callColumn));
}
}
@ -341,12 +347,14 @@ class DebugInformationEntry {
final filename = lineNumberProgram.filename(address);
final line = lineNumberProgram.lineNumber(address);
final column = lineNumberProgram.column(address);
return [
DartCallInfo(
function: unit.nameOfOrigin(abstractOrigin),
inlined: inlined,
filename: filename,
line: line)
line: line,
column: column)
];
}
@ -980,6 +988,8 @@ class LineNumberProgram {
int lineNumber(int address) => this[address]?.line;
int column(int address) => this[address]?.column;
void writeToStringBuffer(StringBuffer buffer) {
header.writeToStringBuffer(buffer);
@ -1054,8 +1064,14 @@ class DartCallInfo extends CallInfo {
final String function;
final String filename;
final int line;
final int column;
DartCallInfo({this.inlined = false, this.function, this.filename, this.line});
DartCallInfo(
{this.inlined = false,
this.function,
this.filename,
this.line,
this.column});
@override
bool get isInternal => false;
@ -1063,9 +1079,12 @@ class DartCallInfo extends CallInfo {
@override
int get hashCode => _hashFinish(_hashCombine(
_hashCombine(
_hashCombine(_hashCombine(0, inlined.hashCode), function.hashCode),
filename.hashCode),
line.hashCode));
_hashCombine(
_hashCombine(
_hashCombine(0, inlined.hashCode), function.hashCode),
filename.hashCode),
line.hashCode),
column.hashCode));
@override
bool operator ==(Object other) {
@ -1073,13 +1092,29 @@ class DartCallInfo extends CallInfo {
return inlined == other.inlined &&
function == other.function &&
filename == other.filename &&
line == other.line;
line == other.line &&
column == other.column;
}
return false;
}
void writeToStringBuffer(StringBuffer buffer) {
buffer..write(function)..write(' (')..write(filename);
if (line > 0) {
buffer..write(':')..write(line);
if (column > 0) {
buffer..write(':')..write(column);
}
}
buffer.write(')');
}
@override
String toString() => "${function} (${filename}${line <= 0 ? '' : ':$line'})";
String toString() {
final buffer = StringBuffer();
writeToStringBuffer(buffer);
return buffer.toString();
}
}
/// Represents the information for a call site located in a Dart stub.

View file

@ -1,6 +1,6 @@
name: native_stack_traces
description: Utilities for working with non-symbolic stack traces.
version: 0.3.7
version: 0.3.8
homepage: https://github.com/dart-lang/sdk/tree/master/pkg/native_stack_traces

View file

@ -132,7 +132,16 @@ main(List<String> args) async {
Expect.isTrue(translatedStackFrames.length > 0);
Expect.isTrue(originalStackFrames.length > 0);
Expect.deepEquals(translatedStackFrames, originalStackFrames);
// 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);
// Since we compiled directly to ELF, there should be a DSO base address
// in the stack trace header and 'virt' markers in the stack frames.
@ -175,6 +184,19 @@ Iterable<String> onlySymbolicFrameLines(Iterable<String> lines) {
return lines.where((line) => _symbolicFrameRE.hasMatch(line));
}
final _columnsRE = RegExp(r'[(](.*:\d+):\d+[)]');
Iterable<String> removeColumns(Iterable<String> 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<int> parseUsingAddressRegExp(RegExp re, Iterable<String> lines) sync* {
for (final line in lines) {
final match = re.firstMatch(line);

View file

@ -132,7 +132,16 @@ main(List<String> args) async {
Expect.isTrue(translatedStackFrames.length > 0);
Expect.isTrue(originalStackFrames.length > 0);
Expect.deepEquals(translatedStackFrames, originalStackFrames);
// 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);
// Since we compiled directly to ELF, there should be a DSO base address
// in the stack trace header and 'virt' markers in the stack frames.
@ -175,6 +184,19 @@ Iterable<String> onlySymbolicFrameLines(Iterable<String> lines) {
return lines.where((line) => _symbolicFrameRE.hasMatch(line));
}
final _columnsRE = RegExp(r'[(](.*:\d+):\d+[)]');
Iterable<String> removeColumns(Iterable<String> 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<int> parseUsingAddressRegExp(RegExp re, Iterable<String> lines) sync* {
for (final line in lines) {
final match = re.firstMatch(line);

View file

@ -574,16 +574,16 @@ CodeSourceMapPtr CodeSourceMapBuilder::Finalize() {
void CodeSourceMapBuilder::WriteChangePosition(TokenPosition pos) {
stream_.Write<uint8_t>(kChangePosition);
if (FLAG_precompiled_mode) {
intptr_t line = -1;
int32_t loc = TokenPosition::kNoSourcePos;
intptr_t inline_id = buffered_inline_id_stack_.Last();
if (inline_id < inline_id_to_function_.length()) {
const Function* function = inline_id_to_function_[inline_id];
Script& script = Script::Handle(function->script());
line = script.GetTokenLineUsingLineStarts(pos.SourcePosition());
loc = script.GetTokenLocationUsingLineStarts(pos.SourcePosition());
}
stream_.Write<int32_t>(static_cast<int32_t>(line));
stream_.Write(loc);
} else {
stream_.Write<int32_t>(static_cast<int32_t>(pos.value()));
stream_.Write<int32_t>(pos.value());
}
written_token_pos_stack_.Last() = pos;
}

View file

@ -170,14 +170,6 @@ intptr_t Dwarf::AddCode(const Code& orig_code,
return index;
}
intptr_t Dwarf::TokenPositionToLine(const TokenPosition& token_pos) {
// By the point we're creating the DWARF information, the values of
// non-special token positions have been converted to line numbers, so
// we just need to handle special (negative) token positions.
ASSERT(token_pos.value() > TokenPosition::kLast.value());
return token_pos.value() < 0 ? kNoLineInformation : token_pos.value();
}
intptr_t Dwarf::AddFunction(const Function& function) {
RELEASE_ASSERT(!function.IsNull());
FunctionIndexPair* pair = function_to_index_.Lookup(&function);
@ -257,8 +249,6 @@ void Dwarf::WriteAbbreviations(DwarfWriteStream* stream) {
stream->uleb128(DW_FORM_string);
stream->uleb128(DW_AT_decl_file);
stream->uleb128(DW_FORM_udata);
stream->uleb128(DW_AT_decl_line);
stream->uleb128(DW_FORM_udata);
stream->uleb128(DW_AT_inline);
stream->uleb128(DW_FORM_udata);
stream->uleb128(0);
@ -289,6 +279,8 @@ void Dwarf::WriteAbbreviations(DwarfWriteStream* stream) {
stream->uleb128(DW_FORM_udata);
stream->uleb128(DW_AT_call_line);
stream->uleb128(DW_FORM_udata);
stream->uleb128(DW_AT_call_column);
stream->uleb128(DW_FORM_udata);
stream->uleb128(0);
stream->uleb128(0); // End of attributes.
@ -354,8 +346,8 @@ void Dwarf::WriteAbstractFunctions(DwarfWriteStream* stream) {
String& name = String::Handle(zone_);
stream->InitializeAbstractOrigins(functions_.length());
// By the point we're creating DWARF information, scripts have already lost
// their token stream, so we can't look up their line number information.
auto const line = kNoLineInformation;
// their token stream and we can't look up their line number or column
// information, hence the lack of DW_AT_decl_line and DW_AT_decl_column.
for (intptr_t i = 0; i < functions_.length(); i++) {
const Function& function = *(functions_[i]);
name = function.QualifiedUserVisibleName();
@ -367,7 +359,6 @@ void Dwarf::WriteAbstractFunctions(DwarfWriteStream* stream) {
stream->uleb128(kAbstractFunction);
stream->string(name_cstr); // DW_AT_name
stream->uleb128(file); // DW_AT_decl_file
stream->uleb128(line); // DW_AT_decl_line
stream->uleb128(DW_INL_inlined); // DW_AT_inline
stream->uleb128(0); // End of children.
}
@ -513,8 +504,13 @@ void Dwarf::WriteInliningNode(DwarfWriteStream* stream,
stream->OffsetFromSymbol(root_asm_name, node->end_pc_offset);
// DW_AT_call_file
stream->uleb128(file);
intptr_t line = kNoLineInformation;
intptr_t col = kNoColumnInformation;
Script::DecodePrecompiledPosition(token_pos, &line, &col);
// DW_AT_call_line
stream->uleb128(TokenPositionToLine(token_pos));
stream->uleb128(line);
// DW_at_call_column
stream->uleb128(col);
for (InliningNode* child = node->children_head; child != NULL;
child = child->children_next) {
@ -582,10 +578,14 @@ void Dwarf::WriteLineNumberProgram(DwarfWriteStream* stream) {
// 6.2.5 The Line Number Program
// The initial values for the line number program state machine registers
// according to the DWARF standard.
intptr_t previous_pc_offset = 0;
intptr_t previous_file = 1;
intptr_t previous_line = 1;
intptr_t previous_column = 0;
// Other info not stored in the state machine registers.
const char* previous_asm_name = nullptr;
intptr_t previous_pc_offset = 0;
Function& root_function = Function::Handle(zone_);
Script& script = Script::Handle(zone_);
@ -641,12 +641,20 @@ void Dwarf::WriteLineNumberProgram(DwarfWriteStream* stream) {
}
// 2. Update LNP line.
const auto line = TokenPositionToLine(token_positions.Last());
auto const position = token_positions.Last();
intptr_t line = kNoLineInformation;
intptr_t column = kNoColumnInformation;
Script::DecodePrecompiledPosition(position, &line, &column);
if (line != previous_line) {
stream->u1(DW_LNS_advance_line);
stream->sleb128(line - previous_line);
previous_line = line;
}
if (column != previous_column) {
stream->u1(DW_LNS_set_column);
stream->uleb128(column);
previous_column = column;
}
// 3. Emit LNP row if the address register has been updated to a
// non-zero value (dartbug.com/41756).

View file

@ -313,6 +313,7 @@ class Dwarf : public ZoneAllocated {
static const intptr_t DW_LNS_advance_pc = 0x2;
static const intptr_t DW_LNS_advance_line = 0x3;
static const intptr_t DW_LNS_set_file = 0x4;
static const intptr_t DW_LNS_set_column = 0x5;
static const intptr_t DW_LNE_end_sequence = 0x01;
static const intptr_t DW_LNE_set_address = 0x02;
@ -325,10 +326,7 @@ class Dwarf : public ZoneAllocated {
};
static constexpr intptr_t kNoLineInformation = 0;
// Returns the line number or kNoLineInformation if there is no line
// information available for the given token position.
static intptr_t TokenPositionToLine(const TokenPosition& token_pos);
static constexpr intptr_t kNoColumnInformation = 0;
void WriteAbstractFunctions(DwarfWriteStream* stream);
void WriteConcreteFunctions(DwarfWriteStream* stream);

View file

@ -36,26 +36,6 @@ KernelLineStartsReader::KernelLineStartsReader(
}
}
intptr_t KernelLineStartsReader::LineNumberForPosition(
intptr_t position) const {
intptr_t line_count = line_starts_data_.Length();
intptr_t current_start = 0;
for (intptr_t i = 0; i < line_count; ++i) {
current_start += helper_->At(line_starts_data_, i);
if (current_start > position) {
// If current_start is greater than the desired position, it means that
// it is for the line after |position|. However, since line numbers
// start at 1, we just return |i|.
return i;
}
if (current_start == position) {
return i + 1;
}
}
return line_count;
}
void KernelLineStartsReader::LocationForPosition(intptr_t position,
intptr_t* line,
intptr_t* col) const {

View file

@ -140,8 +140,6 @@ class KernelLineStartsReader {
return helper_->At(line_starts_data_, index);
}
intptr_t LineNumberForPosition(intptr_t position) const;
void LocationForPosition(intptr_t position,
intptr_t* line,
intptr_t* col) const;

View file

@ -10755,26 +10755,96 @@ void Script::SetLocationOffset(intptr_t line_offset,
StoreNonPointer(&raw_ptr()->col_offset_, col_offset);
}
// Whether a precompiled (non-special) position contains column information.
using PrecompiledPositionContainsColumn = BitField<int32_t, bool, 0, 1>;
// Can be used if PrecompiledPositionContainsColumn::decode(v) is true.
using PrecompiledPositionColumn =
BitField<int32_t,
uint32_t,
PrecompiledPositionContainsColumn::kNextBit,
10>;
// Can be used if PrecompiledPositionContainsColumn::decode(v) is true.
// Does not include the sign bit, which should be 0 for encoded values.
using PrecompiledPositionLine =
BitField<int32_t,
uint32_t,
PrecompiledPositionColumn::kNextBit,
(sizeof(int32_t) * kBitsPerByte - 1) -
(PrecompiledPositionColumn::bitsize() +
PrecompiledPositionContainsColumn::bitsize())>;
// Can be used if PrecompiledPositionContainsColumn::decode(v) is false.
// Does not include the sign bit, which should be 0 for encoded values.
using PrecompiledPositionLineOnly =
BitField<int32_t,
uint32_t,
PrecompiledPositionContainsColumn::kNextBit,
(sizeof(int32_t) * kBitsPerByte - 1) -
PrecompiledPositionContainsColumn::bitsize()>;
bool Script::DecodePrecompiledPosition(TokenPosition token_pos,
intptr_t* line,
intptr_t* column) {
ASSERT(line != nullptr);
ASSERT(column != nullptr);
auto const value = token_pos.value();
if (value < 0) return false;
// When storing CodeSourceMaps in the snapshot, we just add unencoded lines.
if (!FLAG_dwarf_stack_traces_mode) {
*line = value;
return true;
}
// We encode zero-based offsets from start, so convert back to ordinals.
if (PrecompiledPositionContainsColumn::decode(value)) {
*line = PrecompiledPositionLine::decode(value) + 1;
*column = PrecompiledPositionColumn::decode(value) + 1;
} else {
*line = PrecompiledPositionLineOnly::decode(value) + 1;
}
return true;
}
// Specialized for AOT compilation, which does this lookup for every token
// position that could be part of a stack trace.
intptr_t Script::GetTokenLineUsingLineStarts(
int32_t Script::GetTokenLocationUsingLineStarts(
TokenPosition target_token_pos) const {
if (target_token_pos.IsNoSource()) {
return 0;
}
#if !defined(DART_PRECOMPILED_RUNTIME)
// Negative positions denote positions that do not correspond to Dart code.
if (target_token_pos.value() < 0) return TokenPosition::kNoSourcePos;
Zone* zone = Thread::Current()->zone();
TypedData& line_starts_data = TypedData::Handle(zone, line_starts());
// Scripts loaded from bytecode may have null line_starts().
if (line_starts_data.IsNull()) {
return 0;
}
if (line_starts_data.IsNull()) return TokenPosition::kNoSourcePos;
#if !defined(DART_PRECOMPILED_RUNTIME)
kernel::KernelLineStartsReader line_starts_reader(line_starts_data, zone);
return line_starts_reader.LineNumberForPosition(target_token_pos.value());
#else
return 0;
intptr_t line = -1;
intptr_t col = -1;
line_starts_reader.LocationForPosition(target_token_pos.value(), &line, &col);
// The line and column numbers returned are ordinals, so we shouldn't get 0.
ASSERT(line > 0);
ASSERT(col > 0);
// Only return (unencoded) line information when storing CodeSourceMaps.
if (!FLAG_dwarf_stack_traces_mode) {
if (Utils::IsUint(31, line)) {
return line;
}
return TokenPosition::kNoSourcePos;
}
// Encode the returned line and column numbers as 0-based offsets from start
// instead of ordinal numbers for better encoding.
line -= 1;
col -= 1;
if (PrecompiledPositionLine::is_valid(line) &&
PrecompiledPositionColumn::is_valid(col)) {
return PrecompiledPositionLine::encode(line) |
PrecompiledPositionColumn::encode(col) |
PrecompiledPositionContainsColumn::encode(true);
} else if (PrecompiledPositionLineOnly::is_valid(line)) {
return PrecompiledPositionLineOnly::encode(line) |
PrecompiledPositionContainsColumn::encode(false);
}
#endif // !defined(DART_PRECOMPILED_RUNTIME)
return TokenPosition::kNoSourcePos;
}
#if !defined(DART_PRECOMPILED_RUNTIME)
@ -23897,7 +23967,7 @@ static void PrintSymbolicStackFrame(Zone* zone,
intptr_t line = -1;
intptr_t column = -1;
if (FLAG_precompiled_mode) {
line = token_pos.value();
Script::DecodePrecompiledPosition(token_pos, &line, &column);
} else if (token_pos.IsSourcePosition()) {
ASSERT(!script.IsNull());
script.GetTokenLocation(token_pos.SourcePosition(), &line, &column);

View file

@ -4495,7 +4495,14 @@ class Script : public Object {
void SetLocationOffset(intptr_t line_offset, intptr_t col_offset) const;
intptr_t GetTokenLineUsingLineStarts(TokenPosition token_pos) const;
// Decode line number and column information if present. Returns false if
// this is a special location and thus undecodable.
static bool DecodePrecompiledPosition(TokenPosition token_pos,
intptr_t* line,
intptr_t* column);
// For positions that have line numbers and columns, returns a non-negative
// value. Otherwise, returns -1.
int32_t GetTokenLocationUsingLineStarts(TokenPosition token_pos) const;
void GetTokenLocation(TokenPosition token_pos,
intptr_t* line,
intptr_t* column,

View file

@ -0,0 +1,76 @@
// 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_obfuscate.so --obfuscate
import 'dart:io';
import 'package:native_stack_traces/native_stack_traces.dart';
import 'package:path/path.dart' as path;
import 'dwarf_stack_trace_test.dart' as base;
@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_obfuscate.so");
await base.checkStackTrace(rawStack, dwarf, expectedCallsInfo);
}
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_obfuscate_test.dart",
line: 17,
column: 3,
inlined: true),
DartCallInfo(
function: "foo",
filename: "dwarf_stack_trace_obfuscate_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_obfuscate_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.
];

View file

@ -0,0 +1,228 @@
// 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<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 allCallInfo) {
Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}");
}
Expect.deepEquals(externalCallInfo, allCallInfo);
gotCallsInfo.add(allCallInfo.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}"));
}
checkFrames(gotCallsInfo, expectedCallsInfo);
final gotSymbolizedLines = await Stream.fromIterable(rawLines)
.transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: true))
.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);

View file

@ -24,7 +24,7 @@ foo() {
}
Future<void> main() async {
String rawStack;
String rawStack = "";
try {
foo();
} catch (e, st) {
@ -45,7 +45,7 @@ Future<void> main() async {
await base.checkStackTrace(rawStack, dwarf, expectedCallsInfo);
}
final expectedCallsInfo = <List<CallInfo>>[
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).
[
@ -53,11 +53,13 @@ final expectedCallsInfo = <List<CallInfo>>[
function: "bar",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 17,
column: 3,
inlined: true),
DartCallInfo(
function: "foo",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 23,
column: 3,
inlined: false)
],
// The second frame corresponds to call to foo in main.
@ -66,6 +68,7 @@ final expectedCallsInfo = <List<CallInfo>>[
function: "main",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 29,
column: 5,
inlined: false)
],
// Don't assume anything about any of the frames below the call to foo

View file

@ -24,7 +24,7 @@ foo() {
}
Future<void> main() async {
String rawStack;
String rawStack = "";
try {
foo();
} catch (e, st) {
@ -46,10 +46,7 @@ Future<void> main() async {
}
Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
List<List<CallInfo>> expectedCallsInfo) async {
final expectedAllCallsInfo = expectedCallsInfo;
final expectedExternalCallInfo = removeInternalCalls(expectedCallsInfo);
List<List<DartCallInfo>> expectedCallsInfo) async {
print("");
print("Raw stack trace:");
print(rawStack);
@ -61,7 +58,7 @@ Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
// We should have at least enough PC addresses to cover the frames we'll be
// checking.
Expect.isTrue(pcOffsets.length >= expectedAllCallsInfo.length);
Expect.isTrue(pcOffsets.length >= expectedCallsInfo.length);
final virtualAddresses =
pcOffsets.map((o) => dwarf.virtualAddressOf(o)).toList();
@ -82,13 +79,18 @@ Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
Expect.deepEquals(explicits, virtualAddresses);
}
final externalFramesInfo = <List<CallInfo>>[];
final allFramesInfo = <List<CallInfo>>[];
final gotCallsInfo = <List<DartCallInfo>>[];
for (final addr in virtualAddresses) {
externalFramesInfo.add(dwarf.callInfoFor(addr)?.toList());
allFramesInfo
.add(dwarf.callInfoFor(addr, includeInternalFrames: true)?.toList());
final externalCallInfo = dwarf.callInfoFor(addr);
Expect.isNotNull(externalCallInfo);
final allCallInfo = dwarf.callInfoFor(addr, includeInternalFrames: true);
Expect.isNotNull(allCallInfo);
for (final call in allCallInfo) {
Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}");
}
Expect.deepEquals(externalCallInfo, allCallInfo);
gotCallsInfo.add(allCallInfo.cast<DartCallInfo>().toList());
}
print("");
@ -96,66 +98,40 @@ Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
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}"));
gotCallsInfo[i].forEach((frame) => print(" ${frame}"));
}
// Check that our results are also consistent.
checkConsistency(externalFramesInfo, allFramesInfo);
checkFrames(gotCallsInfo, expectedCallsInfo);
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)
final gotSymbolizedLines = await Stream.fromIterable(rawLines)
.transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: true))
.toList();
final allSymbolizedCalls =
allSymbolizedLines.where((s) => s.startsWith('#')).toList();
final gotSymbolizedCalls =
gotSymbolizedLines.where((s) => s.startsWith('#')).toList();
print("");
print("Symbolized full stack trace:");
allSymbolizedLines.forEach(print);
print("Symbolized stack trace:");
gotSymbolizedLines.forEach(print);
print("");
print("Extracted calls:");
allSymbolizedCalls.forEach(print);
gotSymbolizedCalls.forEach(print);
final expectedExternalStrings = extractCallStrings(expectedExternalCallInfo);
final expectedStrings = extractCallStrings(expectedCallsInfo);
// 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);
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 externalCallsTrace =
externalSymbolizedCalls.sublist(0, expectedExternalCallCount).join('\n');
final allCallsTrace =
allSymbolizedCalls.sublist(0, expectedCallCount).join('\n');
final gotCallsTrace =
gotSymbolizedCalls.sublist(0, expectedCallCount).join('\n');
Expect.stringContainsInOrder(externalCallsTrace, expectedExternalStrings);
Expect.stringContainsInOrder(allCallsTrace, expectedStrings);
Expect.stringContainsInOrder(gotCallsTrace, expectedStrings);
}
final expectedCallsInfo = <List<CallInfo>>[
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).
[
@ -163,11 +139,13 @@ final expectedCallsInfo = <List<CallInfo>>[
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.
@ -176,65 +154,25 @@ final expectedCallsInfo = <List<CallInfo>>[
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.
];
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) {
List<List<DartCallInfo>> gotInfo, List<List<DartCallInfo>> expectedInfo) {
// There may be frames below those we check.
Expect.isTrue(framesInfo.length >= expectedInfo.length);
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 DartCallInfo got = framesInfo[i][j];
final DartCallInfo expected = expectedInfo[i][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));
@ -267,9 +205,10 @@ List<String> extractCallStrings(List<List<CallInfo>> expectedCalls) {
Iterable<int> parseUsingAddressRegExp(RegExp re, Iterable<String> lines) sync* {
for (final line in lines) {
final match = re.firstMatch(line);
if (match != null) {
yield int.parse(match.group(1), radix: 16);
}
if (match == null) continue;
final s = match.group(1);
if (s == null) continue;
yield int.parse(s, radix: 16);
}
}