1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-08-28 11:36:17 +03:00

chore(bench): restructure into focused targets, add runner, and tune slow cases (#10)

- Upgrade Criterion 0.5 → 0.8.2
- Split 4 broad bench files into 14 focused targets
- Add Rust runner binary with per-target start/done logging
- Add wrap_render benchmarks (all 4 WrapMode variants × 2 sizes)
- Add undo_redo benchmarks (undo/redo × 3 history depths)
- Apply benchmark_group + SamplingMode::Flat + reduced sample_size on slow insert cases to cut total run time from ~28 min to ~12 min
- Rewrite bench/README.md to document runner, targets, and workflow
Этот коммит содержится в:
srothgan
2026-02-19 00:22:09 +01:00
коммит произвёл GitHub
родитель f03a7b9ec1
Коммит 19151a4076
20 изменённых файлов: 821 добавлений и 351 удалений

Просмотреть файл

@@ -8,26 +8,70 @@ license = "MIT"
[lib]
bench = false
[[bin]]
name = "runner"
path = "src/bin/runner.rs"
[dependencies]
tui-textarea = { package = "tui-textarea-2", path = "..", features = ["no-backend", "search"] }
ratatui = { version = "0.30.0", default-features = false }
[dev-dependencies]
criterion = "0.5"
criterion = "0.8.2"
rand = { version = "0.8.5", features = ["small_rng"] }
[[bench]]
name = "insert"
name = "insert_append"
harness = false
[[bench]]
name = "search"
name = "insert_random"
harness = false
[[bench]]
name = "cursor"
name = "insert_long"
harness = false
[[bench]]
name = "delete"
name = "search_forward"
harness = false
[[bench]]
name = "search_backward"
harness = false
[[bench]]
name = "cursor_char"
harness = false
[[bench]]
name = "cursor_word"
harness = false
[[bench]]
name = "cursor_paragraph"
harness = false
[[bench]]
name = "cursor_edge"
harness = false
[[bench]]
name = "delete_char"
harness = false
[[bench]]
name = "delete_word"
harness = false
[[bench]]
name = "delete_line"
harness = false
[[bench]]
name = "wrap_render"
harness = false
[[bench]]
name = "undo_redo"
harness = false

Просмотреть файл

@@ -1,34 +1,74 @@
Benchmarks for tui-textarea using [Criterion.rs][criterion].
To run all benchmarks:
## Running all benchmarks (Rust runner — recommended)
The Rust runner executes every benchmark target sequentially and prints
per-target start time, exit status, and wall duration.
```sh
cargo bench --benches
cargo run -p tui-textarea-bench --bin runner
```
To run specific benchmark suite:
Example output:
```
[1/12] START insert_append at 2026-02-18T12:00:00Z
[1/12] DONE insert_append status=ok duration=42.31s
[2/12] START insert_random at 2026-02-18T12:00:42Z
...
All 14 benchmark targets completed successfully.
```
## Running a single benchmark target
```sh
cargo bench --bench insert
cargo bench -p tui-textarea-bench --bench insert_append
```
To filter benchmarks:
Available targets:
| Target | What it measures |
|--------------------|-------------------------------------------|
| `insert_append` | Appending lorem lines sequentially |
| `insert_random` | Inserting lorem lines at random positions |
| `insert_long` | Typing a long line without newlines |
| `search_forward` | Forward regex search |
| `search_backward` | Backward regex search |
| `cursor_char` | Character-level cursor movement |
| `cursor_word` | Word-level cursor movement |
| `cursor_paragraph` | Paragraph-level cursor movement |
| `cursor_edge` | Head/end and top/bottom jumps |
| `delete_char` | Deleting one character at a time |
| `delete_word` | Deleting one word at a time |
| `delete_line` | Deleting to line head |
| `wrap_render` | Rendering with Word/Glyph/WordOrGlyph wrap modes |
| `undo_redo` | Undo and redo traversal at varying history depths |
## Filtering within a target
Pass a filter string after `--` to run only matching cases:
```sh
cargo bench append::1_lorem
cargo bench -p tui-textarea-bench --bench insert_append -- append::10_lorem
```
To compare benchmark results with [critcmp][]:
## Comparing branches with [critcmp][]
```sh
git checkout main
cargo bench -- --save-baseline base
cargo bench -p tui-textarea-bench -- --save-baseline base
git checkout your-feature
cargo bench -- --save-baseline change
cargo bench -p tui-textarea-bench -- --save-baseline change
critcmp base change
```
## Note on `cargo test -p tui-textarea-bench`
Running `cargo test` on this package is a smoke test only — it verifies
that the benchmark code compiles and runs without panicking, but produces
no timing data. Use `cargo bench` or the Rust runner for real measurements.
[criterion]: https://github.com/bheisler/criterion.rs
[critcmp]: https://github.com/BurntSushi/critcmp

Просмотреть файл

@@ -1,171 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[derive(Clone, Copy)]
enum Restore {
TopLeft,
BottomLeft,
BottomRight,
None,
}
impl Restore {
fn cursor_move(self) -> Option<CursorMove> {
match self {
Self::TopLeft => Some(CursorMove::Jump(0, 0)),
Self::BottomLeft => Some(CursorMove::Jump(u16::MAX, 0)),
Self::BottomRight => Some(CursorMove::Jump(u16::MAX, u16::MAX)),
Self::None => None,
}
}
}
fn prepare_textarea() -> TextArea<'static> {
let mut lines = Vec::with_capacity(LOREM.len() * 2 + 1);
lines.extend(LOREM.iter().map(|s| s.to_string()));
lines.push("".to_string());
lines.extend(LOREM.iter().map(|s| s.to_string()));
TextArea::new(lines)
}
fn run(
mut textarea: TextArea<'_>,
moves: &[CursorMove],
restore: Restore,
repeat: usize,
) -> (usize, usize) {
let mut term = dummy_terminal();
let mut prev = textarea.cursor();
for _ in 0..repeat {
for m in moves {
textarea.move_cursor(*m);
term.draw_textarea(&textarea);
}
if let Some(m) = restore.cursor_move() {
if textarea.cursor() == prev {
textarea.move_cursor(m);
prev = textarea.cursor();
}
}
}
textarea.cursor()
}
fn move_char(c: &mut Criterion) {
let textarea = prepare_textarea();
c.bench_function("cursor::char::forward", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::Forward],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::char::back", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::Back],
Restore::BottomRight,
1000,
))
})
});
c.bench_function("cursor::char::down", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::Down],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::char::up", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::Up],
Restore::BottomLeft,
1000,
))
})
});
}
fn move_word(c: &mut Criterion) {
let textarea = prepare_textarea();
c.bench_function("cursor::word::forward", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::WordForward],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::word::back", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::WordBack],
Restore::BottomRight,
1000,
))
})
});
}
fn move_paragraph(c: &mut Criterion) {
let textarea = prepare_textarea();
c.bench_function("cursor::paragraph::down", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::ParagraphForward],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::paragraph::up", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::ParagraphBack],
Restore::BottomLeft,
1000,
))
})
});
}
fn move_edge(c: &mut Criterion) {
let textarea = prepare_textarea();
c.bench_function("cursor::edge::head_end", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::End, CursorMove::Head],
Restore::None,
500,
))
})
});
c.bench_function("cursor::edge::top_bottom", |b| {
b.iter(|| {
black_box(run(
textarea.clone(),
&[CursorMove::Bottom, CursorMove::Top],
Restore::None,
500,
))
})
});
}
criterion_group!(cursor, move_char, move_word, move_paragraph, move_edge);
criterion_main!(cursor);

50
bench/benches/cursor_char.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,50 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::CursorMove;
use tui_textarea_bench::{prepare_cursor_textarea, run_cursor, Restore};
fn bench(c: &mut Criterion) {
let textarea = prepare_cursor_textarea();
c.bench_function("cursor::char::forward", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::Forward],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::char::back", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::Back],
Restore::BottomRight,
1000,
))
})
});
c.bench_function("cursor::char::down", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::Down],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::char::up", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::Up],
Restore::BottomLeft,
1000,
))
})
});
}
criterion_group!(cursor_char, bench);
criterion_main!(cursor_char);

30
bench/benches/cursor_edge.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::CursorMove;
use tui_textarea_bench::{prepare_cursor_textarea, run_cursor, Restore};
fn bench(c: &mut Criterion) {
let textarea = prepare_cursor_textarea();
c.bench_function("cursor::edge::head_end", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::End, CursorMove::Head],
Restore::None,
500,
))
})
});
c.bench_function("cursor::edge::top_bottom", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::Bottom, CursorMove::Top],
Restore::None,
500,
))
})
});
}
criterion_group!(cursor_edge, bench);
criterion_main!(cursor_edge);

30
bench/benches/cursor_paragraph.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::CursorMove;
use tui_textarea_bench::{prepare_cursor_textarea, run_cursor, Restore};
fn bench(c: &mut Criterion) {
let textarea = prepare_cursor_textarea();
c.bench_function("cursor::paragraph::down", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::ParagraphForward],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::paragraph::up", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::ParagraphBack],
Restore::BottomLeft,
1000,
))
})
});
}
criterion_group!(cursor_paragraph, bench);
criterion_main!(cursor_paragraph);

30
bench/benches/cursor_word.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::CursorMove;
use tui_textarea_bench::{prepare_cursor_textarea, run_cursor, Restore};
fn bench(c: &mut Criterion) {
let textarea = prepare_cursor_textarea();
c.bench_function("cursor::word::forward", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::WordForward],
Restore::TopLeft,
1000,
))
})
});
c.bench_function("cursor::word::back", |b| {
b.iter(|| {
std::hint::black_box(run_cursor(
textarea.clone(),
&[CursorMove::WordBack],
Restore::BottomRight,
1000,
))
})
});
}
criterion_group!(cursor_word, bench);
criterion_main!(cursor_word);

Просмотреть файл

@@ -2,25 +2,13 @@ use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[derive(Clone, Copy)]
enum Kind {
Char,
Word,
Line,
}
#[inline]
fn run(textarea: &TextArea<'_>, kind: Kind) {
fn run(textarea: &TextArea<'_>) {
let mut term = dummy_terminal();
let mut t = textarea.clone();
t.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
for _ in 0..100 {
let modified = match kind {
Kind::Char => t.delete_char(),
Kind::Word => t.delete_word(),
Kind::Line => t.delete_line_by_head(),
};
if !modified {
if !t.delete_char() {
t = textarea.clone();
t.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
}
@@ -34,11 +22,8 @@ fn bench(c: &mut Criterion) {
lines.extend(LOREM.iter().map(|s| s.to_string()));
}
let textarea = TextArea::new(lines);
c.bench_function("delete::char", |b| b.iter(|| run(&textarea, Kind::Char)));
c.bench_function("delete::word", |b| b.iter(|| run(&textarea, Kind::Word)));
c.bench_function("delete::line", |b| b.iter(|| run(&textarea, Kind::Line)));
c.bench_function("delete::char", |b| b.iter(|| run(&textarea)));
}
criterion_group!(delete, bench);
criterion_main!(delete);
criterion_group!(delete_char, bench);
criterion_main!(delete_char);

29
bench/benches/delete_line.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,29 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[inline]
fn run(textarea: &TextArea<'_>) {
let mut term = dummy_terminal();
let mut t = textarea.clone();
t.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
for _ in 0..100 {
if !t.delete_line_by_head() {
t = textarea.clone();
t.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
}
term.draw_textarea(&t);
}
}
fn bench(c: &mut Criterion) {
let mut lines = vec![];
for _ in 0..10 {
lines.extend(LOREM.iter().map(|s| s.to_string()));
}
let textarea = TextArea::new(lines);
c.bench_function("delete::line", |b| b.iter(|| run(&textarea)));
}
criterion_group!(delete_line, bench);
criterion_main!(delete_line);

29
bench/benches/delete_word.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,29 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[inline]
fn run(textarea: &TextArea<'_>) {
let mut term = dummy_terminal();
let mut t = textarea.clone();
t.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
for _ in 0..100 {
if !t.delete_word() {
t = textarea.clone();
t.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
}
term.draw_textarea(&t);
}
}
fn bench(c: &mut Criterion) {
let mut lines = vec![];
for _ in 0..10 {
lines.extend(LOREM.iter().map(|s| s.to_string()));
}
let textarea = TextArea::new(lines);
c.bench_function("delete::word", |b| b.iter(|| run(&textarea)));
}
criterion_group!(delete_word, bench);
criterion_main!(delete_word);

Просмотреть файл

@@ -1,129 +0,0 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use tui_textarea::{CursorMove, Input, Key, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM, SEED};
#[inline]
fn append_lorem(repeat: usize) -> usize {
let mut textarea = TextArea::default();
let mut term = dummy_terminal();
for _ in 0..repeat {
for line in LOREM {
for c in line.chars() {
textarea.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
}
textarea.input(Input {
key: Key::Enter,
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
textarea.lines().len()
}
#[inline]
fn random_lorem(repeat: usize) -> usize {
let mut rng = SmallRng::from_seed(SEED);
let mut textarea = TextArea::default();
let mut term = dummy_terminal();
for _ in 0..repeat {
for line in LOREM {
let row = rng.gen_range(0..textarea.lines().len() as u16);
textarea.move_cursor(CursorMove::Jump(row, 0));
textarea.move_cursor(CursorMove::End);
textarea.input(Input {
key: Key::Enter,
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
for c in line.chars() {
textarea.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
}
}
textarea.lines().len()
}
#[inline]
fn append_long_lorem(repeat: usize) -> usize {
let mut textarea = TextArea::default();
let mut term = dummy_terminal();
for _ in 0..repeat {
for line in LOREM {
for c in line.chars() {
textarea.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
}
}
textarea.lines().len()
}
fn append(c: &mut Criterion) {
c.bench_function("insert::append::1_lorem", |b| {
b.iter(|| black_box(append_lorem(1)))
});
c.bench_function("insert::append::10_lorem", |b| {
b.iter(|| black_box(append_lorem(10)))
});
c.bench_function("insert::append::50_lorem", |b| {
b.iter(|| black_box(append_lorem(50)))
});
}
fn random(c: &mut Criterion) {
c.bench_function("insert::random::1_lorem", |b| {
b.iter(|| black_box(random_lorem(1)))
});
c.bench_function("insert::random::10_lorem", |b| {
b.iter(|| black_box(random_lorem(10)))
});
c.bench_function("insert::random::50_lorem", |b| {
b.iter(|| black_box(random_lorem(50)))
});
}
// Inserting a long line is slower than multiple short lines into `TextArea`
fn long(c: &mut Criterion) {
c.bench_function("insert::long::1_lorem", |b| {
b.iter(|| black_box(append_long_lorem(1)))
});
c.bench_function("insert::long::5_lorem", |b| {
b.iter(|| black_box(append_long_lorem(5)))
});
c.bench_function("insert::long::10_lorem", |b| {
b.iter(|| black_box(append_long_lorem(10)))
});
}
criterion_group!(insert, append, random, long);
criterion_main!(insert);

57
bench/benches/insert_append.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,57 @@
use criterion::{criterion_group, criterion_main, Criterion, SamplingMode};
use tui_textarea::{Input, Key, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[inline]
fn append_lorem(repeat: usize) -> usize {
let mut textarea = TextArea::default();
let mut term = dummy_terminal();
for _ in 0..repeat {
for line in LOREM {
for c in line.chars() {
textarea.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
}
textarea.input(Input {
key: Key::Enter,
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
textarea.lines().len()
}
fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group("insert::append");
// ~16ms/iter — fits 100 samples in ~6s, keep defaults
group.bench_function("1_lorem", |b| {
b.iter(|| std::hint::black_box(append_lorem(1)))
});
// ~490ms/iter — 20 samples × 490ms ≈ 10s
group.sampling_mode(SamplingMode::Flat);
group.sample_size(20);
group.bench_function("10_lorem", |b| {
b.iter(|| std::hint::black_box(append_lorem(10)))
});
// ~5.9s/iter — 10 samples × 5.9s ≈ 59s (minimum viable)
group.sample_size(10);
group.bench_function("50_lorem", |b| {
b.iter(|| std::hint::black_box(append_lorem(50)))
});
group.finish();
}
criterion_group!(insert_append, bench);
criterion_main!(insert_append);

53
bench/benches/insert_long.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,53 @@
// Inserting a long line is slower than multiple short lines into TextArea
use criterion::{criterion_group, criterion_main, Criterion, SamplingMode};
use tui_textarea::{Input, Key, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[inline]
fn append_long_lorem(repeat: usize) -> usize {
let mut textarea = TextArea::default();
let mut term = dummy_terminal();
for _ in 0..repeat {
for line in LOREM {
for c in line.chars() {
textarea.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
}
}
textarea.lines().len()
}
fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group("insert::long");
// ~25ms/iter — fits 100 samples in ~6s, keep defaults
group.bench_function("1_lorem", |b| {
b.iter(|| std::hint::black_box(append_long_lorem(1)))
});
// ~324ms/iter — 20 samples × 324ms ≈ 6s
group.sampling_mode(SamplingMode::Flat);
group.sample_size(20);
group.bench_function("5_lorem", |b| {
b.iter(|| std::hint::black_box(append_long_lorem(5)))
});
// ~1.1s/iter — 10 samples × 1.1s ≈ 11s
group.sample_size(10);
group.bench_function("10_lorem", |b| {
b.iter(|| std::hint::black_box(append_long_lorem(10)))
});
group.finish();
}
criterion_group!(insert_long, bench);
criterion_main!(insert_long);

67
bench/benches/insert_random.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,67 @@
use criterion::{criterion_group, criterion_main, Criterion, SamplingMode};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use tui_textarea::{CursorMove, Input, Key, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM, SEED};
#[inline]
fn random_lorem(repeat: usize) -> usize {
let mut rng = SmallRng::from_seed(SEED);
let mut textarea = TextArea::default();
let mut term = dummy_terminal();
for _ in 0..repeat {
for line in LOREM {
let row = rng.gen_range(0..textarea.lines().len() as u16);
textarea.move_cursor(CursorMove::Jump(row, 0));
textarea.move_cursor(CursorMove::End);
textarea.input(Input {
key: Key::Enter,
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
for c in line.chars() {
textarea.input(Input {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
term.draw_textarea(&textarea);
}
}
}
textarea.lines().len()
}
fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group("insert::random");
// ~23ms/iter — fits 100 samples in ~5s, keep defaults
group.bench_function("1_lorem", |b| {
b.iter(|| std::hint::black_box(random_lorem(1)))
});
// ~419ms/iter — 20 samples × 419ms ≈ 8s
group.sampling_mode(SamplingMode::Flat);
group.sample_size(20);
group.bench_function("10_lorem", |b| {
b.iter(|| std::hint::black_box(random_lorem(10)))
});
// ~2.1s/iter — 10 samples × 2.1s ≈ 21s
group.sample_size(10);
group.bench_function("50_lorem", |b| {
b.iter(|| std::hint::black_box(random_lorem(50)))
});
group.finish();
}
criterion_group!(insert_random, bench);
criterion_main!(insert_random);

37
bench/benches/search_backward.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,37 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::TextArea;
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[inline]
fn run(pat: &str, mut textarea: TextArea<'_>) {
let mut term = dummy_terminal();
textarea.set_search_pattern(pat).unwrap();
term.draw_textarea(&textarea);
for _ in 0..100 {
textarea.search_back(false);
term.draw_textarea(&textarea);
}
textarea.set_search_pattern(r"").unwrap();
term.draw_textarea(&textarea);
}
fn short(c: &mut Criterion) {
let textarea = TextArea::from(LOREM.iter().map(|s| s.to_string()));
c.bench_function("search::back_short", |b| {
b.iter(|| run(r"\w*i\w*", textarea.clone()))
});
}
fn long(c: &mut Criterion) {
let mut lines = vec![];
for _ in 0..10 {
lines.extend(LOREM.iter().map(|s| s.to_string()));
}
let textarea = TextArea::new(lines);
c.bench_function("search::back_long", |b| {
b.iter(|| run(r"[A-Z]\w*", textarea.clone()))
});
}
criterion_group!(search_backward, short, long);
criterion_main!(search_backward);

Просмотреть файл

@@ -3,16 +3,12 @@ use tui_textarea::TextArea;
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
#[inline]
fn run(pat: &str, mut textarea: TextArea<'_>, forward: bool) {
fn run(pat: &str, mut textarea: TextArea<'_>) {
let mut term = dummy_terminal();
textarea.set_search_pattern(pat).unwrap();
term.draw_textarea(&textarea);
for _ in 0..100 {
if forward {
textarea.search_forward(false);
} else {
textarea.search_back(false);
}
textarea.search_forward(false);
term.draw_textarea(&textarea);
}
textarea.set_search_pattern(r"").unwrap();
@@ -22,10 +18,7 @@ fn run(pat: &str, mut textarea: TextArea<'_>, forward: bool) {
fn short(c: &mut Criterion) {
let textarea = TextArea::from(LOREM.iter().map(|s| s.to_string()));
c.bench_function("search::forward_short", |b| {
b.iter(|| run(r"\w*i\w*", textarea.clone(), true))
});
c.bench_function("search::back_short", |b| {
b.iter(|| run(r"\w*i\w*", textarea.clone(), false))
b.iter(|| run(r"\w*i\w*", textarea.clone()))
});
}
@@ -36,12 +29,9 @@ fn long(c: &mut Criterion) {
}
let textarea = TextArea::new(lines);
c.bench_function("search::forward_long", |b| {
b.iter(|| run(r"[A-Z]\w*", textarea.clone(), true))
});
c.bench_function("search::back_long", |b| {
b.iter(|| run(r"[A-Z]\w*", textarea.clone(), false))
b.iter(|| run(r"[A-Z]\w*", textarea.clone()))
});
}
criterion_group!(search, short, long);
criterion_main!(search);
criterion_group!(search_forward, short, long);
criterion_main!(search_forward);

66
bench/benches/undo_redo.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,66 @@
use criterion::{criterion_group, criterion_main, Criterion, SamplingMode};
use tui_textarea::TextArea;
use tui_textarea_bench::{dummy_terminal, TerminalExt};
/// Build a textarea with `depth` undo-able edits in history using insert_str,
/// interspersed with delete_str to cover the cursor-clamping code path.
fn make_history(depth: usize) -> TextArea<'static> {
let mut ta = TextArea::default();
for i in 0..depth {
if i % 4 == 3 {
// Every 4th edit is a delete_str — the operation fixed in e6cc8ab.
ta.delete_str(3);
} else {
ta.insert_str("hello ");
}
}
ta
}
#[inline]
fn undo_all(textarea: &TextArea<'_>) -> usize {
let mut t = textarea.clone();
let mut term = dummy_terminal();
let mut count = 0;
while t.undo() {
term.draw_textarea(&t);
count += 1;
}
count
}
#[inline]
fn redo_all(textarea: &TextArea<'_>) -> usize {
// Start from fully-undone state so redo has work to do.
let mut t = textarea.clone();
while t.undo() {}
let mut term = dummy_terminal();
let mut count = 0;
while t.redo() {
term.draw_textarea(&t);
count += 1;
}
count
}
fn bench(c: &mut Criterion) {
let mut group = c.benchmark_group("undo_redo");
// undo::50 ran Linear with R²=0.19 (Criterion warned "enable flat sampling").
// All depths benefit from Flat since history-clone cost varies with depth.
group.sampling_mode(SamplingMode::Flat);
for depth in [50usize, 200, 500] {
let ta = make_history(depth);
group.bench_function(format!("undo::{}", depth), |b| {
b.iter(|| std::hint::black_box(undo_all(&ta)))
});
group.bench_function(format!("redo::{}", depth), |b| {
b.iter(|| std::hint::black_box(redo_all(&ta)))
});
}
group.finish();
}
criterion_group!(undo_redo, bench);
criterion_main!(undo_redo);

57
bench/benches/wrap_render.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,57 @@
use criterion::{criterion_group, criterion_main, Criterion};
use tui_textarea::{TextArea, WrapMode};
use tui_textarea_bench::{dummy_terminal, TerminalExt, LOREM};
const MODES: &[WrapMode] = &[
WrapMode::None,
WrapMode::Word,
WrapMode::Glyph,
WrapMode::WordOrGlyph,
];
fn make_textarea(repeat: usize, mode: WrapMode) -> TextArea<'static> {
let mut lines = Vec::with_capacity(LOREM.len() * repeat);
for _ in 0..repeat {
lines.extend(LOREM.iter().map(|s| s.to_string()));
}
let mut ta = TextArea::new(lines);
ta.set_wrap_mode(mode);
ta
}
// Short text: 7 lines, each ~63 chars. DummyBackend width=40 → all lines wrap.
fn short(c: &mut Criterion) {
for mode in MODES {
let ta = make_textarea(1, *mode);
let name = format!("wrap_render::short::{:?}", mode);
c.bench_function(&name, |b| {
b.iter(|| {
let mut term = dummy_terminal();
for _ in 0..50 {
term.draw_textarea(&ta);
}
std::hint::black_box(())
})
});
}
}
// Long text: 70 lines, same wrapping pressure.
fn long(c: &mut Criterion) {
for mode in MODES {
let ta = make_textarea(10, *mode);
let name = format!("wrap_render::long::{:?}", mode);
c.bench_function(&name, |b| {
b.iter(|| {
let mut term = dummy_terminal();
for _ in 0..50 {
term.draw_textarea(&ta);
}
std::hint::black_box(())
})
});
}
}
criterion_group!(wrap_render, short, long);
criterion_main!(wrap_render);

124
bench/src/bin/runner.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,124 @@
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
const TARGETS: &[&str] = &[
"insert_append",
"insert_random",
"insert_long",
"search_forward",
"search_backward",
"cursor_char",
"cursor_word",
"cursor_paragraph",
"cursor_edge",
"delete_char",
"delete_word",
"delete_line",
"wrap_render",
"undo_redo",
];
fn main() {
let total = TARGETS.len();
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = Path::new(manifest_dir)
.parent()
.expect("bench package must have a parent workspace directory");
let mut failed: Vec<&str> = Vec::new();
for (i, target) in TARGETS.iter().enumerate() {
let n = i + 1;
let timestamp = utc_now();
println!("[{}/{}] START {} at {}", n, total, target, timestamp);
io::stdout().flush().ok();
let wall_start = Instant::now();
let result = Command::new(&cargo)
.args([
"bench",
"-p",
"tui-textarea-bench",
"--bench",
target,
"--",
"--verbose",
"--noplot",
])
.current_dir(workspace_root)
.status();
let duration = wall_start.elapsed();
let (status_str, ok) = match result {
Ok(s) if s.success() => ("ok", true),
Ok(_) => ("err", false),
Err(e) => {
eprintln!(" error: failed to spawn cargo for {}: {}", target, e);
("err", false)
}
};
println!(
"[{}/{}] DONE {} status={} duration={:.2}s",
n,
total,
target,
status_str,
duration.as_secs_f64()
);
if !ok {
failed.push(target);
}
}
println!();
if failed.is_empty() {
println!("All {} benchmark targets completed successfully.", total);
} else {
eprintln!("Failed targets ({}/{}):", failed.len(), total);
for t in &failed {
eprintln!(" - {}", t);
}
std::process::exit(1);
}
}
/// Format the current UTC time as `YYYY-MM-DDTHH:MM:SSZ`.
/// Uses only `std::time` — no external date crate needed.
fn utc_now() -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let secs = (ts % 60) as u32;
let mins = ((ts / 60) % 60) as u32;
let hours = ((ts / 3600) % 24) as u32;
let days = ts / 86400;
let (year, month, day) = days_since_epoch_to_ymd(days);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hours, mins, secs
)
}
/// Convert days since Unix epoch (1970-01-01) to (year, month, day).
/// Algorithm: http://howardhinnant.github.io/date_algorithms.html
fn days_since_epoch_to_ymd(days: u64) -> (u32, u32, u32) {
let z = days as i64 + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u32, m as u32, d as u32)
}

Просмотреть файл

@@ -7,7 +7,7 @@ use ratatui::layout::{Position, Size};
use ratatui::prelude::backend::ClearType;
use ratatui::Terminal;
use std::io;
use tui_textarea::TextArea;
use tui_textarea::{CursorMove, TextArea};
pub const LOREM: &[&str] = &[
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do",
@@ -138,3 +138,55 @@ impl TerminalExt for Terminal<DummyBackend> {
self.draw(|f| f.render_widget(textarea, f.area())).unwrap();
}
}
// --- Cursor benchmark helpers ---
#[derive(Clone, Copy)]
pub enum Restore {
TopLeft,
BottomLeft,
BottomRight,
None,
}
impl Restore {
pub fn cursor_move(self) -> Option<CursorMove> {
match self {
Self::TopLeft => Some(CursorMove::Jump(0, 0)),
Self::BottomLeft => Some(CursorMove::Jump(u16::MAX, 0)),
Self::BottomRight => Some(CursorMove::Jump(u16::MAX, u16::MAX)),
Self::None => None,
}
}
}
pub fn prepare_cursor_textarea() -> TextArea<'static> {
let mut lines = Vec::with_capacity(LOREM.len() * 2 + 1);
lines.extend(LOREM.iter().map(|s| s.to_string()));
lines.push("".to_string());
lines.extend(LOREM.iter().map(|s| s.to_string()));
TextArea::new(lines)
}
pub fn run_cursor(
mut textarea: TextArea<'_>,
moves: &[CursorMove],
restore: Restore,
repeat: usize,
) -> (usize, usize) {
let mut term = dummy_terminal();
let mut prev = textarea.cursor();
for _ in 0..repeat {
for m in moves {
textarea.move_cursor(*m);
term.draw_textarea(&textarea);
}
if let Some(m) = restore.cursor_move() {
if textarea.cursor() == prev {
textarea.move_cursor(m);
prev = textarea.cursor();
}
}
}
textarea.cursor()
}