use criterion::{BatchSize, Criterion, black_box, criterion_group, criterion_main};
use kimun_notes::components::text_editor::markdown::ParsedBuffer;
use kimun_notes::components::text_editor::parse_incremental::{
WidenResult, compute_damage_range, widen_to_safe,
};
use kimun_notes::components::text_editor::snapshot::EditorSnapshot;
use kimun_notes::ropetext::{Layout, Metrics, RowHints, Text};
use std::num::NonZeroU64;
fn snap_for(lines: &[String], cursor: (usize, usize), generation: u64) -> EditorSnapshot {
let rev = NonZeroU64::new(generation.max(1)).unwrap();
let clamped = if lines.is_empty() {
(0, 0)
} else {
(cursor.0.min(lines.len() - 1), cursor.1)
};
EditorSnapshot::borrowed(lines, clamped, rev)
}
fn warmed_view(
lines: &[String],
cursor: (usize, usize),
rect: ratatui::layout::Rect,
) -> kimun_notes::components::text_editor::view::MarkdownEditorView {
use kimun_notes::components::text_editor::view::MarkdownEditorView;
let mut view = MarkdownEditorView::new();
view.update(&snap_for(lines, cursor, 1), rect);
let text = Text::from(lines.join("\n").as_str());
if let Some(generation) = view.take_pending_full_parse() {
view.install_full_parse(generation, ParsedBuffer::parse(&text));
}
if let Some(job) = view.take_pending_full_layout() {
let hints: Vec<RowHints<'_>> = Vec::new();
let layout = Layout::compute(&job.text, job.width, Metrics::default(), &hints);
view.install_full_layout(job.generation, layout);
}
view
}
fn make_5000_line_buffer() -> Vec<String> {
(0..5000)
.map(|i| {
format!("paragraph number {i} with some sample text to give the parser work to do")
})
.collect()
}
fn bench_full_parse_5000_lines(c: &mut Criterion) {
let lines = make_5000_line_buffer();
c.bench_function("full_parse_5000_lines", |b| {
b.iter(|| {
let pb = ParsedBuffer::parse_lines(black_box(&lines));
black_box(pb);
});
});
}
fn bench_compute_damage_range_5000_lines(c: &mut Criterion) {
let lines = make_5000_line_buffer();
let mut edited = lines.clone();
edited.remove(2500);
c.bench_function("compute_damage_range_backspace_5000_lines", |b| {
b.iter(|| {
let r = compute_damage_range(black_box(&lines), black_box(&edited), 2500);
black_box(r);
});
});
}
fn bench_incremental_paragraph_insert_5000_lines(c: &mut Criterion) {
let lines = make_5000_line_buffer();
let initial_pb = ParsedBuffer::parse_lines(&lines);
let mut edited = lines.clone();
edited[2500].push('x');
c.bench_function("incremental_paragraph_insert_5000_lines", |b| {
b.iter_batched(
|| initial_pb.clone(),
|mut pb| {
let damaged =
compute_damage_range(&lines, &edited, 2500).expect("damaged should be Some");
let widened = match widen_to_safe(&pb.kinds, damaged) {
WidenResult::Widened(r) => r,
WidenResult::FullRebuild => {
panic!("paragraph insert should take incremental path")
}
};
let slice = ParsedBuffer::parse_range_lines(black_box(&edited), widened.clone());
pb.splice(widened, slice);
black_box(pb);
},
BatchSize::SmallInput,
);
});
}
fn bench_incremental_fallback_5000_lines(c: &mut Criterion) {
let lines = make_5000_line_buffer();
let _initial_pb = ParsedBuffer::parse_lines(&lines);
let mut edited = lines.clone();
edited.insert(2500, "```".to_string());
c.bench_function("incremental_fallback_5000_lines", |b| {
b.iter(|| {
let pb = ParsedBuffer::parse_lines(black_box(&edited));
black_box(pb);
});
});
}
fn bench_wrap_5000_lines(c: &mut Criterion) {
let lines = make_5000_line_buffer();
let pb = ParsedBuffer::parse_lines(&lines);
let rendered: Vec<Vec<bool>> = pb.lines.iter().map(|p| p.content_vis.clone()).collect();
c.bench_function("wrap_5000_lines", |b| {
b.iter(|| {
let text = Text::from(lines.join("\n").as_str());
let hints: Vec<RowHints<'_>> = rendered
.iter()
.map(|row| RowHints {
visible: row.as_slice(),
inset: 0,
})
.collect();
let layout = Layout::compute(black_box(&text), 80, Metrics::default(), &hints);
black_box(layout);
});
});
}
fn bench_full_view_update_5000_lines_incremental(c: &mut Criterion) {
use ratatui::layout::Rect;
let lines = make_5000_line_buffer();
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
let warmed = warmed_view(&lines, (2500, 0), rect);
let mut edited = lines.clone();
edited[2500].push('x');
c.bench_function("full_view_update_5000_lines_incremental", |b| {
b.iter_batched(
|| warmed.clone(),
|mut v| {
v.note_damage(2500..2501, 0);
v.update(
&snap_for(black_box(&edited), (2500, edited[2500].len()), 2),
rect,
);
black_box(v);
},
BatchSize::SmallInput,
);
});
}
fn bench_full_view_update_5000_lines_backspace(c: &mut Criterion) {
use ratatui::layout::Rect;
let lines = make_5000_line_buffer();
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
let warmed = warmed_view(&lines, (2500, 0), rect);
let mut edited = lines.clone();
edited[2500].pop();
c.bench_function("full_view_update_5000_lines_backspace", |b| {
b.iter_batched(
|| warmed.clone(),
|mut v| {
v.note_damage(2500..2501, 0);
v.update(
&snap_for(black_box(&edited), (2500, edited[2500].len()), 2),
rect,
);
black_box(v);
},
BatchSize::SmallInput,
);
});
}
fn bench_full_view_update_5000_lines_first_parse(c: &mut Criterion) {
use kimun_notes::components::text_editor::view::MarkdownEditorView;
use ratatui::layout::Rect;
let lines = make_5000_line_buffer();
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
c.bench_function("full_view_update_5000_lines_first_parse", |b| {
b.iter(|| {
let mut v = MarkdownEditorView::new();
v.update(&snap_for(black_box(&lines), (0, 0), 1), rect);
black_box(v);
});
});
}
fn make_heavy_lists_buffer() -> Vec<String> {
let mut out = Vec::with_capacity(571);
for i in 1..=500 {
out.push(format!(
"- list item {i} with text content for editing tests"
));
if i % 7 == 0 {
out.push(String::new());
}
}
out
}
fn bench_full_view_update_heavy_lists_typing(c: &mut Criterion) {
use ratatui::layout::Rect;
let lines = make_heavy_lists_buffer();
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
let target_row = 250.min(lines.len() - 1);
let warmed = warmed_view(&lines, (target_row, 0), rect);
let mut edited = lines.clone();
edited[target_row].push('x');
c.bench_function("full_view_update_heavy_lists_571_typing", |b| {
b.iter_batched(
|| warmed.clone(),
|mut v| {
v.note_damage(target_row..target_row + 1, 0);
v.update(
&snap_for(
black_box(&edited),
(target_row, edited[target_row].len()),
2,
),
rect,
);
black_box(v);
},
BatchSize::SmallInput,
);
});
}
fn bench_full_view_update_heavy_lists_first_parse(c: &mut Criterion) {
use kimun_notes::components::text_editor::view::MarkdownEditorView;
use ratatui::layout::Rect;
let lines = make_heavy_lists_buffer();
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
c.bench_function("full_view_update_heavy_lists_571_first_parse", |b| {
b.iter(|| {
let mut v = MarkdownEditorView::new();
v.update(&snap_for(black_box(&lines), (0, 0), 1), rect);
black_box(v);
});
});
}
fn make_blockquotes_lazy_buffer() -> Vec<String> {
let mut out = Vec::with_capacity(400);
for i in 1..=100 {
out.push(format!("> Blockquote paragraph {i}"));
out.push(format!("lazy continuation line for paragraph {i}"));
out.push("another continuation line".to_string());
out.push(String::new());
}
out
}
fn bench_full_view_update_blockquotes_typing(c: &mut Criterion) {
use ratatui::layout::Rect;
let lines = make_blockquotes_lazy_buffer();
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
let target_row = 200;
let warmed = warmed_view(&lines, (target_row, 0), rect);
let mut edited = lines.clone();
edited[target_row].push('x');
c.bench_function("full_view_update_blockquotes_400_typing", |b| {
b.iter_batched(
|| warmed.clone(),
|mut v| {
v.note_damage(target_row..target_row + 1, 0);
v.update(
&snap_for(
black_box(&edited),
(target_row, edited[target_row].len()),
2,
),
rect,
);
black_box(v);
},
BatchSize::SmallInput,
);
});
}
fn bench_incremental_range_parse_5000_lines(c: &mut Criterion) {
let lines = make_5000_line_buffer();
let pb = ParsedBuffer::parse_lines(&lines);
let mut edited = lines.clone();
edited[2500].push('x');
let text = Text::from(edited.join("\n").as_str());
let damaged = compute_damage_range(&lines, &edited, 2500).expect("damaged should be Some");
let widened = match widen_to_safe(&pb.kinds, damaged) {
WidenResult::Widened(r) => r,
WidenResult::FullRebuild => panic!("a paragraph edit should stay incremental"),
};
c.bench_function("incremental_range_parse_5000_lines", |b| {
b.iter(|| {
let slice = ParsedBuffer::parse_range(black_box(&text), widened.clone());
black_box(slice);
});
});
}
fn bench_newline_vs_typing(c: &mut Criterion) {
use kimun_notes::components::text_editor::view::MarkdownEditorView;
use ratatui::layout::Rect;
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
for rows in [800usize, 5000] {
let lines: Vec<String> = (0..rows)
.map(|i| {
format!("paragraph number {i} with some sample text to give the parser work to do")
})
.collect();
let mid = rows / 2;
let base = Text::from(lines.join("\n").as_str());
let mut typed_lines = lines.clone();
typed_lines[mid].push('x');
let typed = Text::from(typed_lines.join("\n").as_str());
let mut split_lines = lines.clone();
let tail = split_lines[mid].split_off(20);
split_lines.insert(mid + 1, tail);
let split = Text::from(split_lines.join("\n").as_str());
let rev = |n: u64| NonZeroU64::new(n).unwrap();
let mut warmed = MarkdownEditorView::new();
warmed.update(
&EditorSnapshot::of_buffer(base.clone(), (mid, 0), rev(1)),
rect,
);
if let Some(generation) = warmed.take_pending_full_parse() {
warmed.install_full_parse(generation, ParsedBuffer::parse(&base));
}
if let Some(job) = warmed.take_pending_full_layout() {
let hints: Vec<RowHints<'_>> = Vec::new();
let layout = Layout::compute(&job.text, job.width, Metrics::default(), &hints);
warmed.install_full_layout(job.generation, layout);
}
let mut group = c.benchmark_group(format!("keystroke_{rows}_rows"));
group.bench_function("typing", |b| {
b.iter_batched(
|| warmed.clone(),
|mut v| {
v.note_damage(mid..mid + 1, 0);
let snap =
EditorSnapshot::of_buffer(black_box(typed.clone()), (mid, 21), rev(2));
v.update(&snap, rect);
black_box(v);
},
BatchSize::SmallInput,
);
});
group.bench_function("newline", |b| {
b.iter_batched(
|| warmed.clone(),
|mut v| {
v.note_damage(mid..mid + 2, 1);
let snap =
EditorSnapshot::of_buffer(black_box(split.clone()), (mid + 1, 0), rev(2));
v.update(&snap, rect);
black_box(v);
},
BatchSize::SmallInput,
);
});
group.finish();
}
}
criterion_group!(
benches,
bench_newline_vs_typing,
bench_incremental_range_parse_5000_lines,
bench_full_parse_5000_lines,
bench_compute_damage_range_5000_lines,
bench_incremental_paragraph_insert_5000_lines,
bench_incremental_fallback_5000_lines,
bench_wrap_5000_lines,
bench_full_view_update_5000_lines_incremental,
bench_full_view_update_5000_lines_first_parse,
bench_full_view_update_5000_lines_backspace,
bench_full_view_update_heavy_lists_typing,
bench_full_view_update_heavy_lists_first_parse,
bench_full_view_update_blockquotes_typing,
);
criterion_main!(benches);