Skip to content

coverage: Only merge adjacent coverage spans #139966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/rustc_mir_transform/src/coverage/mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub(super) fn extract_all_mapping_info_from_mir<'tcx>(
// When debugging flag `-Zcoverage-options=no-mir-spans` is set, we need
// to give the same treatment to _all_ functions, because `llvm-cov`
// seems to ignore functions that don't have any ordinary code spans.
if let Some(span) = hir_info.fn_sig_span_extended {
if let Some(span) = hir_info.fn_sig_span {
code_mappings.push(CodeMapping { span, bcb: START_BCB });
}
} else {
Expand Down
32 changes: 11 additions & 21 deletions compiler/rustc_mir_transform/src/coverage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ fn inject_statement(mir_body: &mut mir::Body<'_>, counter_kind: CoverageKind, bb
struct ExtractedHirInfo {
function_source_hash: u64,
is_async_fn: bool,
/// The span of the function's signature, extended to the start of `body_span`.
/// The span of the function's signature, if available.
/// Must have the same context and filename as the body span.
fn_sig_span_extended: Option<Span>,
fn_sig_span: Option<Span>,
body_span: Span,
/// "Holes" are regions within the function body (or its expansions) that
/// should not be included in coverage spans for this function
Expand Down Expand Up @@ -308,30 +308,20 @@ fn extract_hir_info<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId) -> ExtractedHir

// The actual signature span is only used if it has the same context and
// filename as the body, and precedes the body.
let fn_sig_span_extended = maybe_fn_sig
.map(|fn_sig| fn_sig.span)
.filter(|&fn_sig_span| {
let source_map = tcx.sess.source_map();
let file_idx = |span: Span| source_map.lookup_source_file_idx(span.lo());

fn_sig_span.eq_ctxt(body_span)
&& fn_sig_span.hi() <= body_span.lo()
&& file_idx(fn_sig_span) == file_idx(body_span)
})
// If so, extend it to the start of the body span.
.map(|fn_sig_span| fn_sig_span.with_hi(body_span.lo()));
let fn_sig_span = maybe_fn_sig.map(|fn_sig| fn_sig.span).filter(|&fn_sig_span| {
let source_map = tcx.sess.source_map();
let file_idx = |span: Span| source_map.lookup_source_file_idx(span.lo());

fn_sig_span.eq_ctxt(body_span)
&& fn_sig_span.hi() <= body_span.lo()
&& file_idx(fn_sig_span) == file_idx(body_span)
});

let function_source_hash = hash_mir_source(tcx, hir_body);

let hole_spans = extract_hole_spans_from_hir(tcx, hir_body);

ExtractedHirInfo {
function_source_hash,
is_async_fn,
fn_sig_span_extended,
body_span,
hole_spans,
}
ExtractedHirInfo { function_source_hash, is_async_fn, fn_sig_span, body_span, hole_spans }
}

fn hash_mir_source<'tcx>(tcx: TyCtxt<'tcx>, hir_body: &'tcx hir::Body<'tcx>) -> u64 {
Expand Down
118 changes: 47 additions & 71 deletions compiler/rustc_mir_transform/src/coverage/spans.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use std::collections::VecDeque;
use std::iter;

use rustc_data_structures::fx::FxHashSet;
use rustc_middle::mir;
use rustc_middle::ty::TyCtxt;
use rustc_span::{DesugaringKind, ExpnKind, MacroKind, Span};
use tracing::{debug, debug_span, instrument};
use tracing::instrument;

use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
use crate::coverage::spans::from_mir::{Hole, RawSpanFromMir, SpanFromMir};
Expand Down Expand Up @@ -42,12 +39,12 @@ pub(super) fn extract_refined_covspans<'tcx>(
return;
}

// Also add the adjusted function signature span, if available.
// Also add the function signature span, if available.
// Otherwise, add a fake span at the start of the body, to avoid an ugly
// gap between the start of the body and the first real span.
// FIXME: Find a more principled way to solve this problem.
covspans.push(SpanFromMir::for_fn_sig(
hir_info.fn_sig_span_extended.unwrap_or_else(|| body_span.shrink_to_lo()),
hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()),
));

// First, perform the passes that need macro information.
Expand Down Expand Up @@ -83,24 +80,17 @@ pub(super) fn extract_refined_covspans<'tcx>(
holes.sort_by(|a, b| compare_spans(a.span, b.span));
holes.dedup_by(|b, a| a.merge_if_overlapping_or_adjacent(b));

// Split the covspans into separate buckets that don't overlap any holes.
let buckets = divide_spans_into_buckets(covspans, &holes);

for covspans in buckets {
let _span = debug_span!("processing bucket", ?covspans).entered();
// Discard any span that overlaps with a hole.
discard_spans_overlapping_holes(&mut covspans, &holes);

let mut covspans = remove_unwanted_overlapping_spans(covspans);
debug!(?covspans, "after removing overlaps");
// Perform more refinement steps after holes have been dealt with.
let mut covspans = remove_unwanted_overlapping_spans(covspans);
covspans.dedup_by(|b, a| a.merge_if_eligible(b));

// Do one last merge pass, to simplify the output.
covspans.dedup_by(|b, a| a.merge_if_eligible(b));
debug!(?covspans, "after merge");

code_mappings.extend(covspans.into_iter().map(|Covspan { span, bcb }| {
// Each span produced by the refiner represents an ordinary code region.
mappings::CodeMapping { span, bcb }
}));
}
code_mappings.extend(covspans.into_iter().map(|Covspan { span, bcb }| {
// Each span produced by the refiner represents an ordinary code region.
mappings::CodeMapping { span, bcb }
}));
}

/// Macros that expand into branches (e.g. `assert!`, `trace!`) tend to generate
Expand Down Expand Up @@ -142,52 +132,36 @@ fn shrink_visible_macro_spans(tcx: TyCtxt<'_>, covspans: &mut Vec<SpanFromMir>)
}
}

/// Uses the holes to divide the given covspans into buckets, such that:
/// - No span in any hole overlaps a bucket (discarding spans if necessary).
/// - The spans in each bucket are strictly after all spans in previous buckets,
/// and strictly before all spans in subsequent buckets.
/// Discard all covspans that overlap a hole.
///
/// The lists of covspans and holes must be sorted.
/// The resulting buckets are sorted relative to each other, and each bucket's
/// contents are sorted.
#[instrument(level = "debug")]
fn divide_spans_into_buckets(input_covspans: Vec<Covspan>, holes: &[Hole]) -> Vec<Vec<Covspan>> {
debug_assert!(input_covspans.is_sorted_by(|a, b| compare_spans(a.span, b.span).is_le()));
/// The lists of covspans and holes must be sorted, and any holes that overlap
/// with each other must have already been merged.
fn discard_spans_overlapping_holes(covspans: &mut Vec<Covspan>, holes: &[Hole]) {
debug_assert!(covspans.is_sorted_by(|a, b| compare_spans(a.span, b.span).is_le()));
debug_assert!(holes.is_sorted_by(|a, b| compare_spans(a.span, b.span).is_le()));
debug_assert!(holes.array_windows().all(|[a, b]| !a.span.overlaps_or_adjacent(b.span)));

let mut curr_hole = 0usize;
let mut overlaps_hole = |covspan: &Covspan| -> bool {
while let Some(hole) = holes.get(curr_hole) {
// Both lists are sorted, so we can permanently skip any holes that
// end before the start of the current span.
if hole.span.hi() <= covspan.span.lo() {
curr_hole += 1;
continue;
}

// Now we're ready to start grouping spans into buckets separated by holes.

let mut input_covspans = VecDeque::from(input_covspans);

// For each hole:
// - Identify the spans that are entirely or partly before the hole.
// - Discard any that overlap with the hole.
// - Add the remaining identified spans to the corresponding bucket.
let mut buckets = (0..holes.len()).map(|_| vec![]).collect::<Vec<_>>();
for (hole, bucket) in holes.iter().zip(&mut buckets) {
bucket.extend(
drain_front_while(&mut input_covspans, |c| c.span.lo() < hole.span.hi())
.filter(|c| !c.span.overlaps(hole.span)),
);
}

// Any remaining spans form their own final bucket, after the final hole.
// (If there were no holes, this will just be all of the initial spans.)
buckets.push(Vec::from(input_covspans));
return hole.span.overlaps(covspan.span);
}

buckets
}
// No holes left, so this covspan doesn't overlap with any holes.
false
};

/// Similar to `.drain(..)`, but stops just before it would remove an item not
/// satisfying the predicate.
fn drain_front_while<'a, T>(
queue: &'a mut VecDeque<T>,
mut pred_fn: impl FnMut(&T) -> bool,
) -> impl Iterator<Item = T> {
iter::from_fn(move || queue.pop_front_if(|x| pred_fn(x)))
covspans.retain(|covspan| !overlaps_hole(covspan));
}

/// Takes one of the buckets of (sorted) spans extracted from MIR, and "refines"
/// Takes a list of sorted spans extracted from MIR, and "refines"
/// those spans by removing spans that overlap in unwanted ways.
#[instrument(level = "debug")]
fn remove_unwanted_overlapping_spans(sorted_spans: Vec<Covspan>) -> Vec<Covspan> {
Expand Down Expand Up @@ -227,19 +201,21 @@ struct Covspan {
}

impl Covspan {
/// If `self` and `other` can be merged (i.e. they have the same BCB),
/// mutates `self.span` to also include `other.span` and returns true.
/// If `self` and `other` can be merged, mutates `self.span` to also
/// include `other.span` and returns true.
///
/// Note that compatible covspans can be merged even if their underlying
/// spans are not overlapping/adjacent; any space between them will also be
/// part of the merged covspan.
/// Two covspans can be merged if they have the same BCB, and they are
/// overlapping or adjacent.
fn merge_if_eligible(&mut self, other: &Self) -> bool {
if self.bcb != other.bcb {
return false;
let eligible_for_merge =
|a: &Self, b: &Self| (a.bcb == b.bcb) && a.span.overlaps_or_adjacent(b.span);

if eligible_for_merge(self, other) {
self.span = self.span.to(other.span);
true
} else {
false
}

self.span = self.span.to(other.span);
true
}
}

Expand Down
28 changes: 18 additions & 10 deletions tests/coverage/abort.cov-map
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Function name: abort::main
Raw bytes (83): 0x[01, 01, 07, 05, 01, 05, 0b, 01, 09, 05, 13, 01, 0d, 05, 1b, 01, 11, 0d, 01, 0d, 01, 01, 1b, 05, 02, 0b, 00, 18, 02, 01, 0c, 00, 19, 09, 00, 1a, 02, 0a, 06, 02, 09, 00, 0a, 02, 02, 0c, 00, 19, 0d, 00, 1a, 00, 31, 0e, 00, 30, 00, 31, 02, 04, 0c, 00, 19, 11, 00, 1a, 00, 31, 16, 00, 30, 00, 31, 02, 01, 09, 00, 17, 01, 02, 05, 01, 02]
Raw bytes (98): 0x[01, 01, 07, 05, 01, 05, 0b, 01, 09, 05, 13, 01, 0d, 05, 1b, 01, 11, 10, 01, 0d, 01, 00, 1c, 01, 01, 09, 00, 16, 01, 00, 19, 00, 1b, 05, 01, 0b, 00, 18, 02, 01, 0c, 00, 19, 09, 00, 1a, 02, 0a, 06, 02, 09, 00, 0a, 02, 02, 0c, 00, 19, 0d, 00, 1a, 00, 31, 0e, 00, 30, 00, 31, 02, 04, 0c, 00, 19, 11, 00, 1a, 00, 31, 16, 00, 30, 00, 31, 02, 01, 09, 00, 17, 01, 02, 05, 00, 0b, 01, 01, 01, 00, 02]
Number of files: 1
- file 0 => global file 1
Number of expressions: 7
Expand All @@ -10,9 +10,11 @@ Number of expressions: 7
- expression 4 operands: lhs = Counter(0), rhs = Counter(3)
- expression 5 operands: lhs = Counter(1), rhs = Expression(6, Add)
- expression 6 operands: lhs = Counter(0), rhs = Counter(4)
Number of file 0 mappings: 13
- Code(Counter(0)) at (prev + 13, 1) to (start + 1, 27)
- Code(Counter(1)) at (prev + 2, 11) to (start + 0, 24)
Number of file 0 mappings: 16
- Code(Counter(0)) at (prev + 13, 1) to (start + 0, 28)
- Code(Counter(0)) at (prev + 1, 9) to (start + 0, 22)
- Code(Counter(0)) at (prev + 0, 25) to (start + 0, 27)
- Code(Counter(1)) at (prev + 1, 11) to (start + 0, 24)
- Code(Expression(0, Sub)) at (prev + 1, 12) to (start + 0, 25)
= (c1 - c0)
- Code(Counter(2)) at (prev + 0, 26) to (start + 2, 10)
Expand All @@ -30,19 +32,25 @@ Number of file 0 mappings: 13
= (c1 - (c0 + c4))
- Code(Expression(0, Sub)) at (prev + 1, 9) to (start + 0, 23)
= (c1 - c0)
- Code(Counter(0)) at (prev + 2, 5) to (start + 1, 2)
- Code(Counter(0)) at (prev + 2, 5) to (start + 0, 11)
- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2)
Highest counter ID seen: c4

Function name: abort::might_abort
Raw bytes (21): 0x[01, 01, 01, 01, 05, 03, 01, 03, 01, 01, 14, 05, 02, 09, 01, 0f, 02, 02, 0c, 03, 02]
Raw bytes (41): 0x[01, 01, 01, 01, 05, 07, 01, 03, 01, 00, 2e, 01, 01, 08, 00, 14, 05, 01, 09, 00, 11, 05, 00, 12, 00, 1f, 05, 01, 09, 00, 0f, 02, 01, 0c, 02, 06, 02, 03, 01, 00, 02]
Number of files: 1
- file 0 => global file 1
Number of expressions: 1
- expression 0 operands: lhs = Counter(0), rhs = Counter(1)
Number of file 0 mappings: 3
- Code(Counter(0)) at (prev + 3, 1) to (start + 1, 20)
- Code(Counter(1)) at (prev + 2, 9) to (start + 1, 15)
- Code(Expression(0, Sub)) at (prev + 2, 12) to (start + 3, 2)
Number of file 0 mappings: 7
- Code(Counter(0)) at (prev + 3, 1) to (start + 0, 46)
- Code(Counter(0)) at (prev + 1, 8) to (start + 0, 20)
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 17)
- Code(Counter(1)) at (prev + 0, 18) to (start + 0, 31)
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 15)
- Code(Expression(0, Sub)) at (prev + 1, 12) to (start + 2, 6)
= (c0 - c1)
- Code(Expression(0, Sub)) at (prev + 3, 1) to (start + 0, 2)
= (c0 - c1)
Highest counter ID seen: c1

19 changes: 13 additions & 6 deletions tests/coverage/assert-ne.cov-map
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
Function name: assert_ne::main
Raw bytes (28): 0x[01, 01, 02, 01, 05, 01, 09, 04, 01, 08, 01, 03, 15, 05, 04, 0d, 00, 13, 02, 02, 0d, 00, 13, 06, 03, 05, 01, 02]
Raw bytes (55): 0x[01, 01, 03, 01, 05, 01, 09, 01, 09, 09, 01, 08, 01, 00, 0a, 01, 01, 05, 00, 0f, 01, 01, 09, 00, 12, 01, 00, 13, 00, 19, 01, 01, 0c, 00, 15, 05, 01, 0d, 00, 13, 02, 02, 0d, 00, 13, 0a, 03, 05, 00, 07, 0a, 01, 01, 00, 02]
Number of files: 1
- file 0 => global file 1
Number of expressions: 2
Number of expressions: 3
- expression 0 operands: lhs = Counter(0), rhs = Counter(1)
- expression 1 operands: lhs = Counter(0), rhs = Counter(2)
Number of file 0 mappings: 4
- Code(Counter(0)) at (prev + 8, 1) to (start + 3, 21)
- Code(Counter(1)) at (prev + 4, 13) to (start + 0, 19)
- expression 2 operands: lhs = Counter(0), rhs = Counter(2)
Number of file 0 mappings: 9
- Code(Counter(0)) at (prev + 8, 1) to (start + 0, 10)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 15)
- Code(Counter(0)) at (prev + 1, 9) to (start + 0, 18)
- Code(Counter(0)) at (prev + 0, 19) to (start + 0, 25)
- Code(Counter(0)) at (prev + 1, 12) to (start + 0, 21)
- Code(Counter(1)) at (prev + 1, 13) to (start + 0, 19)
- Code(Expression(0, Sub)) at (prev + 2, 13) to (start + 0, 19)
= (c0 - c1)
- Code(Expression(1, Sub)) at (prev + 3, 5) to (start + 1, 2)
- Code(Expression(2, Sub)) at (prev + 3, 5) to (start + 0, 7)
= (c0 - c2)
- Code(Expression(2, Sub)) at (prev + 1, 1) to (start + 0, 2)
= (c0 - c2)
Highest counter ID seen: c1

24 changes: 15 additions & 9 deletions tests/coverage/assert.cov-map
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Function name: assert::main
Raw bytes (61): 0x[01, 01, 06, 05, 01, 05, 17, 01, 09, 05, 13, 17, 0d, 01, 09, 09, 01, 09, 01, 01, 1b, 05, 02, 0b, 00, 18, 02, 01, 0c, 00, 1a, 09, 00, 1b, 02, 0a, 06, 02, 13, 00, 20, 0d, 00, 21, 02, 0a, 0e, 02, 09, 00, 0a, 02, 01, 09, 00, 17, 01, 02, 05, 01, 02]
Raw bytes (76): 0x[01, 01, 06, 05, 01, 05, 17, 01, 09, 05, 13, 17, 0d, 01, 09, 0c, 01, 09, 01, 00, 1c, 01, 01, 09, 00, 16, 01, 00, 19, 00, 1b, 05, 01, 0b, 00, 18, 02, 01, 0c, 00, 1a, 09, 00, 1b, 02, 0a, 06, 02, 13, 00, 20, 0d, 00, 21, 02, 0a, 0e, 02, 09, 00, 0a, 02, 01, 09, 00, 17, 01, 02, 05, 00, 0b, 01, 01, 01, 00, 02]
Number of files: 1
- file 0 => global file 1
Number of expressions: 6
Expand All @@ -9,9 +9,11 @@ Number of expressions: 6
- expression 3 operands: lhs = Counter(1), rhs = Expression(4, Add)
- expression 4 operands: lhs = Expression(5, Add), rhs = Counter(3)
- expression 5 operands: lhs = Counter(0), rhs = Counter(2)
Number of file 0 mappings: 9
- Code(Counter(0)) at (prev + 9, 1) to (start + 1, 27)
- Code(Counter(1)) at (prev + 2, 11) to (start + 0, 24)
Number of file 0 mappings: 12
- Code(Counter(0)) at (prev + 9, 1) to (start + 0, 28)
- Code(Counter(0)) at (prev + 1, 9) to (start + 0, 22)
- Code(Counter(0)) at (prev + 0, 25) to (start + 0, 27)
- Code(Counter(1)) at (prev + 1, 11) to (start + 0, 24)
- Code(Expression(0, Sub)) at (prev + 1, 12) to (start + 0, 26)
= (c1 - c0)
- Code(Counter(2)) at (prev + 0, 27) to (start + 2, 10)
Expand All @@ -22,18 +24,22 @@ Number of file 0 mappings: 9
= (c1 - ((c0 + c2) + c3))
- Code(Expression(0, Sub)) at (prev + 1, 9) to (start + 0, 23)
= (c1 - c0)
- Code(Counter(0)) at (prev + 2, 5) to (start + 1, 2)
- Code(Counter(0)) at (prev + 2, 5) to (start + 0, 11)
- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2)
Highest counter ID seen: c3

Function name: assert::might_fail_assert
Raw bytes (21): 0x[01, 01, 01, 01, 05, 03, 01, 04, 01, 02, 0f, 02, 02, 25, 00, 3d, 05, 01, 01, 00, 02]
Raw bytes (36): 0x[01, 01, 01, 01, 05, 06, 01, 04, 01, 00, 28, 01, 01, 05, 00, 0d, 01, 00, 0e, 00, 20, 01, 01, 05, 00, 0f, 02, 00, 25, 00, 3d, 05, 01, 01, 00, 02]
Number of files: 1
- file 0 => global file 1
Number of expressions: 1
- expression 0 operands: lhs = Counter(0), rhs = Counter(1)
Number of file 0 mappings: 3
- Code(Counter(0)) at (prev + 4, 1) to (start + 2, 15)
- Code(Expression(0, Sub)) at (prev + 2, 37) to (start + 0, 61)
Number of file 0 mappings: 6
- Code(Counter(0)) at (prev + 4, 1) to (start + 0, 40)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 13)
- Code(Counter(0)) at (prev + 0, 14) to (start + 0, 32)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 15)
- Code(Expression(0, Sub)) at (prev + 0, 37) to (start + 0, 61)
= (c0 - c1)
- Code(Counter(1)) at (prev + 1, 1) to (start + 0, 2)
Highest counter ID seen: c1
Expand Down
17 changes: 11 additions & 6 deletions tests/coverage/assert_not.cov-map
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
Function name: assert_not::main
Raw bytes (29): 0x[01, 01, 00, 05, 01, 06, 01, 01, 11, 01, 02, 05, 00, 13, 01, 01, 05, 00, 13, 01, 01, 05, 00, 15, 01, 01, 01, 00, 02]
Raw bytes (54): 0x[01, 01, 00, 0a, 01, 06, 01, 00, 0a, 01, 01, 05, 00, 0c, 01, 00, 0d, 00, 11, 01, 01, 05, 00, 0c, 01, 00, 0d, 00, 13, 01, 01, 05, 00, 0c, 01, 00, 0d, 00, 13, 01, 01, 05, 00, 0c, 01, 00, 0d, 00, 15, 01, 01, 01, 00, 02]
Number of files: 1
- file 0 => global file 1
Number of expressions: 0
Number of file 0 mappings: 5
- Code(Counter(0)) at (prev + 6, 1) to (start + 1, 17)
- Code(Counter(0)) at (prev + 2, 5) to (start + 0, 19)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 19)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 21)
Number of file 0 mappings: 10
- Code(Counter(0)) at (prev + 6, 1) to (start + 0, 10)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 12)
- Code(Counter(0)) at (prev + 0, 13) to (start + 0, 17)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 12)
- Code(Counter(0)) at (prev + 0, 13) to (start + 0, 19)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 12)
- Code(Counter(0)) at (prev + 0, 13) to (start + 0, 19)
- Code(Counter(0)) at (prev + 1, 5) to (start + 0, 12)
- Code(Counter(0)) at (prev + 0, 13) to (start + 0, 21)
- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2)
Highest counter ID seen: c0

Loading
Loading