use criterion::{BatchSize, BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use std::time::Duration;
use text_document::{
Alignment, BlockFormat, FindOptions, ListStyle, MoveMode, MoveOperation, SelectionType,
TextDocument, TextFormat,
};
const PARAGRAPH: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
const SIZES: &[(usize, &str)] = &[
(1, "small/1para"),
(100, "medium/100para"),
(1000, "large/1000para"),
];
fn make_doc(paragraphs: usize) -> (TextDocument, usize) {
let text: String = (0..paragraphs)
.map(|_| PARAGRAPH)
.collect::<Vec<_>>()
.join("\n");
let len = text.len();
let doc = TextDocument::new();
doc.set_plain_text(&text).unwrap();
(doc, len)
}
fn bench_document_creation(c: &mut Criterion) {
let mut group = c.benchmark_group("creation");
group.bench_function("document_new", |b| {
b.iter(|| {
black_box(TextDocument::new());
});
});
group.bench_function("document_new_with_text", |b| {
b.iter(|| {
let doc = TextDocument::new();
doc.set_plain_text(black_box("Hello, world!")).unwrap();
});
});
group.finish();
}
fn bench_insertion(c: &mut Criterion) {
let mut group = c.benchmark_group("insertion");
group.sample_size(50);
group.measurement_time(Duration::from_secs(10));
for &(n, label) in SIZES {
group.bench_with_input(
BenchmarkId::new("insert_char_at_start", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let (doc, _) = make_doc(n);
let cursor = doc.cursor_at(0);
(doc, cursor)
},
|(_doc, cursor)| cursor.insert_text(black_box("X")).unwrap(),
BatchSize::SmallInput,
);
},
);
group.bench_with_input(
BenchmarkId::new("insert_char_at_middle", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| cursor.insert_text(black_box("X")).unwrap(),
BatchSize::SmallInput,
);
},
);
group.bench_with_input(
BenchmarkId::new("insert_char_at_end", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len);
(doc, cursor)
},
|(_doc, cursor)| cursor.insert_text(black_box("X")).unwrap(),
BatchSize::SmallInput,
);
},
);
group.bench_with_input(BenchmarkId::new("insert_word", label), &n, |b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| cursor.insert_text(black_box("benchmark ")).unwrap(),
BatchSize::SmallInput,
);
});
group.bench_with_input(BenchmarkId::new("insert_paragraph", label), &n, |b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| cursor.insert_text(black_box(PARAGRAPH)).unwrap(),
BatchSize::SmallInput,
);
});
group.bench_with_input(BenchmarkId::new("insert_block", label), &n, |b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| cursor.insert_block().unwrap(),
BatchSize::SmallInput,
);
});
}
group.finish();
}
fn bench_deletion(c: &mut Criterion) {
let mut group = c.benchmark_group("deletion");
group.sample_size(50);
group.measurement_time(Duration::from_secs(10));
for &(n, label) in SIZES {
group.bench_with_input(
BenchmarkId::new("delete_char_forward", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| cursor.delete_char().unwrap(),
BatchSize::SmallInput,
);
},
);
group.bench_with_input(
BenchmarkId::new("delete_char_backward", label),
&n,
|b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| cursor.delete_previous_char().unwrap(),
BatchSize::SmallInput,
);
},
);
group.bench_with_input(BenchmarkId::new("delete_selection", label), &n, |b, &n| {
b.iter_batched(
|| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
let select_len = 100.min(len / 2);
cursor.set_position(len / 2 + select_len, MoveMode::KeepAnchor);
(doc, cursor)
},
|(_doc, cursor)| {
let _ = cursor.remove_selected_text().unwrap();
},
BatchSize::SmallInput,
);
});
}
group.finish();
}
fn bench_cursor_movement(c: &mut Criterion) {
let mut group = c.benchmark_group("cursor_movement");
group.sample_size(50);
group.measurement_time(Duration::from_secs(10));
for &(n, label) in SIZES {
group.bench_with_input(BenchmarkId::new("move_next_char", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let cursor = doc.cursor_at(0);
b.iter(|| {
cursor.set_position(0, MoveMode::MoveAnchor);
black_box(cursor.move_position(
MoveOperation::NextCharacter,
MoveMode::MoveAnchor,
1,
));
});
});
group.bench_with_input(BenchmarkId::new("move_next_word", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let cursor = doc.cursor_at(0);
b.iter(|| {
cursor.set_position(0, MoveMode::MoveAnchor);
black_box(cursor.move_position(MoveOperation::NextWord, MoveMode::MoveAnchor, 1));
});
});
group.bench_with_input(BenchmarkId::new("move_next_block", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let cursor = doc.cursor_at(0);
b.iter(|| {
cursor.set_position(0, MoveMode::MoveAnchor);
black_box(cursor.move_position(MoveOperation::NextBlock, MoveMode::MoveAnchor, 1));
});
});
group.bench_with_input(BenchmarkId::new("move_to_end", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let cursor = doc.cursor_at(0);
b.iter(|| {
cursor.set_position(0, MoveMode::MoveAnchor);
black_box(cursor.move_position(MoveOperation::End, MoveMode::MoveAnchor, 1));
});
});
group.bench_with_input(BenchmarkId::new("move_to_start", label), &n, |b, &n| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len);
b.iter(|| {
cursor.set_position(len, MoveMode::MoveAnchor);
black_box(cursor.move_position(MoveOperation::Start, MoveMode::MoveAnchor, 1));
});
});
group.bench_with_input(BenchmarkId::new("select_word", label), &n, |b, &n| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
b.iter(|| {
cursor.select(SelectionType::WordUnderCursor);
cursor.clear_selection();
});
});
group.bench_with_input(BenchmarkId::new("select_block", label), &n, |b, &n| {
let (doc, len) = make_doc(n);
let cursor = doc.cursor_at(len / 2);
b.iter(|| {
cursor.select(SelectionType::BlockUnderCursor);
cursor.clear_selection();
});
});
}
group.finish();
}
fn bench_search(c: &mut Criterion) {
let mut group = c.benchmark_group("search");
group.sample_size(50);
group.measurement_time(Duration::from_secs(10));
for &(n, label) in SIZES {
group.bench_with_input(BenchmarkId::new("find_first", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let opts = FindOptions::default();
b.iter(|| {
black_box(doc.find(black_box("amet"), 0, &opts).unwrap());
});
});
group.bench_with_input(BenchmarkId::new("find_all", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let opts = FindOptions::default();
b.iter(|| {
black_box(doc.find_all(black_box("amet"), &opts).unwrap());
});
});
group.bench_with_input(BenchmarkId::new("find_regex", label), &n, |b, &n| {
let (doc, _) = make_doc(n);
let opts = FindOptions {
use_regex: true,
..Default::default()
};
b.iter(|| {
black_box(doc.find(black_box("\\b[Ll]orem\\b"), 0, &opts).unwrap());
});
});
group.bench_with_input(
BenchmarkId::new("find_all_case_insensitive", label),
&n,
|b, &n| {
let (doc, _) = make_doc(n);
let opts = FindOptions {
case_sensitive: false,
..Default::default()
};
b.iter(|| {
black_box(doc.find_all(black_box("LOREM"), &opts).unwrap());
});
},
);
}
group.finish();
}
fn bench_undo_redo(c: &mut Criterion) {
let mut group = c.benchmark_group("undo_redo");
group.bench_function("undo_single", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
cursor.insert_text("bench").unwrap();
doc
},
|doc| doc.undo().unwrap(),
BatchSize::SmallInput,
);
});
group.bench_function("redo_single", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
cursor.insert_text("bench").unwrap();
doc.undo().unwrap();
doc
},
|doc| doc.redo().unwrap(),
BatchSize::SmallInput,
);
});
group.bench_function("undo_chain_10", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
for i in 0..10 {
cursor.insert_text(&format!("item{i} ")).unwrap();
}
doc
},
|doc| {
for _ in 0..10 {
doc.undo().unwrap();
}
},
BatchSize::SmallInput,
);
});
group.finish();
}
fn bench_formatting(c: &mut Criterion) {
let mut group = c.benchmark_group("formatting");
group.bench_function("insert_formatted_text", |b| {
b.iter_batched(
|| {
let (doc, _) = make_doc(100);
let cursor = doc.cursor_at(10);
let format = cursor.char_format().unwrap();
(doc, cursor, format)
},
|(_doc, cursor, format)| {
cursor
.insert_formatted_text(black_box("formatted "), &format)
.unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("merge_char_format_bold", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
cursor.set_position(len / 2 + 100, MoveMode::KeepAnchor);
let format = TextFormat {
font_bold: Some(true),
..Default::default()
};
(doc, cursor, format)
},
|(_doc, cursor, format)| {
cursor.merge_char_format(&format).unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("set_block_format_center", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
let format = BlockFormat {
alignment: Some(Alignment::Center),
..Default::default()
};
(doc, cursor, format)
},
|(_doc, cursor, format)| {
cursor.set_block_format(&format).unwrap();
},
BatchSize::SmallInput,
);
});
group.finish();
}
fn bench_tables(c: &mut Criterion) {
let mut group = c.benchmark_group("tables");
group.bench_function("insert_table_3x3", |b| {
b.iter_batched(
|| {
let doc = TextDocument::new();
doc.set_plain_text("Hello world").unwrap();
let cursor = doc.cursor_at(5);
(doc, cursor)
},
|(_doc, cursor)| {
cursor.insert_table(3, 3).unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("insert_table_10x10", |b| {
b.iter_batched(
|| {
let doc = TextDocument::new();
doc.set_plain_text("Hello world").unwrap();
let cursor = doc.cursor_at(5);
(doc, cursor)
},
|(_doc, cursor)| {
cursor.insert_table(10, 10).unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("table_cell_access", |b| {
let doc = TextDocument::new();
doc.set_plain_text("Hello").unwrap();
let cursor = doc.cursor_at(0);
let table = cursor.insert_table(5, 5).unwrap();
b.iter(|| {
black_box(table.cell(2, 2));
});
});
group.bench_function("insert_table_row", |b| {
b.iter_batched(
|| {
let doc = TextDocument::new();
doc.set_plain_text("Hello").unwrap();
let cursor = doc.cursor_at(0);
let table = cursor.insert_table(5, 5).unwrap();
let table_id = table.id();
(doc, cursor, table_id)
},
|(_doc, cursor, table_id)| {
cursor.insert_table_row(table_id, 2).unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("insert_table_column", |b| {
b.iter_batched(
|| {
let doc = TextDocument::new();
doc.set_plain_text("Hello").unwrap();
let cursor = doc.cursor_at(0);
let table = cursor.insert_table(5, 5).unwrap();
let table_id = table.id();
(doc, cursor, table_id)
},
|(_doc, cursor, table_id)| {
cursor.insert_table_column(table_id, 2).unwrap();
},
BatchSize::SmallInput,
);
});
group.finish();
}
fn bench_lists(c: &mut Criterion) {
let mut group = c.benchmark_group("lists");
group.bench_function("create_list", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| {
cursor.create_list(ListStyle::Disc).unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("insert_list", |b| {
b.iter_batched(
|| {
let (doc, len) = make_doc(100);
let cursor = doc.cursor_at(len / 2);
(doc, cursor)
},
|(_doc, cursor)| {
cursor.insert_list(ListStyle::Disc).unwrap();
},
BatchSize::SmallInput,
);
});
group.finish();
}
fn bench_editing_session(c: &mut Criterion) {
let mut group = c.benchmark_group("editing_session");
group.sample_size(30);
group.measurement_time(Duration::from_secs(15));
group.bench_function("mixed_operations_30", |b| {
b.iter_batched(
|| make_doc(100),
|(doc, len)| {
let c1 = doc.cursor_at(len / 4);
c1.insert_text("This is a new sentence. ").unwrap();
c1.move_position(MoveOperation::NextWord, MoveMode::MoveAnchor, 1);
c1.select(SelectionType::WordUnderCursor);
c1.remove_selected_text().unwrap();
c1.insert_block().unwrap();
c1.insert_text("New paragraph after break.").unwrap();
c1.move_position(MoveOperation::PreviousWord, MoveMode::MoveAnchor, 3);
c1.move_position(MoveOperation::NextWord, MoveMode::KeepAnchor, 1);
c1.merge_char_format(&TextFormat {
font_bold: Some(true),
..Default::default()
})
.unwrap();
c1.move_position(MoveOperation::EndOfBlock, MoveMode::MoveAnchor, 1);
c1.insert_text(" Appended at end of block.").unwrap();
c1.move_position(MoveOperation::NextBlock, MoveMode::MoveAnchor, 5);
c1.select(SelectionType::BlockUnderCursor);
c1.insert_text("Replaced block content entirely.").unwrap();
c1.set_block_format(&BlockFormat {
alignment: Some(Alignment::Center),
..Default::default()
})
.unwrap();
c1.move_position(MoveOperation::Start, MoveMode::MoveAnchor, 1);
c1.insert_list(ListStyle::Decimal).unwrap();
c1.insert_text("First item").unwrap();
c1.insert_block().unwrap();
c1.insert_text("Second item").unwrap();
c1.insert_block().unwrap();
c1.insert_text("Third item").unwrap();
c1.move_position(MoveOperation::End, MoveMode::MoveAnchor, 1);
c1.move_position(MoveOperation::PreviousBlock, MoveMode::MoveAnchor, 10);
c1.insert_formatted_text(
"italic insertion ",
&TextFormat {
font_italic: Some(true),
..Default::default()
},
)
.unwrap();
c1.move_position(MoveOperation::NextCharacter, MoveMode::KeepAnchor, 20);
c1.delete_previous_char().unwrap();
for _ in 0..5 {
c1.delete_char().unwrap();
}
let c2 = doc.cursor_at(0);
c2.insert_text("Prepended by cursor2. ").unwrap();
c2.move_position(MoveOperation::NextBlock, MoveMode::MoveAnchor, 2);
c2.insert_block().unwrap();
c2.insert_text("Cursor2 inserted block.").unwrap();
c1.move_position(MoveOperation::NextWord, MoveMode::MoveAnchor, 10);
c1.insert_text("mid-word-jump ").unwrap();
doc.undo().unwrap();
doc.undo().unwrap();
doc.undo().unwrap();
doc.redo().unwrap();
c1.select(SelectionType::LineUnderCursor);
c1.merge_char_format(&TextFormat {
font_bold: Some(true),
font_italic: Some(true),
..Default::default()
})
.unwrap();
},
BatchSize::SmallInput,
);
});
group.bench_function("mixed_operations_30_large", |b| {
b.iter_batched(
|| make_doc(1000),
|(doc, len)| {
let c1 = doc.cursor_at(len / 4);
c1.insert_text("This is a new sentence. ").unwrap();
c1.move_position(MoveOperation::NextWord, MoveMode::MoveAnchor, 1);
c1.select(SelectionType::WordUnderCursor);
c1.remove_selected_text().unwrap();
c1.insert_block().unwrap();
c1.insert_text("New paragraph after break.").unwrap();
c1.move_position(MoveOperation::PreviousWord, MoveMode::MoveAnchor, 3);
c1.move_position(MoveOperation::NextWord, MoveMode::KeepAnchor, 1);
c1.merge_char_format(&TextFormat {
font_bold: Some(true),
..Default::default()
})
.unwrap();
c1.move_position(MoveOperation::EndOfBlock, MoveMode::MoveAnchor, 1);
c1.insert_text(" Appended at end of block.").unwrap();
c1.move_position(MoveOperation::NextBlock, MoveMode::MoveAnchor, 5);
c1.select(SelectionType::BlockUnderCursor);
c1.insert_text("Replaced block content entirely.").unwrap();
c1.set_block_format(&BlockFormat {
alignment: Some(Alignment::Center),
..Default::default()
})
.unwrap();
c1.move_position(MoveOperation::Start, MoveMode::MoveAnchor, 1);
c1.insert_list(ListStyle::Decimal).unwrap();
c1.insert_text("First item").unwrap();
c1.insert_block().unwrap();
c1.insert_text("Second item").unwrap();
c1.insert_block().unwrap();
c1.insert_text("Third item").unwrap();
c1.move_position(MoveOperation::End, MoveMode::MoveAnchor, 1);
c1.move_position(MoveOperation::PreviousBlock, MoveMode::MoveAnchor, 10);
c1.insert_formatted_text(
"italic insertion ",
&TextFormat {
font_italic: Some(true),
..Default::default()
},
)
.unwrap();
c1.move_position(MoveOperation::NextCharacter, MoveMode::KeepAnchor, 20);
c1.delete_previous_char().unwrap();
for _ in 0..5 {
c1.delete_char().unwrap();
}
let c2 = doc.cursor_at(0);
c2.insert_text("Prepended by cursor2. ").unwrap();
c2.move_position(MoveOperation::NextBlock, MoveMode::MoveAnchor, 2);
c2.insert_block().unwrap();
c2.insert_text("Cursor2 inserted block.").unwrap();
c1.move_position(MoveOperation::NextWord, MoveMode::MoveAnchor, 10);
c1.insert_text("mid-word-jump ").unwrap();
doc.undo().unwrap();
doc.undo().unwrap();
doc.undo().unwrap();
doc.redo().unwrap();
c1.select(SelectionType::LineUnderCursor);
c1.merge_char_format(&TextFormat {
font_bold: Some(true),
font_italic: Some(true),
..Default::default()
})
.unwrap();
},
BatchSize::SmallInput,
);
});
group.finish();
}
criterion_group!(creation, bench_document_creation);
criterion_group!(editing, bench_insertion, bench_deletion);
criterion_group!(navigation, bench_cursor_movement, bench_search);
criterion_group!(history, bench_undo_redo);
criterion_group!(
formatting_group,
bench_formatting,
bench_tables,
bench_lists
);
criterion_group!(session, bench_editing_session);
criterion_main!(
creation,
editing,
navigation,
history,
formatting_group,
session
);