use crate::{Diagnostic, Document, FindQuery, Motion, SelectionSet, Severity};
const BLOCK: &str = "fn f() {\n mark = 0\n}\n";
fn block_len() -> u32 {
BLOCK.len() as u32
}
fn block_start(i: usize) -> u32 {
i as u32 * block_len()
}
fn header_end(i: usize) -> u32 {
block_start(i) + BLOCK.find('{').unwrap() as u32 + 1
}
fn brace_off(i: usize) -> u32 {
block_start(i) + BLOCK.find('{').unwrap() as u32
}
fn build(blocks: usize) -> Document {
let mut s = String::with_capacity(blocks * BLOCK.len());
for _ in 0..blocks {
s.push_str(BLOCK);
}
Document::new(&s).unwrap()
}
fn fold_first(d: &mut Document, k: usize) {
for i in 0..k {
d.toggle_fold_opener(brace_off(i));
}
let _ = d.fold_map();
}
fn find_on(d: &mut Document) {
d.set_find_query(Some(FindQuery { text: "mark".into(), case_sensitive: true, ..Default::default() }), 0);
}
fn meter_of(d: &mut Document, op: impl FnOnce(&mut Document)) -> u64 {
crate::perf::reset();
op(d);
crate::perf::meter()
}
enum Budget {
Constant,
Linear,
}
#[track_caller]
fn assert_budget(name: &str, budget: Budget, small: u64, big: u64) {
let ratio = big as f64 / small.max(1) as f64;
eprintln!("[perf_gate] {name:<38} {small:>7} -> {big:>7} ({ratio:.2}x)");
let ok = match budget {
Budget::Constant => big <= small + small / 4 + 256,
Budget::Linear => big <= small * 13 / 5 + 256, };
assert!(ok, "{name}: meter {small} -> {big} ({ratio:.2}x) violates its {} budget",
match budget { Budget::Constant => "O(1)", Budget::Linear => "O(n)" });
}
#[test]
fn multicaret_over_folds_is_linear() {
const BLOCKS: usize = 700;
let (s, b) = (150usize, 300usize);
let cell = |k: usize| {
let mut d = build(BLOCKS);
fold_first(&mut d, k);
let ranges: Vec<(u32, u32)> = (0..k).map(|i| (header_end(i), header_end(i))).collect();
d.set_selections(SelectionSet::from_ranges(&ranges, 0));
meter_of(&mut d, |d| d.insert_text("a"))
};
assert_budget("repro: type, N carets on N folds", Budget::Linear, cell(s), cell(b));
}
#[test]
fn multicaret_over_decorations_is_linear() {
let (s, b) = (150usize, 300usize);
let cell = |k: usize| {
let mut d = build(k); find_on(&mut d);
let ranges: Vec<(u32, u32)> = (0..k).map(|i| (block_start(i), block_start(i))).collect();
d.set_selections(SelectionSet::from_ranges(&ranges, 0));
meter_of(&mut d, |d| d.insert_text("a"))
};
assert_budget("decos: type, N carets, N matches", Budget::Linear, cell(s), cell(b));
}
#[test]
fn multicaret_movement_is_linear() {
const BLOCKS: usize = 700;
let (s, b) = (150usize, 300usize);
let cell = |k: usize| {
let mut d = build(BLOCKS);
let ranges: Vec<(u32, u32)> = (0..k).map(|i| (block_start(i), block_start(i))).collect();
d.set_selections(SelectionSet::from_ranges(&ranges, 0));
meter_of(&mut d, |d| d.move_carets(Motion::Right, false))
};
assert_budget("carets: arrow, N carets", Budget::Linear, cell(s), cell(b));
}
#[test]
fn single_caret_movement_is_size_independent() {
let (s, b) = (400usize, 800usize);
let cell = |blocks: usize| {
let mut d = build(blocks);
d.set_selections(SelectionSet::new(block_start(blocks / 2)));
meter_of(&mut d, |d| d.move_carets(Motion::Right, false))
};
assert_budget("size: arrow, 1 caret", Budget::Constant, cell(s), cell(b));
}
#[test]
fn multicaret_over_folds_allocates_linearly() {
use crate::sum_tree::NODE_ALLOCS;
let (s, b) = (400usize, 1600usize); let per_caret = |k: usize| {
let mut d = build(k);
let opens: Vec<(u32, u32)> = (0..k).map(|i| (brace_off(i), brace_off(i))).collect();
d.set_selections(SelectionSet::from_ranges(&opens, 0));
d.fold_at_carets(false);
d.move_carets(Motion::LineEnd, false);
let _ = d.fold_map();
NODE_ALLOCS.with(|c| c.set(0));
d.insert_text("a");
let _ = d.fold_map();
NODE_ALLOCS.with(std::cell::Cell::get) as f64 / k as f64
};
let (ps, pb) = (per_caret(s), per_caret(b));
eprintln!("[perf_gate] fold+type node_allocs/caret {ps:>7.1} -> {pb:>7.1} ({:.2}x)", pb / ps);
assert!(pb <= ps * 1.8, "fold+type allocates superlinearly: {ps:.1} -> {pb:.1}/caret");
assert!(pb <= 30.0, "fold+type allocates {pb:.1} nodes/caret — a view went per-caret, not batched");
}
#[test]
fn single_fold_edit_is_size_independent() {
let (s, b) = (400usize, 800usize);
let cell = |blocks: usize| {
let mut d = build(blocks);
fold_first(&mut d, 1); d.set_selections(SelectionSet::new(header_end(0)));
meter_of(&mut d, |d| d.insert_text("a"))
};
assert_budget("size: type on 1 fold", Budget::Constant, cell(s), cell(b));
}
fn armed_with_diags(k: usize) -> Document {
use crate::{Diagnostic, Severity};
let mut s = String::with_capacity(12_000);
for _ in 0..6000 {
s.push_str("a\n");
}
let mut d = Document::new(&s).unwrap();
let end = d.text().len() as u32;
d.set_selections(SelectionSet::new(end));
d.type_char('('); let diags: Vec<Diagnostic> =
(0..k as u32).map(|i| Diagnostic::new(i * 2..i * 2 + 1, Severity::Warning, "d")).collect();
d.set_diagnostics(d.revision(), diags);
d
}
#[test]
fn plain_type_with_pair_armed_is_decoration_count_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = armed_with_diags(k);
meter_of(&mut d, |d| d.type_char('x')) };
assert_budget("autoclose: type, pair armed, k diags", Budget::Constant, cell(s), cell(b));
}
#[test]
fn move_caret_with_pair_armed_is_decoration_count_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = armed_with_diags(k);
meter_of(&mut d, |d| d.move_carets(Motion::Right, false))
};
assert_budget("autoclose: arrow, pair armed, k diags", Budget::Constant, cell(s), cell(b));
}
#[test]
fn autoclose_ranges_is_pair_count_independent() {
use crate::decorations::DECORATION_VISITS;
let visits_for = |k: usize| -> u64 {
let d = armed_with_diags(k);
let base = DECORATION_VISITS.with(std::cell::Cell::get);
let _ = d.autoclose_ranges();
DECORATION_VISITS.with(std::cell::Cell::get) - base
};
let (s, b) = (visits_for(1000), visits_for(2000));
eprintln!("[perf_gate] autoclose_ranges visits {s:>7} -> {b:>7}");
assert!(
b <= s + 4,
"autoclose_ranges visited {s} -> {b}: it scanned the bulk decoration store, not the ≤C own store",
);
}
fn find_k(k: usize) -> Document {
let mut d = build(k);
find_on(&mut d);
d
}
#[test]
fn find_count_is_match_count_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = find_k(k);
meter_of(&mut d, |d| {
let _ = d.find_match_count();
})
};
assert_budget("find: count, k matches", Budget::Constant, cell(s), cell(b));
}
#[test]
fn find_navigate_is_match_count_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = find_k(k);
d.set_selections(SelectionSet::new(0));
meter_of(&mut d, |d| {
d.find_next(0); })
};
assert_budget("find: navigate, k matches", Budget::Constant, cell(s), cell(b));
}
#[test]
fn plain_type_with_active_match_is_match_count_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = find_k(k);
d.set_selections(SelectionSet::new(0));
d.find_next(0); let end = d.text().len() as u32;
d.set_selections(SelectionSet::new(end)); meter_of(&mut d, |d| d.type_char('x'))
};
assert_budget("find: type, active match, k matches", Budget::Constant, cell(s), cell(b));
}
#[test]
fn line_scoped_type_is_document_size_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = build(k);
d.set_find_query(
Some(FindQuery { text: ".*".into(), regex: true, ..Default::default() }),
0,
);
let end = d.text().len() as u32;
d.set_selections(SelectionSet::new(end)); meter_of(&mut d, |d| d.type_char('x'))
};
assert_budget("find: type, regex `.*`, k blocks", Budget::Constant, cell(s), cell(b));
}
#[test]
fn whole_word_type_is_document_size_independent() {
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = build(k);
d.set_find_query(
Some(FindQuery { text: "mark".into(), whole_word: true, ..Default::default() }),
0,
);
let end = d.text().len() as u32;
d.set_selections(SelectionSet::new(end));
meter_of(&mut d, |d| d.type_char('x'))
};
assert_budget("find: type, whole-word, k blocks", Budget::Constant, cell(s), cell(b));
}
#[test]
fn diagnostic_overview_is_diag_count_independent() {
let bounds: Vec<u32> = (0..=200u32).map(|i| i * 32).collect();
let (s, b) = (1000usize, 2000usize);
let cell = |k: usize| {
let mut d = build(k); let rev = d.revision();
let diags: Vec<Diagnostic> = (0..k)
.map(|i| Diagnostic::new(block_start(i)..block_start(i) + 1, Severity::Warning, "w"))
.collect();
d.set_diagnostics(rev, diags);
let mut sev = Vec::new();
let mut find = Vec::new();
meter_of(&mut d, |d| d.overview_marks(&bounds, &mut sev, &mut find))
};
assert_budget("overview: marks, k diagnostics", Budget::Constant, cell(s), cell(b));
}