Files
sdk/pkg/dart2bytecode/lib/source_positions.dart
T
Tess Strickland 52cfd29cbb [vm,dynamic_modules] Add RecordCoverage instruction.
The RecordCoverage instruction has an A/E encoding. The A argument
is the type of coverage being recorded, whereas the E argument is
the logical index into the coverage array for updating whether that
source position has been hit.

Also adds new metadata to the bytecode component for the coverage
arrays associated with bytecode containing RecordCoverage instructions
and a new runtime entry for lazily allocate the coverage array for
an interpreted function when needed.

The type of coverage is encoded in the RecordCoverage instruction,
despite being redundant with the information in the coverage array, so that checking whether that type of coverage is currently enabled at
runtime doesn't require either accessing the coverage array (which may
be lazily allocated), forcing allocation of the coverage array just to
discover that type of coverage is currently disabled, or reading the
serialized bytecode component to avoid that forced allocation.

------

Other changes:

Source reporting now treats unexecuted interpreted functions when
not forcing compilation as if they were uncompiled native functions,
so that the source report from running the same code gives the same
result whether using the interpreter or the native compiler.

Bytecode closures are no longer skipped in source reports. Previously
any closure without a context scope was skipped, but bytecode closures
don't have those.

TEST=vm/cc/SourceReport_Coverage

Cq-Include-Trybots: luci.dart.try:vm-dyn-linux-debug-x64-try,vm-aot-dyn-linux-debug-x64-try,vm-aot-dyn-linux-product-x64-try
Change-Id: I7557e5dd4c98331c7ca2f5c867dd5f6d03e9d756
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/501520
Reviewed-by: Alexander Markov <alexmarkov@google.com>
2026-05-19 04:27:39 -07:00

223 lines
7.6 KiB
Dart

// 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.
import 'bytecode_serialization.dart'
show
BufferedWriter,
BufferedReader,
BytecodeDeclaration,
PackedUInt30DeltaEncoder,
PackedUInt30DeltaDecoder,
SLEB128DeltaEncoder,
SLEB128DeltaDecoder;
/// Maintains mapping between bytecode instructions and source positions.
class SourcePositions extends BytecodeDeclaration {
// Special value of fileOffset which marks synthetic code without a source
// position.
static const noSourcePosition = -1;
// The flags encoded into the low bits of the source position.
static const syntheticFlag = 1 << 0;
static const yieldPointFlag = 1 << 1;
static const _numFlags = 2;
static const _flagMask = (1 << _numFlags) - 1;
final _positions = <int>[]; // Pairs (PC, fileOffset).
// Stored separately just to make sure no call to add uses a smaller
// PC offset than the previous call, even if the previous call didn't
// add an entry to the list because the last entry covers it.
int _lastPcAdded = 0;
SourcePositions();
int _encode(int fileOffset, int flags) =>
(flags == 0 || fileOffset == noSourcePosition)
? fileOffset
: -((fileOffset << _numFlags) | flags) - 1;
(int, int) _decode(int encoded) {
if (encoded >= 0 || encoded == noSourcePosition) {
return (encoded, 0);
}
final value = -encoded - 1;
return (value >> _numFlags, value & _flagMask);
}
// Adds a mapping from the PC to the given file offset as long as there's
// no mapping for that PC already, otherwise no change is made. Returns
// whether the requested mapping exists, which can be either because a new
// mapping was created or the mapping already existed before the request.
//
// Marks the source position as synthetic (not to be used by the debugger
// or coverage calculations) if [(flags & syntheticFlag) != 0].
//
// Marks the pc as within a yield point if [(flags & yieldPointFlag) != 0].
//
// Assumes that the pc is greater than or equal to the pc used in the most
// recent call to add, if any.
bool add(int pc, int fileOffset, int flags) {
assert(fileOffset >= 0 || fileOffset == noSourcePosition);
assert((flags & ~_flagMask) == 0);
if (_lastPcAdded > pc) {
throw ArgumentError('Attempt to add entry for $pc after $_lastPcAdded');
}
_lastPcAdded = pc;
final encodedFileOffset = _encode(fileOffset, flags);
if (_positions.isNotEmpty) {
final i = _positions.length - 2;
final lastPc = _positions[i];
final lastFileOffset = _positions[i + 1];
if (lastFileOffset == encodedFileOffset) {
// The last entry covers this PC offset as well, or this is a repeated
// request for the same (pc, offset) mapping.
return true;
}
if (lastPc == pc) {
// There's already a mapping for (pc, lastFileOffset).
return false;
}
}
_positions.add(pc);
_positions.add(encodedFileOffset);
return true;
}
bool get isEmpty => _positions.isEmpty;
bool get isNotEmpty => !isEmpty;
void write(BufferedWriter writer) {
final pairs = _positions.length ~/ 2;
writer.writePackedUInt30(pairs);
final encodePC = new PackedUInt30DeltaEncoder();
final encodeOffset = new SLEB128DeltaEncoder();
for (int i = 0; i < pairs; i++) {
encodePC.write(writer, _positions[2 * i]);
encodeOffset.write(writer, _positions[2 * i + 1]);
}
}
SourcePositions.read(BufferedReader reader) {
final int pairs = reader.readPackedUInt30();
final decodePC = new PackedUInt30DeltaDecoder();
final decodeOffset = new SLEB128DeltaDecoder();
for (int i = 0; i < pairs; i++) {
_positions.add(decodePC.read(reader));
_positions.add(decodeOffset.read(reader));
}
_lastPcAdded = _positions.isEmpty ? 0 : _positions[_positions.length - 2];
}
@override
String toString() => _positions.toString();
Map<int, String> getBytecodeAnnotations() {
final map = <int, String>{};
for (int i = 0; i < _positions.length; i += 2) {
final pc = _positions[i];
final (fileOffset, flags) = _decode(_positions[i + 1]);
String annotation = '';
if ((flags & syntheticFlag) != 0) {
annotation += 'synthetic ';
}
if ((flags & yieldPointFlag) != 0) {
annotation += 'yield point @ ';
}
annotation += 'source position $fileOffset';
// There is at most one entry per PC offset.
assert(map[pc] == null);
map[pc] = annotation;
}
return map;
}
}
/// Keeps file offsets of line starts. This information is used to
/// decode source positions to line/column.
class LineStarts extends BytecodeDeclaration {
final List<int> lineStarts;
LineStarts(this.lineStarts);
void write(BufferedWriter writer) {
writer.writePackedUInt30(lineStarts.length);
final encodeLineStarts = new PackedUInt30DeltaEncoder();
for (int lineStart in lineStarts) {
encodeLineStarts.write(writer, lineStart);
}
}
factory LineStarts.read(BufferedReader reader) {
final decodeLineStarts = new PackedUInt30DeltaDecoder();
final lineStarts = new List<int>.generate(
reader.readPackedUInt30(),
(_) => decodeLineStarts.read(reader),
);
return new LineStarts(lineStarts);
}
@override
String toString() => 'Line starts: $lineStarts';
}
enum RecordedCoverageType {
// Used for most types of coverage.
regular,
// Used when recording that a branch reached a particular target.
branchTarget,
}
/// Keeps types and file offsets of coverage information recorded
/// by RecordCoverage instructions.
///
/// RecordCoverage instructions use indices into the list of types
/// and file offsets collected during generation, and the bytecode reader
/// generates an appropriate coverage array from it at load time.
class RecordedCoverageArray extends BytecodeDeclaration {
final _recordedCoverageMap = <(RecordedCoverageType, int), int>{};
final _recordedCoverageList = <(RecordedCoverageType, int)>[];
RecordedCoverageArray();
bool get isEmpty => _recordedCoverageList.isEmpty;
bool get isNotEmpty => !isEmpty;
// Adds the type and file offset to the list of types and file offsets
// recorded for RecordCoverage instructions. Returns the index into
// the list for use as the argument to the RecordCoverage instruction.
int add(RecordedCoverageType type, int fileOffset) {
final key = (type, fileOffset);
int? index = _recordedCoverageMap[key];
if (index == null) {
index = _recordedCoverageList.length;
_recordedCoverageList.add(key);
_recordedCoverageMap[key] = index;
}
return index;
}
void write(BufferedWriter writer) {
writer.writePackedUInt30(_recordedCoverageList.length);
final encodeFileOffsets = new SLEB128DeltaEncoder();
for (final (type, fileOffset) in _recordedCoverageList) {
writer.writePackedUInt30(type.index);
encodeFileOffsets.write(writer, fileOffset);
}
}
RecordedCoverageArray.read(BufferedReader reader) {
final decodeFileOffsets = new SLEB128DeltaDecoder();
final length = reader.readPackedUInt30();
for (int i = 0; i < length; i++) {
final type = RecordedCoverageType.values[(reader.readPackedUInt30())];
final fileOffset = decodeFileOffsets.read(reader);
final key = (type, fileOffset);
_recordedCoverageList.add(key);
_recordedCoverageMap[key] = i;
}
}
@override
String toString() => _recordedCoverageList.toString();
}