diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..4710a48 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,29 @@ +name: CodSpeed benchmarks + +on: + push: + branches: + - "main" + pull_request: + workflow_dispatch: + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Setup rust toolchain, cache and cargo-codspeed binary + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-codspeed + - name: Build the benchmark target(s) + run: cargo codspeed build + - name: Run the benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: instrumentation + run: cargo codspeed run + token: ${{ secrets.CODSPEED_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index fe16822..088d458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -294,6 +303,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.51" @@ -314,6 +329,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -342,6 +358,7 @@ dependencies = [ "assert_cmd", "base64", "clap", + "codspeed-divan-compat", "content_inspector", "dirs", "humantime", @@ -363,6 +380,66 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "codspeed" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b847e05a34be5c38f3f2a5052178a3bd32e6b5702f3ea775efde95c483a539" +dependencies = [ + "anyhow", + "cc", + "colored", + "getrandom 0.2.16", + "glob", + "libc", + "nix", + "serde", + "serde_json", + "statrs", +] + +[[package]] +name = "codspeed-divan-compat" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f0e9fe5eaa39995ec35e46407f7154346cc25bd1300c64c21636f3d00cb2cc" +dependencies = [ + "clap", + "codspeed", + "codspeed-divan-compat-macros", + "codspeed-divan-compat-walltime", + "regex", +] + +[[package]] +name = "codspeed-divan-compat-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c8babf2a40fd2206a2e030cf020d0d58144cd56e1dc408bfba02cdefb08b4f" +dependencies = [ + "divan-macros", + "itertools 0.14.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "codspeed-divan-compat-walltime" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f26092328e12a36704ffc552f379c6405dd94d3149970b79b22d371717c2aae" +dependencies = [ + "cfg-if", + "clap", + "codspeed", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -375,6 +452,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "console" version = "0.15.11" @@ -498,6 +591,17 @@ dependencies = [ "syn", ] +[[package]] +name = "divan-macros" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -702,6 +806,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "half" version = "2.7.1" @@ -963,6 +1073,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1152,6 +1271,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "8.0.0" @@ -1381,6 +1512,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1555,7 +1695,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", @@ -1645,6 +1785,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + [[package]] name = "regex-syntax" version = "0.8.8" @@ -1719,6 +1865,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.228" @@ -1749,6 +1901,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1806,6 +1971,16 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2025,8 +2200,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -2038,6 +2213,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2047,7 +2231,28 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 9a8eece..99422bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,11 @@ base64 = "0.22" tempfile = "3.23" proptest = "1.9" insta = "1.43" +divan = { version = "4.1.0", package = "codspeed-divan-compat" } + +[[bench]] +name = "base" +harness = false [profile.release] strip = true diff --git a/benches/base.rs b/benches/base.rs new file mode 100644 index 0000000..6fc681d --- /dev/null +++ b/benches/base.rs @@ -0,0 +1,59 @@ +use std::io::Cursor; +use std::sync::LazyLock; + +use clipvault::cli::{GetDelArgs, ListArgs, StoreArgs}; +use clipvault::commands::{get, list, store}; +use clipvault::defaults; +use tempfile::NamedTempFile; + +/// Get temporary file for DB. +fn get_temp() -> NamedTempFile { + NamedTempFile::new().expect("couldn't create tempfile") +} + +static DB: LazyLock = LazyLock::new(|| { + let db = get_temp(); + for n in 0..defaults::MAX_ENTRIES { + let args = StoreArgs::default(); + let bytes = "0".repeat(n).into_bytes(); + store::execute_with_source(db.path(), args, Cursor::new(bytes)).expect("failed to store"); + } + db +}); + +#[divan::bench(args = [1, 10, 100, 10_000, 100_000, 1_000_000], sample_size=10)] +fn store(n: usize) { + let db = get_temp(); + + let args = StoreArgs::default(); + let bytes = "0".repeat(n).into_bytes(); + store::execute_with_source(db.path(), args, Cursor::new(bytes)).expect("failed to store"); +} + +#[divan::bench(args = [1, 5, 10, 25, 50, 100, 1000], sample_size=10)] +fn list(n: usize) { + let path_db = DB.path(); + + let args = ListArgs { + max_preview_width: n, + ..Default::default() + }; + + list::execute_without_output(path_db, args).expect("failed to list"); +} + +#[divan::bench(args = [-100000, -1, 0, 1, 100000], sample_size=100)] +fn get(n: isize) { + let path_db = DB.path(); + + let args = GetDelArgs { + input: String::new(), + index: Some(n), + }; + + get::execute_without_output(path_db, args).expect("failed to get"); +} + +fn main() { + divan::main(); +} diff --git a/src/cli.rs b/src/cli.rs index c2f20a2..59b9d5f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use clap::{Parser, Subcommand, ValueHint, command}; use regex::Regex; @@ -87,6 +87,20 @@ pub struct StoreArgs { pub ignore_pattern: Option>, } +impl Default for StoreArgs { + fn default() -> Self { + Self { + max_entries: defaults::MAX_ENTRIES, + max_entry_age: humantime::Duration::from_str(defaults::MAX_ENTRY_AGE) + .expect("default max entry age should be valid"), + max_entry_length: defaults::MAX_ENTRY_LEN, + min_entry_length: defaults::MIN_ENTRY_LEN, + store_sensitive: false, + ignore_pattern: None, + } + } +} + #[derive(Debug, clap::Args)] pub struct ListArgs { /// Maximum width in characters for the previews. @@ -105,7 +119,16 @@ pub struct ListArgs { pub reverse: bool, } -#[derive(Debug, clap::Args)] +impl Default for ListArgs { + fn default() -> Self { + Self { + max_preview_width: defaults::MAX_PREVIEW_WIDTH, + reverse: false, + } + } +} + +#[derive(Debug, Default, clap::Args)] pub struct GetDelArgs { /// The selected row from `clipvault list`, or just the ID of the entry. /// diff --git a/src/commands/get.rs b/src/commands/get.rs index d22a767..a37d61d 100644 --- a/src/commands/get.rs +++ b/src/commands/get.rs @@ -17,8 +17,35 @@ use crate::{ utils::ignore_broken_pipe, }; +fn get_entry(path_db: &Path, mut input: String) -> Result { + // Read from STDIN if no argument given + if input.is_empty() { + stdin() + .lock() + .read_to_string(&mut input) + .into_diagnostic() + .context("failed to read STDIN")?; + } + + let id = extract_id(input)?; + let conn = init_db(path_db)?; + get_entry_by_id(&conn, id) +} + +fn get_entry_rel(path_db: &Path, i: isize) -> Result { + let conn = &init_db(path_db)?; + + let len = count_entries(conn)?; + if len == 0 { + return Err(miette!("there are currently no saved clipboard entries")); + } + + let index = wrap_index(len, i); + get_entry_by_position(conn, index) +} + #[tracing::instrument(skip(path_db))] -pub fn execute(path_db: &Path, args: GetDelArgs) -> Result<()> { +fn execute_inner(path_db: &Path, args: GetDelArgs, show_output: bool) -> Result<()> { let GetDelArgs { input, index } = args; assert!( @@ -33,6 +60,11 @@ pub fn execute(path_db: &Path, args: GetDelArgs) -> Result<()> { get_entry(path_db, input) }?; + // Used for benchmarks - don't actually write to stdout + if !show_output { + return Ok(()); + } + // Write to STDOUT let stdout = stdout(); let mut stdout = stdout.lock(); @@ -47,29 +79,17 @@ pub fn execute(path_db: &Path, args: GetDelArgs) -> Result<()> { Ok(()) } -fn get_entry(path_db: &Path, mut input: String) -> Result { - // Read from STDIN if no argument given - if input.is_empty() { - stdin() - .lock() - .read_to_string(&mut input) - .into_diagnostic() - .context("failed to read STDIN")?; - } - - let id = extract_id(input)?; - let conn = init_db(path_db)?; - get_entry_by_id(&conn, id) +#[tracing::instrument(skip(path_db))] +pub fn execute(path_db: &Path, args: GetDelArgs) -> Result<()> { + execute_inner(path_db, args, true) } -fn get_entry_rel(path_db: &Path, i: isize) -> Result { - let conn = &init_db(path_db)?; - - let len = count_entries(conn)?; - if len == 0 { - return Err(miette!("there are currently no saved clipboard entries")); - } - - let index = wrap_index(len, i); - get_entry_by_position(conn, index) +#[doc(hidden)] +#[tracing::instrument(skip(path_db))] +pub fn execute_without_output(path_db: &Path, args: GetDelArgs) -> Result<()> { + assert!( + !cfg!(debug_assertions), + "Not intended to run in production code" + ); + execute_inner(path_db, args, false) } diff --git a/src/commands/list.rs b/src/commands/list.rs index fbdc115..7633ad1 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -73,7 +73,7 @@ fn preview(id: u64, data: &[u8], width: usize) -> String { } #[tracing::instrument(skip(path_db))] -pub fn execute(path_db: &Path, args: ListArgs) -> Result<()> { +fn execute_inner(path_db: &Path, args: ListArgs, show_output: bool) -> Result<()> { let ListArgs { max_preview_width, reverse, @@ -109,6 +109,11 @@ pub fn execute(path_db: &Path, args: ListArgs) -> Result<()> { .collect::>() .join("\n"); + // Used for benchmarks - don't actually write to stdout + if !show_output { + return Ok(()); + } + let mut stdout = stdout().lock(); ignore_broken_pipe(writeln!(&mut stdout, "{output}",)) .into_diagnostic() @@ -119,3 +124,18 @@ pub fn execute(path_db: &Path, args: ListArgs) -> Result<()> { Ok(()) } + +#[tracing::instrument(skip(path_db))] +pub fn execute(path_db: &Path, args: ListArgs) -> Result<()> { + execute_inner(path_db, args, true) +} + +#[doc(hidden)] +#[tracing::instrument(skip(path_db))] +pub fn execute_without_output(path_db: &Path, args: ListArgs) -> Result<()> { + assert!( + !cfg!(debug_assertions), + "Not intended to run in production code" + ); + execute_inner(path_db, args, false) +} diff --git a/src/commands/store.rs b/src/commands/store.rs index c51dc11..9f75d03 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -18,6 +18,12 @@ use crate::{ #[instrument] pub fn execute(path_db: &Path, args: StoreArgs) -> Result<()> { + execute_with_source(path_db, args, stdin()) +} + +#[doc(hidden)] +#[instrument(skip(source))] +pub fn execute_with_source(path_db: &Path, args: StoreArgs, mut source: impl Read) -> Result<()> { let StoreArgs { max_entries, max_entry_age: max_age, @@ -57,12 +63,15 @@ pub fn execute(path_db: &Path, args: StoreArgs) -> Result<()> { } }; - // Read input from STDIN - let mut buf = vec![]; - stdin() - .read_to_end(&mut buf) - .into_diagnostic() - .context("failed to read from STDIN")?; + // Read input using given source - this should be STDIN for production code + let buf = { + let mut buf = vec![]; + source + .read_to_end(&mut buf) + .into_diagnostic() + .context("failed to read from STDIN")?; + buf + }; // No content to store if buf.is_empty() {