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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! Cold undo-tree node jump cost vs history depth — the regression guard for
//! the "keyframe materialization" item of issue #302.
//!
//! Storage (`src/undo.rs`, Phase 3a): the root holds a full base rope, every
//! other edge holds a reversible `Delta`, and a node's content is reconstructed
//! on demand behind a bounded warm LRU. `u` / `<C-r>` have an adjacent-node fast
//! path (one inverse delta apply), but `g-` / `g+` (`seq_earlier_step` /
//! `seq_later_step`) go through `entry_of` → `materialize`, which walks UP to
//! the nearest ancestor holding content and replays forward deltas. Before
//! keyframes that ancestor could be the root, making one jump O(depth) and a
//! full walk O(depth²); pinning a materialized rope every `KEYFRAME_INTERVAL`
//! nodes bounds a jump at O(K) and — since `materialize` also caches the
//! intermediates it replays — a full walk at O(N).
//!
//! The benches below measure the two shapes that matter:
//!
//! - `cold_jump_back` — hold `g-` / `:earlier 9999`: walk tip → root.
//! - `single_deep_jump` — ONE `g-` deep inside the history (see [`WALKBACK`]
//! for what that isolates before and after keyframes), plus
//! `single_deep_jump_no_drop`, the same jump with the history's teardown
//! outside the timed region — see [`bench_single_deep_jump_no_drop`] for why
//! that is a different number and not a smaller one.
//!
//! All are parameterized over history depth N so the scaling curve is visible.
//! History construction always happens in the SETUP closure, so it is never
//! inside the timed region; the timed closure only makes jump calls.
use ;
use ;
use black_box;
use ;
/// Base document size: ~2000 lines of realistic prose-ish width.
const BASE_LINES: usize = 2_000;
const BASE_WIDTH: usize = 60;
/// History depths to chart. N = 16 is the shallow control point; the rest
/// progressively exceed the warm window (`WARM_CAP`).
const DEPTHS: = ;
/// How far back `single_deep_jump`'s setup walks before the timed jump.
///
/// Held at 20 so the number stays comparable across the keyframe change, but
/// note what it measures on each side. Before keyframes (`WARM_CAP = 16`, only
/// the jump TARGET cached) 20 steps left the warm LRU holding descendants only,
/// so the timed jump replayed the whole ancestor chain from the root base — the
/// cold-jump cost. Now `materialize` caches every intermediate it replays, so
/// walking back pre-warms the nodes ahead of the walk and no sequential `g-`
/// lands cold: the timed jump measures the steady-state per-step cost of holding
/// `g-` instead. The worst-case cold bound (`< KEYFRAME_INTERVAL` delta applies
/// from any node) is asserted directly in
/// `undo.rs::keyframes_bound_the_cold_replay_distance`, which is a sharper tool
/// for it than a wall-clock bench.
const WALKBACK: usize = 20;
/// Build a `View` with a LINEAR undo history of `n` committed nodes.
///
/// One node per edit, using the only public commit path there is — the same
/// pre-edit-snapshot dance `Editor::push_undo_at` + `mutate_edit` performs in
/// `hjkl-engine`, and the same one the in-crate tests use
/// (`buffer.rs::undo_stack_shared_across_views`, `undo.rs::Driver::edit`):
/// push an `UndoEntry` holding the PRE-edit rope/cursor/marks, then mutate the
/// buffer. Timestamps are deterministic (`UNIX_EPOCH + i s`) like the tests'
/// fixtures, so no wall-clock jitter leaks into the history shape.
///
/// Each edit is small and realistic (a short marker inserted at the start of a
/// line), so each edge `Delta` is small — exactly the case keyframes would
/// target, since small deltas are what makes a long replay chain plausible.
/// One `g-` step against `view`, mirroring `Editor::seq_earlier_core`: hand the
/// tree the LIVE rope (stashed into the node being left) and take the restored
/// snapshot back. Returns the restored rope, which becomes the next call's live
/// rope — i.e. what `restore_rope` would have put in the buffer.
/// Hold `g-` from the tip all the way to the root.
///
/// Fresh deep-history `View` per iteration via `iter_batched` setup: walking
/// back mutates `current` and warms the LRU, so a reused tree would measure a
/// warm walk from iteration 2 onward.
/// ONE `g-` deep inside a long history, after an untimed `WALKBACK`-step walk.
///
/// See [`WALKBACK`] for exactly what this measures before and after keyframes.
///
/// N = 16 is excluded: that whole history fits in the warm window, so there is
/// no deep jump to measure there — reporting that point would be a warm number
/// wearing a cold label.
/// The same single deep `g-` as [`bench_single_deep_jump`], with the history's
/// TEARDOWN moved out of the timed region.
///
/// `iter_batched` hands the routine its input BY VALUE, so the input's
/// destructor runs between `Measurement::start` and `end` (criterion 0.8.2,
/// `bencher.rs`: `outputs.extend(inputs.into_iter().map(&mut routine))` sits
/// inside the timed span, while `outputs` is dropped after it). Freeing an
/// N-node `UndoTree` — two `String`s and a rope handle per node — is O(N), so
/// `single_deep_jump` charges an O(depth) teardown to a jump that is not
/// O(depth), and reads as depth-scaling however cheap the jump gets.
/// `iter_batched_ref` is criterion's documented answer: it lends the routine
/// `&mut I` and drops the batch after `end`.
///
/// Kept as a SEPARATE case rather than a fix to `single_deep_jump`, whose
/// numbers are the recorded history of this bench and stay comparable.
criterion_group!;
criterion_main!;