1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! Performance regression guard for the per-block range index (B2-M1).
//!
//! Before the index, resolving a block's highlight spans scanned the whole range vector, so a
//! snapshot was O(blocks × ranges) and the "incremental" single-block path leaked O(total
//! ranges). On a Lorem-Ipsum-dense document — one flagged range per word, tens of thousands of
//! them — that pinned a core. The index makes both O(ranges-in-block).
//!
//! The gates are **ratios** (loaded ÷ zero-range baseline), measured back-to-back in one
//! process, so machine speed largely cancels and they hold on any CI runner. That is the
//! property that catches a regression to the old behaviour, whose ratios were ≈3.8× for a full
//! snapshot and ≈6× for one keystroke. Absolute times are printed for information but never
//! asserted (they would flake on a shared runner).
//!
//! A **full** snapshot's ratio does not sit near 1, and cannot: the index removed the
//! O(blocks × ranges) scan, not the O(ranges) materialization behind it — every range still
//! becomes a `HighlightSpan` carrying a cloned `HighlightFormat` (120 bytes a span, ≈1.7 MB per
//! snapshot at the density below) that the zero-range baseline never pays for. What is left is a
//! constant factor near 1.7×, whose exact value tracks the allocator and the build profile:
//! macOS is visibly heavier than glibc on tens of thousands of small allocations, in the debug
//! profile CI tests with. So the gate sits between that constant and the pre-index behaviour,
//! rather than just above whatever one machine prints. Only the single-block path really is a
//! rounding error next to its baseline.
use std::time::Instant;
use text_document::{Color, HighlightFormat, HighlightMask, RangeHighlight, TextDocument};
const PARAGRAPHS: usize = 298; // Scene 1 of the reported project
const WORD: &str = "lorem "; // 6 chars incl. the trailing space
const WORDS_PER_PARA: usize = 47;
fn big_doc() -> TextDocument {
let para = WORD.repeat(WORDS_PER_PARA);
let text = vec![para.trim_end(); PARAGRAPHS].join("\n\n");
let doc = TextDocument::new();
doc.set_plain_text(&text).unwrap();
doc
}
/// One flagged range per word across the whole document — the Lorem-Ipsum-vs-English case.
fn one_range_per_word(doc: &TextDocument) -> Vec<RangeHighlight> {
let fmt = HighlightFormat {
background_color: Some(Color {
red: 255,
green: 0,
blue: 0,
alpha: 255,
}),
..Default::default()
};
let mut out = Vec::new();
for e in &doc.snapshot_flow_masked(&HighlightMask::all()).elements {
if let text_document::FlowElementSnapshot::Block(b) = e {
let base = b.position;
let mut off = 0usize;
for word in b.text.split(' ') {
let len = word.chars().count();
if len > 0 {
out.push(RangeHighlight {
start: base + off,
length: len,
format: fmt.clone(),
});
}
off += len + 1; // + the space
}
}
}
out
}
/// The **fastest** of `iters` runs, in milliseconds.
///
/// The minimum, not the mean: on a shared runner every sample carries whatever the scheduler did
/// to it, and an average keeps that noise, while the fastest run estimates the structural cost
/// these gates are about. Over repeated runs of this file the mean snapshot ratio wandered across
/// 1.65–1.83× where the min held 1.69–1.71×.
fn time_min(iters: usize, mut f: impl FnMut()) -> f64 {
f(); // warm
let mut best = f64::INFINITY;
for _ in 0..iters {
let t = Instant::now();
f();
best = best.min(t.elapsed().as_secs_f64() * 1000.0);
}
best
}
#[test]
fn a_full_snapshot_does_not_scale_with_document_range_count() {
let doc = big_doc();
let session = doc.add_range_session();
let ranges = one_range_per_word(&doc);
assert!(
ranges.len() > 10_000,
"expected a dense document, got {} ranges",
ranges.len()
);
// Baseline: no ranges.
doc.set_session_ranges(session, Vec::new());
let base = time_min(20, || {
std::hint::black_box(doc.snapshot_flow());
});
// Loaded: one range per word.
doc.set_session_ranges(session, ranges.clone());
let loaded = time_min(20, || {
std::hint::black_box(doc.snapshot_flow());
});
let ratio = loaded / base;
println!(
"snapshot_flow: base={base:.2}ms loaded({} ranges)={loaded:.2}ms ratio={ratio:.2}×",
ranges.len()
);
assert!(
ratio < 2.5,
"a full snapshot must not scale with the document's range count (was ≈3.8× before the \
index, and sits near 1.7× with it — see the module header on why not 1×); got \
{ratio:.2}× ({loaded:.2}ms vs {base:.2}ms baseline)"
);
}
#[test]
fn one_keystroke_does_not_leak_the_whole_documents_range_count() {
let doc = big_doc();
let session = doc.add_range_session();
let ranges = one_range_per_word(&doc);
// A block in the middle — the block a keystroke in the thick of the document would relayout.
let mid = match &doc.snapshot_flow_masked(&HighlightMask::all()).elements[PARAGRAPHS] {
text_document::FlowElementSnapshot::Block(b) => b.position + 3,
_ => 3,
};
doc.set_session_ranges(session, Vec::new());
let base = time_min(200, || {
std::hint::black_box(doc.snapshot_block_at_position(mid));
});
doc.set_session_ranges(session, ranges.clone());
let loaded = time_min(200, || {
std::hint::black_box(doc.snapshot_block_at_position(mid));
});
let ratio = loaded / base.max(f64::MIN_POSITIVE);
println!(
"one block: base={base:.4}ms loaded={loaded:.4}ms ratio={ratio:.2}× ({} ranges)",
ranges.len()
);
assert!(
ratio < 2.5,
"the single-block path must stay O(ranges-in-block), not leak O(total ranges) (was ≈6× \
before the index); got {ratio:.2}×"
);
}
#[test]
fn building_the_index_at_push_is_cheaper_than_a_snapshot() {
// The index is rebuilt on every push, and pushes cluster on word-boundary keystrokes — the
// latency-sensitive moment. Guard that the build is not itself a new stall: one push
// (including the O(blocks) position scan + the bucketing of ~14k ranges) must cost less than
// a single full snapshot of the same document, so it never dominates a frame.
let doc = big_doc();
let session = doc.add_range_session();
let ranges = one_range_per_word(&doc);
doc.set_session_ranges(session, ranges.clone());
let snapshot = time_min(20, || {
std::hint::black_box(doc.snapshot_flow());
});
let push = time_min(20, || {
doc.set_session_ranges(session, ranges.clone());
});
println!(
"index build: push={push:.2}ms snapshot={snapshot:.2}ms ({} ranges)",
ranges.len()
);
assert!(
push < snapshot * 2.0,
"building the index on a push must not dwarf a snapshot; push={push:.2}ms vs \
snapshot={snapshot:.2}ms"
);
}