diff --git a/.gitignore b/.gitignore index f672c5d..c9d1920 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /*.log /*.gif +/AGENTS.md diff --git a/Cargo.lock b/Cargo.lock index 565b009..8284a52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -321,6 +327,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -367,6 +379,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.57" @@ -527,6 +566,61 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -561,6 +655,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1328,6 +1428,7 @@ dependencies = [ "clap", "clap_mangen", "cli-clipboard", + "criterion", "crossterm", "directories", "edit", @@ -2110,6 +2211,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.3.1" @@ -2161,6 +2273,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2510,6 +2628,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2522,6 +2651,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "855de4169757ca6b92b396d7a0380ef234e9a1ec2ec603c80779453ed0ab45e4" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -3097,6 +3235,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3388,6 +3532,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3714,7 +3886,7 @@ dependencies = [ "compact_str", "hashbrown 0.16.1", "indoc", - "itertools", + "itertools 0.14.0", "kasuari", "lru", "strum", @@ -3778,7 +3950,7 @@ dependencies = [ "hashbrown 0.16.1", "indoc", "instability", - "itertools", + "itertools 0.14.0", "line-clipping", "ratatui-core", "strum", @@ -3787,6 +3959,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4717,6 +4909,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -4983,7 +5185,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools", + "itertools 0.14.0", "unicode-segmentation", "unicode-width", ] @@ -5237,6 +5439,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9dc4fc8..f296ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = ["crates/hyperrat", "crates/ratatui-toaster"] [features] default = ["keyring/linux-native"] persist-token = ["keyring/sync-secret-service"] +benches = [] [dependencies] anyhow = "1.0" @@ -61,7 +62,20 @@ lto = "fat" # Enables Link-time Optimization. opt-level = "z" # Prioritizes small binary size. Use `3` if you prefer speed. strip = true # Ensures debug symbols are removed. +[profile.profiling] +inherits = "release" +debug = true +strip = false + [build-dependencies] anyhow = "1.0.101" vergen-gix = { version = "9.1.0", features = ["build", "cargo"] } + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + +[[bench]] +name = "ui_hotspots" +harness = false +required-features = ["benches"] diff --git a/benches/ui_hotspots.rs b/benches/ui_hotspots.rs new file mode 100644 index 0000000..640cc2d --- /dev/null +++ b/benches/ui_hotspots.rs @@ -0,0 +1,52 @@ +use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; +use gitv_tui::bench_support::{ + build_issue_body_preview_for_bench, issue_body_fixture, markdown_fixture, + render_markdown_for_bench, +}; + +fn bench_issue_list_preview(c: &mut Criterion) { + let mut group = c.benchmark_group("issue_list_preview"); + for repeat in [1_usize, 4, 12, 32] { + let body = issue_body_fixture(repeat); + group.throughput(Throughput::Bytes(body.len() as u64)); + for width in [40_usize, 80, 120] { + group.bench_with_input( + BenchmarkId::new(format!("repeat_{repeat}"), width), + &width, + |b, &width| { + b.iter(|| { + build_issue_body_preview_for_bench(black_box(&body), black_box(width)) + }); + }, + ); + } + } + group.finish(); +} + +fn bench_markdown_render(c: &mut Criterion) { + let mut group = c.benchmark_group("markdown_render"); + for repeat in [1_usize, 2, 6] { + let markdown = markdown_fixture(repeat); + group.throughput(Throughput::Bytes(markdown.len() as u64)); + for (width, indent) in [(48_usize, 2_usize), (80, 2), (100, 4)] { + group.bench_with_input( + BenchmarkId::new(format!("repeat_{repeat}_indent_{indent}"), width), + &width, + |b, &width| { + b.iter(|| { + render_markdown_for_bench( + black_box(&markdown), + black_box(width), + black_box(indent), + ) + }); + }, + ); + } + } + group.finish(); +} + +criterion_group!(ui_hotspots, bench_issue_list_preview, bench_markdown_render); +criterion_main!(ui_hotspots); diff --git a/src/bench_support.rs b/src/bench_support.rs new file mode 100644 index 0000000..f3f5b6f --- /dev/null +++ b/src/bench_support.rs @@ -0,0 +1,46 @@ +use crate::ui::components::{ + issue_conversation::render_markdown_lines, issue_list::build_issue_body_preview, +}; +use ratatui::text::Line; +use textwrap::Options; + +pub fn render_markdown_for_bench(text: &str, width: usize, indent: usize) -> Vec> { + render_markdown_lines(text, width, indent) +} + +pub fn build_issue_body_preview_for_bench(body_text: &str, width: usize) -> String { + build_issue_body_preview(body_text, Options::new(width)) +} + +pub fn issue_body_fixture(repeat: usize) -> String { + let paragraph = "This issue body mixes plain text, unicode like cafe and naive width tests, long URLs such as https://example.com/some/really/long/path/with/query?alpha=1&beta=2, and enough prose to trigger multi-line wrapping in the issue list preview. "; + paragraph.repeat(repeat) +} + +pub fn markdown_fixture(repeat: usize) -> String { + let section = r#"# Hot Path Markdown + +This fixture exercises **bold text**, _emphasis_, `inline code`, [links](https://github.com/jayanaxhf/gitv), and long prose that must wrap across multiple rendered lines inside the TUI. + +> [!NOTE] +> Notes should render with a title and wrapped body content. + +- first bullet with a very long explanation that wraps onto the next line +- second bullet with a reference to `IssueConversation` + +```rust +fn render_preview(width: usize) -> Vec { + (0..width.min(4)).map(|n| format!("line-{n}")).collect() +} +``` + +| column | value | +| ------ | ----- | +| width | 80 | +| indent | 2 | + +Trailing paragraph to keep textwrap and markdown layout busy. + +"#; + section.repeat(repeat) +} diff --git a/src/lib.rs b/src/lib.rs index db0c450..7e3f0a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod app; pub mod auth; +#[cfg(feature = "benches")] +pub mod bench_support; pub mod bookmarks; pub mod errors; pub mod github; diff --git a/src/ui/components/issue_list.rs b/src/ui/components/issue_list.rs index 4ff4ed4..366d0cc 100644 --- a/src/ui/components/issue_list.rs +++ b/src/ui/components/issue_list.rs @@ -770,8 +770,7 @@ impl<'a> IssueList<'a> { let body_text = pool .resolve_opt_str(issue.body) .unwrap_or("No desc provided"); - let mut body = wrap(body_text.trim(), options); - body.truncate(2); + let body_preview = build_issue_body_preview(body_text, options); let bookmarked = bookmarks.is_bookmarked(&self.owner, &self.repo, issue.number); let bookmark_symbol = if bookmarked { " b " } else { " " }; @@ -801,15 +800,18 @@ impl<'a> IssueList<'a> { " ", span!(format!("Opened by {author} at {created_at}")).dim(), ], - line![ - " ", - span!(body.join(" ").to_string()).style(Style::new().dim()) - ], + line![" ", span!(body_preview).style(Style::new().dim())], ]; ListItem::new(lines) } } +pub(crate) fn build_issue_body_preview(body_text: &str, options: Options<'_>) -> String { + let mut body = wrap(body_text.trim(), options); + body.truncate(2); + body.join(" ") +} + pub(crate) fn render_issue_close_popup( popup: &mut IssueClosePopupState, area: Rect,