зеркало из
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
Этот коммит содержится в:
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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()
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user