From 8ae50c87979ee942a7b227d1c471799789dfdc6c Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 10:57:42 +0300 Subject: [PATCH 01/15] Make clippy to run pedantic mode --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7dafff4..832c521 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ pgweasel is a fast CLI PostgreSQL log parser written in Rust (edition 2024, MSRV cargo build # Build cargo test # Run all tests (unit + integration) cargo test --test connections # Run a single integration test file -cargo clippy # Lint +cargo clippy --all-targets -- -W clippy::pedantic # Lint (always run in pedantic mode) cargo fmt --all -- --check # Check formatting ``` From 6562ec919f1fbabc848f13ba377260358d042013 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 11:03:57 +0300 Subject: [PATCH 02/15] Make AI instructions available for wider ecosystem --- AGENTS.md | 42 ++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 43 +------------------------------------------ GEMINI.md | 1 + 3 files changed, 44 insertions(+), 42 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md create mode 120000 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..832c521 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# CLAUDE.md + +## Project overview + +pgweasel is a fast CLI PostgreSQL log parser written in Rust (edition 2024, MSRV 1.92). It aggregates errors, locks, slow queries, connections, and system events from Postgres log files. + +## Build & test commands + +```bash +cargo build # Build +cargo test # Run all tests (unit + integration) +cargo test --test connections # Run a single integration test file +cargo clippy --all-targets -- -W clippy::pedantic # Lint (always run in pedantic mode) +cargo fmt --all -- --check # Check formatting +``` + +Each change should check tests and formatting, and if necessary fix. +Also each change should write app tests using new functionality. + +## Commiting changes + +Usually changes are done according to GitHub issues which are stated in subject: + +`#12 Added sorting for connections analysis` + +## Project structure + +- `src/main.rs` — CLI entry point (clap-based subcommands: `err`, `locks`, `slow`, `conn`, `system`) +- `src/aggregators/` — One aggregator per feature (connections, error_frequency, error_histogram, top_slow_query) +- `src/filters/` — Log line filters +- `src/format/` — Log format detection and parsing +- `src/output_results/` — Output orchestration +- `tests/` — Integration tests using `assert_cmd` + `predicates`, one file per subcommand +- `tests/files/` — Sample log files for tests + +## Conventions + +- Integration tests run the compiled binary via `assert_cmd::cargo::cargo_bin!("pgweasel")` and assert on stdout/stderr +- Aggregators implement the `Aggregator` trait (`update`, `merge_box`, `print`, `boxed_clone`, `as_any`) +- Sorted output sections use `Vec::sort_by` on collected HashMap entries, sorted descending by count +- Use `memchr::memmem` for fast byte-level string searches in hot paths +- Parallel processing via `rayon`; aggregator results merged with `merge_box()` diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 832c521..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,42 +0,0 @@ -# CLAUDE.md - -## Project overview - -pgweasel is a fast CLI PostgreSQL log parser written in Rust (edition 2024, MSRV 1.92). It aggregates errors, locks, slow queries, connections, and system events from Postgres log files. - -## Build & test commands - -```bash -cargo build # Build -cargo test # Run all tests (unit + integration) -cargo test --test connections # Run a single integration test file -cargo clippy --all-targets -- -W clippy::pedantic # Lint (always run in pedantic mode) -cargo fmt --all -- --check # Check formatting -``` - -Each change should check tests and formatting, and if necessary fix. -Also each change should write app tests using new functionality. - -## Commiting changes - -Usually changes are done according to GitHub issues which are stated in subject: - -`#12 Added sorting for connections analysis` - -## Project structure - -- `src/main.rs` — CLI entry point (clap-based subcommands: `err`, `locks`, `slow`, `conn`, `system`) -- `src/aggregators/` — One aggregator per feature (connections, error_frequency, error_histogram, top_slow_query) -- `src/filters/` — Log line filters -- `src/format/` — Log format detection and parsing -- `src/output_results/` — Output orchestration -- `tests/` — Integration tests using `assert_cmd` + `predicates`, one file per subcommand -- `tests/files/` — Sample log files for tests - -## Conventions - -- Integration tests run the compiled binary via `assert_cmd::cargo::cargo_bin!("pgweasel")` and assert on stdout/stderr -- Aggregators implement the `Aggregator` trait (`update`, `merge_box`, `print`, `boxed_clone`, `as_any`) -- Sorted output sections use `Vec::sort_by` on collected HashMap entries, sorted descending by count -- Use `memchr::memmem` for fast byte-level string searches in hot paths -- Parallel processing via `rayon`; aggregator results merged with `merge_box()` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 331506d0c50af5111cf23c2a9aabda6c81360d9b Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 11:25:19 +0300 Subject: [PATCH 03/15] Todo --- REFACTOR.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 REFACTOR.md diff --git a/REFACTOR.md b/REFACTOR.md new file mode 100644 index 0000000..6547ba7 --- /dev/null +++ b/REFACTOR.md @@ -0,0 +1,56 @@ +# Code refactoring TODO + +## Perfomance + +- [ ] Find big example +- [ ] Benchmark +- [ ] Isolate +- [ ] Optimize + +## Pedantic clippy TODO + +Outstanding `cargo clippy --all-targets -- -W clippy::pedantic` findings. +Check each off as it is fixed; the file is clean once all boxes are ticked. + +Re-run to refresh this list: + +```bash +cargo clippy --all-targets -- -W clippy::pedantic +``` + +Totals: 43 warnings across 8 lints (27 in tests, 16 in `src/`). + +## Source code + +### `too_many_lines` (3) — split into smaller functions +- [ ] `src/cli.rs:7` — 113/100 lines +- [ ] `src/main.rs:68` — `main()` 105/100 lines +- [ ] `src/output_results/mod.rs:17` — `output_results()` 130/100 lines + +### Numeric casts — `src/aggregators/error_histogram.rs:67-69` (6) +Use checked/`try_into` conversions or document why the cast is safe (then `#[allow]`). +- [ ] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `usize` (line 67) +- [ ] `cast_precision_loss` ×2 — `i64` → `f64` (line 67) +- [ ] `cast_precision_loss` ×2 — `usize` → `f64` (lines 67, 69) + +### Numeric casts — `src/duration.rs:47-48` (4) +- [ ] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `u64` (line 47) +- [ ] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `u64` (line 48) + +### Other +- [ ] `needless_pass_by_value` — `src/filters/filter_contains_ci.rs:9` — `new(substring: String)` should take `&str` +- [ ] `elidable_lifetime_names` — `src/output_results/mod.rs:279` — elide `'a` on `record_passes` (`&FilterContainer<'_>`) + +## Tests + +### `unnecessary_wraps` (27) — drop `-> Result<(), Box>` from tests that never use `?` +- [ ] `tests/grep.rs:6, 18, 30, 48` +- [ ] `tests/locks.rs:6` +- [ ] `tests/system.rs:7` +- [ ] `tests/connections.rs:6` +- [ ] `tests/help.rs:6, 18, 32, 46, 58` +- [ ] `tests/slow.rs:7, 19, 31, 43` +- [ ] `tests/errors.rs:9, 21, 33, 67, 86, 103, 115, 129, 154, 173, 195` + +### `uninlined_format_args` (1) +- [ ] `tests/errors.rs:53` — inline the variable into the `format!` string From 268d9e5d676b3f6499a583bebd6f699828f06313 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 13:28:19 +0300 Subject: [PATCH 04/15] upgrade dependencies to latest --- Cargo.lock | 556 +++++++++++++++++++++++++++++++++++------------------ Cargo.toml | 31 +-- 2 files changed, 386 insertions(+), 201 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ffb094..1598b2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,12 +10,12 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cfg-if", "cipher", + "cpubits", "cpufeatures", ] @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -54,15 +54,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -88,19 +88,16 @@ dependencies = [ ] [[package]] -name = "arbitrary" -version = "1.4.2" +name = "anyhow" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -125,11 +122,12 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", + "zeroize", ] [[package]] @@ -178,9 +176,9 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -192,9 +190,9 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "crypto-common", "inout", @@ -202,9 +200,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -212,9 +210,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -224,9 +222,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -236,9 +234,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "colorchoice" @@ -246,11 +250,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation-sys" @@ -259,29 +269,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "cpubits" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] -name = "crc" -version = "3.3.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "crc-catalog", + "libc", ] -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -318,12 +319,11 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] @@ -347,6 +347,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "deflate64" version = "0.1.10" @@ -355,23 +364,9 @@ checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "derive_more" @@ -402,13 +397,15 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" -version = "0.10.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common", - "subtle", + "ctutils", + "zeroize", ] [[package]] @@ -419,9 +416,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -429,9 +426,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -470,13 +467,13 @@ checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -489,14 +486,10 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" @@ -506,8 +499,32 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] [[package]] @@ -524,9 +541,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ "digest", ] @@ -537,6 +554,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -561,6 +587,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.12.1" @@ -568,16 +600,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -594,22 +628,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -622,7 +656,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] @@ -636,6 +670,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libbz2-rs-sys" version = "0.2.2" @@ -644,52 +684,42 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lzma-rust2" -version = "0.15.4" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48172246aa7c3ea28e423295dd1ca2589a24617cc4e588bb8cfe177cb2c54d95" +checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02" dependencies = [ - "crc", "sha2", ] [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -712,9 +742,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -727,9 +757,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -739,9 +769,9 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "pbkdf2" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ "digest", "hmac", @@ -801,15 +831,15 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -835,20 +865,30 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -859,11 +899,17 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -881,9 +927,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -904,9 +950,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustc_version" @@ -919,9 +965,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -978,11 +1024,24 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures", @@ -991,9 +1050,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", @@ -1018,17 +1077,11 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1037,12 +1090,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", @@ -1056,28 +1109,35 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "time" -version = "0.3.44" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "typed-path" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -1086,16 +1146,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] -name = "version_check" -version = "0.9.5" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wait-timeout" @@ -1112,7 +1172,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -1174,6 +1243,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -1320,40 +1423,112 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] -name = "zeroize" -version = "1.8.2" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ - "zeroize_derive", + "anyhow", + "heck", + "wit-parser", ] [[package]] -name = "zeroize_derive" -version = "1.4.2" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", "proc-macro2", "quote", "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zip" -version = "7.0.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd8a47718a4ee5fe78e07667cd36f3de80e7c2bfe727c7074245ffc7303c037" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "aes", - "arbitrary", "bzip2", "constant_time_eq", "crc32fast", "deflate64", "flate2", - "generic-array", - "getrandom", + "getrandom 0.4.2", "hmac", "indexmap", "lzma-rust2", @@ -1362,6 +1537,7 @@ dependencies = [ "ppmd-rust", "sha1", "time", + "typed-path", "zeroize", "zopfli", "zstd", @@ -1369,9 +1545,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index 1dba45f..d811cd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,25 +5,28 @@ edition = "2024" rust-version = "1.92" [dependencies] -clap = { version = "4.5.53", features = ["derive"] } -chrono = { version = "0.4", features = ["serde"] } -regex = "1.12.2" -once_cell = "1.21.3" -log = "0.4.29" -env_logger = "0.11" -flate2 = "1.1.5" +clap = { version = "4.6.1", features = ["derive"] } +chrono = { version = "0.4.45", features = ["serde"] } +regex = "1.12.4" +once_cell = "1.21.4" +log = "0.4.32" +env_logger = "0.11.10" +flate2 = "1.1.9" csv = "1.4.0" serde = { version = "1.0.228", features = ["derive"] } -tempfile = "3.23.0" -zip = "7.0.0" +tempfile = "3.27.0" +zip = "8.6.0" derive_more = { version = "2.1.1", features = ["from"] } -memchr = "2.7.6" -memmap2 = "0.9.9" -rayon = "1.11.0" +memchr = "2.8.2" +memmap2 = "0.9.10" +rayon = "1.12.0" humantime = "2.3.0" aho-corasick = { version = "1.1.4", default-features = false } [dev-dependencies] -assert_cmd = "2" -predicates = "3" +assert_cmd = "2.2.2" +predicates = "3.1.4" tempfile = "3" + +[profile.release] +debug = true From 9de58cf0b1f02120bf3ebe81be755d647d4f99ee Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:01:02 +0300 Subject: [PATCH 05/15] dhat-heap --- Cargo.lock | 139 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++ REFACTOR.md | 9 +++ src/main.rs | 7 ++ src/output_results/mod.rs | 16 +---- src/util.rs | 36 ++++++++++ 6 files changed, 198 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1598b2c..1ea5459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -114,6 +123,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.0", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -389,6 +413,22 @@ dependencies = [ "syn", ] +[[package]] +name = "dhat" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash", + "serde", + "serde_json", + "thousands", +] + [[package]] name = "difflib" version = "0.4.0" @@ -518,6 +558,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "hashbrown" version = "0.15.5" @@ -670,6 +716,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -694,6 +746,15 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.32" @@ -734,6 +795,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mintex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -755,6 +822,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -767,6 +843,29 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.0", +] + [[package]] name = "pbkdf2" version = "0.13.0" @@ -787,6 +886,7 @@ dependencies = [ "clap", "csv", "derive_more", + "dhat", "env_logger", "flate2", "humantime", @@ -925,6 +1025,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.4" @@ -954,6 +1063,18 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -988,6 +1109,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -1071,6 +1198,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "strsim" version = "0.11.1" @@ -1107,6 +1240,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + [[package]] name = "time" version = "0.3.49" diff --git a/Cargo.toml b/Cargo.toml index d811cd1..79afd75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,10 @@ memmap2 = "0.9.10" rayon = "1.12.0" humantime = "2.3.0" aho-corasick = { version = "1.1.4", default-features = false } +dhat = "0.3.3" + +[features] +dhat-heap = [] [dev-dependencies] assert_cmd = "2.2.2" diff --git a/REFACTOR.md b/REFACTOR.md index 6547ba7..d9672e5 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -3,7 +3,16 @@ ## Perfomance - [ ] Find big example + +hyperfine './target/release/pgweasel slow top ../log_examples/pgbench_large.log' +Benchmark 1: ./target/release/pgweasel slow top ../log_examples/pgbench_large.log + Time (mean ± σ): 462.9 ms ± 16.9 ms [User: 3991.5 ms, System: 40.8 ms] + Range (min … max): 449.6 ms … 496.3 ms 10 runs + - [ ] Benchmark + +2026-06-14T13:59:52Z DEBUG pgweasel::output_results] Finished aggregating in: 617.285042ms + - [ ] Isolate - [ ] Optimize diff --git a/src/main.rs b/src/main.rs index 1cbb9f8..c67d546 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,14 @@ mod util; pub use self::error::{Error, Result}; +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + fn main() -> Result<()> { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + let cli = cli::cli(); let matches = cli.clone().get_matches(); diff --git a/src/output_results/mod.rs b/src/output_results/mod.rs index dd72fe1..84c4dfb 100644 --- a/src/output_results/mod.rs +++ b/src/output_results/mod.rs @@ -9,7 +9,7 @@ use crate::aggregators::Aggregator; use crate::convert_args::ConvertedArgs; use crate::filters::{Filter, FilterContains}; use crate::format::Format; -use crate::util::parse_timestamp_from_string; +use crate::util::parse_timestamp_prefix; use rayon::prelude::*; use crate::Result; @@ -207,15 +207,7 @@ fn filter_record( return Ok(()); } - let mut parts = text.split_whitespace(); - let ts_str = format!( - "{} {} {}", - parts.next().ok_or("Missing timestamp first part")?, - parts.next().ok_or("Missing timestamp second part")?, - parts.next().ok_or("Missing timestamp third part")? - ); - - let log_time_local = parse_timestamp_from_string(ts_str.as_str())?; + let log_time_local = parse_timestamp_prefix(text)?; if filters.begin.is_some_and(|b| log_time_local < b) { return Ok(()); } @@ -290,9 +282,7 @@ fn record_passes<'a>( if i32::from(severity) < filter_container.min_severity { return None; } - let mut parts = text.split_whitespace(); - let ts_str = format!("{} {} {}", parts.next()?, parts.next()?, parts.next()?); - let log_time = parse_timestamp_from_string(ts_str.as_str()).ok()?; + let log_time = parse_timestamp_prefix(text).ok()?; if filter_container.begin.is_some_and(|b| log_time < b) { return None; } diff --git a/src/util.rs b/src/util.rs index 6675728..736b25f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -168,6 +168,26 @@ fn parse_timestamp( ))) } +/// Parses the timestamp at the start of a log record without allocating. +/// +/// The timestamp occupies the first three whitespace-separated tokens +/// (date, time, timezone), e.g. `2025-08-24 00:05:48.870 CEST`. Rather than +/// re-joining those tokens into a fresh `String` for every record, we slice the +/// contiguous span of the original text covering them and parse that directly. +pub fn parse_timestamp_prefix(text: &str) -> Result, String> { + let mut tokens = text.split_whitespace(); + let first = tokens.next().ok_or("Missing timestamp first part")?; + tokens.next().ok_or("Missing timestamp second part")?; + let third = tokens.next().ok_or("Missing timestamp third part")?; + + // `first` and `third` are sub-slices of `text`, so their pointers fall + // within `text`'s allocation; the offsets bound the timestamp span. + let base = text.as_ptr() as usize; + let start = first.as_ptr() as usize - base; + let end = third.as_ptr() as usize - base + third.len(); + parse_timestamp_from_string(&text[start..end]) +} + pub fn parse_timestamp_from_string(input: &str) -> Result, String> { let input = input.trim(); @@ -204,6 +224,22 @@ mod tests { use super::*; use chrono::{Datelike, Local, TimeZone}; + #[test] + fn test_parse_timestamp_prefix_matches_full_line() { + // Full log line: the prefix parse must yield the same instant as parsing + // just the timestamp tokens, ignoring the trailing log message. + let line = "2025-08-24 00:05:48.870 CEST [123] LOG: database system is ready"; + let prefix = parse_timestamp_prefix(line).unwrap(); + let expected = parse_timestamp_from_string("2025-08-24 00:05:48.870 CEST").unwrap(); + assert_eq!(prefix, expected); + } + + #[test] + fn test_parse_timestamp_prefix_missing_parts() { + assert!(parse_timestamp_prefix("2025-08-24 00:05:48.870").is_err()); + assert!(parse_timestamp_prefix("").is_err()); + } + #[test] fn test_today() { let result = time_or_interval_string_to_time("today", None).unwrap(); From 06b3743f36efcb4667a88abb737fe76cd9ff4623 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:15:07 +0300 Subject: [PATCH 06/15] Info --- REFACTOR.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/REFACTOR.md b/REFACTOR.md index d9672e5..55f3ec0 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -9,9 +9,11 @@ Benchmark 1: ./target/release/pgweasel slow top ../log_examples/pgbench_large.lo Time (mean ± σ): 462.9 ms ± 16.9 ms [User: 3991.5 ms, System: 40.8 ms] Range (min … max): 449.6 ms … 496.3 ms 10 runs +2026-06-14T14:11:15Z DEBUG pgweasel::output_results] Finished aggregating in: 475.911125ms + - [ ] Benchmark -2026-06-14T13:59:52Z DEBUG pgweasel::output_results] Finished aggregating in: 617.285042ms +2026-06-14T14:12:55Z DEBUG pgweasel::output_results] Finished aggregating in: 436.87ms - [ ] Isolate - [ ] Optimize From ad270ffc32b84b48f8de13e3193f8d619cbee849 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:32:56 +0300 Subject: [PATCH 07/15] Reuse evicted heap buffer in TopSlowQueries to cut allocations In the steady state (heap at capacity), every faster query freed the evicted entry's Vec and allocated a fresh one via record.to_vec(). Reuse the popped buffer (clear + extend_from_slice) so eviction allocates only when the new record exceeds the recycled capacity. On `slow top` over a large pgbench log this halves total allocation blocks (1,483 -> 726) with no behavior change. Co-Authored-By: Claude Opus 4.8 --- src/aggregators/top_slow_query.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/aggregators/top_slow_query.rs b/src/aggregators/top_slow_query.rs index b3ccaeb..2a0db36 100644 --- a/src/aggregators/top_slow_query.rs +++ b/src/aggregators/top_slow_query.rs @@ -42,8 +42,12 @@ impl Aggregator for TopSlowQueries { if let Some(Reverse((min, _))) = self.heap.peek() && duration > *min { - self.heap.pop(); - self.heap.push(Reverse((duration, record.to_vec()))); + // Reuse the evicted entry's buffer instead of freeing it and + // allocating a fresh Vec for every faster query we encounter. + let Reverse((_, mut buf)) = self.heap.pop().expect("peek implies non-empty"); + buf.clear(); + buf.extend_from_slice(record); + self.heap.push(Reverse((duration, buf))); } Ok(()) } From 65fac5fd03249f243d8f82bee219d54b1cfc69d3 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:49:10 +0300 Subject: [PATCH 08/15] Adding peformance logs to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 92322c4..774911f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .idea/ target/ +.DS_Store +dhat-heap.json +profile.json +profile.json.gz From c08465c92d5a259813a01e3b8253ee09c316f8f5 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:56:13 +0300 Subject: [PATCH 09/15] Eliminiate double date parsing --- src/util.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/src/util.rs b/src/util.rs index 736b25f..c6b023b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -177,9 +177,21 @@ fn parse_timestamp( pub fn parse_timestamp_prefix(text: &str) -> Result, String> { let mut tokens = text.split_whitespace(); let first = tokens.next().ok_or("Missing timestamp first part")?; - tokens.next().ok_or("Missing timestamp second part")?; + let second = tokens.next().ok_or("Missing timestamp second part")?; let third = tokens.next().ok_or("Missing timestamp third part")?; + // Fast path: the Postgres prefix is a rigid `YYYY-MM-DD HH:MM:SS[.fff]` + // followed by a timezone abbreviation. Parse the date/time bytes directly + // instead of letting chrono re-parse the format pattern for every record. + // The timezone token is intentionally ignored and the value is interpreted + // as local time, matching `parse_timestamp_from_string`. + if let Some(naive) = parse_pg_naive_datetime(first, second) + && let Some(local) = Local.from_local_datetime(&naive).single() + { + return Ok(local); + } + + // Fall back to the general (slower) parser for non-standard formats. // `first` and `third` are sub-slices of `text`, so their pointers fall // within `text`'s allocation; the offsets bound the timestamp span. let base = text.as_ptr() as usize; @@ -188,6 +200,65 @@ pub fn parse_timestamp_prefix(text: &str) -> Result, String> { parse_timestamp_from_string(&text[start..end]) } +/// Parses the rigid Postgres date/time tokens without invoking chrono's +/// format-pattern parser. `date` must be `YYYY-MM-DD` and `time` must be +/// `HH:MM:SS` optionally followed by `.` and up to nine fractional-second +/// digits. Returns `None` for anything that does not match exactly, leaving +/// the caller to fall back to a more permissive parser. +fn parse_pg_naive_datetime(date: &str, time: &str) -> Option { + let d = date.as_bytes(); + if d.len() != 10 || d[4] != b'-' || d[7] != b'-' { + return None; + } + let year = parse_ascii_u32(&d[0..4])?; + let month = parse_ascii_u32(&d[5..7])?; + let day = parse_ascii_u32(&d[8..10])?; + + let t = time.as_bytes(); + if t.len() < 8 || t[2] != b':' || t[5] != b':' { + return None; + } + let hour = parse_ascii_u32(&t[0..2])?; + let minute = parse_ascii_u32(&t[3..5])?; + let second = parse_ascii_u32(&t[6..8])?; + + let nanos = if t.len() > 8 { + if t[8] != b'.' { + return None; + } + let frac = &t[9..]; + if frac.is_empty() || frac.len() > 9 { + return None; + } + let mut value = parse_ascii_u32(frac)?; + // Scale the fractional part up to nanoseconds (e.g. `.870` -> 870_000_000). + for _ in 0..(9 - frac.len()) { + value *= 10; + } + value + } else { + 0 + }; + + let date = NaiveDate::from_ymd_opt(i32::try_from(year).ok()?, month, day)?; + date.and_hms_nano_opt(hour, minute, second, nanos) +} + +/// Parses an ASCII byte slice of decimal digits into a `u32`, rejecting any +/// non-digit byte. The slices passed here are short (at most nine digits) so +/// overflow is not a concern. +#[inline] +fn parse_ascii_u32(bytes: &[u8]) -> Option { + let mut acc: u32 = 0; + for &b in bytes { + if !b.is_ascii_digit() { + return None; + } + acc = acc * 10 + u32::from(b - b'0'); + } + Some(acc) +} + pub fn parse_timestamp_from_string(input: &str) -> Result, String> { let input = input.trim(); @@ -240,6 +311,42 @@ mod tests { assert!(parse_timestamp_prefix("").is_err()); } + #[test] + fn test_parse_timestamp_prefix_without_millis() { + // The fast path must also handle `HH:MM:SS` without a fractional part + // and agree with the general parser. + let line = "2025-08-24 00:05:48 CEST [123] LOG: ready"; + let prefix = parse_timestamp_prefix(line).unwrap(); + let expected = parse_timestamp_from_string("2025-08-24 00:05:48 CEST").unwrap(); + assert_eq!(prefix, expected); + } + + #[test] + fn test_parse_pg_naive_datetime_fast_path() { + use chrono::Timelike; + + let naive = parse_pg_naive_datetime("2025-08-24", "00:05:48.870").unwrap(); + assert_eq!(naive.year(), 2025); + assert_eq!(naive.month(), 8); + assert_eq!(naive.day(), 24); + assert_eq!(naive.hour(), 0); + assert_eq!(naive.minute(), 5); + assert_eq!(naive.second(), 48); + assert_eq!(naive.nanosecond(), 870_000_000); + + // No fractional seconds. + let naive = parse_pg_naive_datetime("2025-12-31", "23:59:59").unwrap(); + assert_eq!(naive.nanosecond(), 0); + assert_eq!(naive.hour(), 23); + + // Malformed inputs return None so the caller can fall back. + assert!(parse_pg_naive_datetime("2025/08/24", "00:05:48").is_none()); + assert!(parse_pg_naive_datetime("2025-08-24", "00-05-48").is_none()); + assert!(parse_pg_naive_datetime("2025-08-24", "00:05:48.").is_none()); + assert!(parse_pg_naive_datetime("2025-08-24", "0a:05:48").is_none()); + assert!(parse_pg_naive_datetime("2025-13-24", "00:05:48").is_none()); + } + #[test] fn test_today() { let result = time_or_interval_string_to_time("today", None).unwrap(); From 79be36c92c8af0f74a531a73b08e3c40046a52e3 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:56:31 +0300 Subject: [PATCH 10/15] Update refactoring info. --- REFACTOR.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/REFACTOR.md b/REFACTOR.md index 55f3ec0..30eab21 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -4,19 +4,13 @@ - [ ] Find big example -hyperfine './target/release/pgweasel slow top ../log_examples/pgbench_large.log' -Benchmark 1: ./target/release/pgweasel slow top ../log_examples/pgbench_large.log - Time (mean ± σ): 462.9 ms ± 16.9 ms [User: 3991.5 ms, System: 40.8 ms] - Range (min … max): 449.6 ms … 496.3 ms 10 runs - 2026-06-14T14:11:15Z DEBUG pgweasel::output_results] Finished aggregating in: 475.911125ms -- [ ] Benchmark - -2026-06-14T14:12:55Z DEBUG pgweasel::output_results] Finished aggregating in: 436.87ms +- Benchmark --debug option +- Isolate `samply record ./target/release/pgweasel slow top ../log_examples/pgbench_large.log` & `cargo run --release --features dhat-heap slow top ../log_examples/pgbench_large.log` +- Optimize Ty Claude -- [ ] Isolate -- [ ] Optimize +2026-06-14T14:50:42Z DEBUG pgweasel::output_results] Finished aggregating in: 261.124166ms ## Pedantic clippy TODO From 952c6f33f089a84b6cef101e19c27285e64e5825 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 17:59:09 +0300 Subject: [PATCH 11/15] Fix uninlined_format_args clippy lint in tests Inline the variable into the format! string in tests/errors.rs and tick the task off in REFACTOR.md. Co-Authored-By: Claude Opus 4.8 --- REFACTOR.md | 2 +- tests/errors.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/REFACTOR.md b/REFACTOR.md index 30eab21..6d4cad0 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -58,4 +58,4 @@ Use checked/`try_into` conversions or document why the cast is safe (then `#[all - [ ] `tests/errors.rs:9, 21, 33, 67, 86, 103, 115, 129, 154, 173, 195` ### `uninlined_format_args` (1) -- [ ] `tests/errors.rs:53` — inline the variable into the `format!` string +- [x] `tests/errors.rs:53` — inline the variable into the `format!` string diff --git a/tests/errors.rs b/tests/errors.rs index adc3815..d5b1f40 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -50,7 +50,7 @@ fn simple_error_filter_with_begin_min() -> Result<(), Box let content = format!("{now_str} ERROR: error message example\n"); let mut tmp = Builder::new().suffix(".log").tempfile()?; - write!(tmp, "{}", content)?; + write!(tmp, "{content}")?; tmp.flush()?; cmd.args(["-b", "10m", "err", tmp.path().to_str().unwrap()]) From 7b209c4e6399e16679dfd6fa60911a532b2708ed Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 18:14:59 +0300 Subject: [PATCH 12/15] Drop unnecessary Result wrappers from tests (clippy::unnecessary_wraps) Remove `-> Result<(), Box>` and trailing `Ok(())` from the 27 integration tests that never use `?`. Tests that genuinely use the try-operator keep their Result return type. Co-Authored-By: Claude Opus 4.8 --- tests/connections.rs | 4 +--- tests/errors.rs | 45 +++++++++++--------------------------------- tests/grep.rs | 16 ++++------------ tests/help.rs | 20 +++++--------------- tests/locks.rs | 4 +--- tests/slow.rs | 16 ++++------------ tests/system.rs | 3 +-- 7 files changed, 27 insertions(+), 81 deletions(-) diff --git a/tests/connections.rs b/tests/connections.rs index cf3fd9f..e165d56 100644 --- a/tests/connections.rs +++ b/tests/connections.rs @@ -3,15 +3,13 @@ use assert_cmd::prelude::*; use std::process::Command; #[test] -fn simple_connection_aggregate() -> Result<(), Box> { +fn simple_connection_aggregate() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["conn", "./tests/files/azure_connections.log"]) .assert() .success() .stdout(predicates::str::contains("5 2025-05-21 11:00:00")); - - Ok(()) } #[test] diff --git a/tests/errors.rs b/tests/errors.rs index d5b1f40..c9d5ded 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -6,39 +6,33 @@ use std::process::Command; use tempfile::Builder; #[test] -fn simple_error_filter() -> Result<(), Box> { +fn simple_error_filter() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["err", "./tests/files/csvlog1.csv"]) .assert() .success() .stdout(predicates::str::contains("2025-05-08 12:24:37.731 EEST")); - - Ok(()) } #[test] -fn simple_error_filter_for_log() -> Result<(), Box> { +fn simple_error_filter_for_log() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["err", "./tests/files/debian_default2.log"]) .assert() .success() .stdout(predicates::str::contains("2025-05-22 15:15:09.392")); - - Ok(()) } #[test] -fn error_multiline_csv() -> Result<(), Box> { +fn error_multiline_csv() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["err", "./tests/files/multiple_lines.csv"]) .assert() .success() .stdout(predicates::str::contains("2025-12-15 12:41:20.659")); - - Ok(()) } #[test] @@ -64,7 +58,7 @@ fn simple_error_filter_with_begin_min() -> Result<(), Box } #[test] -fn simple_error_filter_with_begin_end() -> Result<(), Box> { +fn simple_error_filter_with_begin_end() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -78,12 +72,10 @@ fn simple_error_filter_with_begin_end() -> Result<(), Box .assert() .success() .stdout(predicates::str::contains("2025-05-08 12:24:37.731 EEST")); - - Ok(()) } #[test] -fn simple_error_filter_with_mask() -> Result<(), Box> { +fn simple_error_filter_with_mask() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -95,24 +87,20 @@ fn simple_error_filter_with_mask() -> Result<(), Box> { .assert() .success() .stdout(predicates::str::contains("2025-05-08 12:24:37.731 EEST")); - - Ok(()) } #[test] -fn simple_filter_with_list_subcommand() -> Result<(), Box> { +fn simple_filter_with_list_subcommand() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["err", "list", "./tests/files/csvlog1.csv"]) .assert() .success() .stdout(predicates::str::contains("2025-05-08 12:24:37.731 EEST")); - - Ok(()) } #[test] -fn simple_filter_with_top_subcommand() -> Result<(), Box> { +fn simple_filter_with_top_subcommand() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["err", "top", "./tests/files/debian_default2.log"]) @@ -121,12 +109,10 @@ fn simple_filter_with_top_subcommand() -> Result<(), Box> .stdout(predicates::str::contains( "new row for relation \"pgbench_accounts\" violates check constraint \"posbal\"", )); - - Ok(()) } #[test] -fn simple_filter_with_top_max_subcommand() -> Result<(), Box> { +fn simple_filter_with_top_max_subcommand() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -146,13 +132,10 @@ fn simple_filter_with_top_max_subcommand() -> Result<(), Box Result<(), Box> { +fn simple_filter_with_top_max_subcommand_contains_same_max() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -165,12 +148,10 @@ fn simple_filter_with_top_max_subcommand_contains_same_max() .assert() .success() .stdout(predicates::str::contains("8 new row for relation")); - - Ok(()) } #[test] -fn simple_filter_with_hist_subcommand() -> Result<(), Box> { +fn simple_filter_with_hist_subcommand() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -187,18 +168,14 @@ fn simple_filter_with_hist_subcommand() -> Result<(), Box .stdout(predicates::str::contains( "[2025-05-22 15:18:10] ##################################---------------- 11", )); - - Ok(()) } #[test] -fn non_existing_file() -> Result<(), Box> { +fn non_existing_file() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["err", "list", "./tests/files/csvlog1.csv_non_existing"]) .assert() .failure() .stderr(predicates::str::contains("FileDoesNotExist")); - - Ok(()) } diff --git a/tests/grep.rs b/tests/grep.rs index 470332f..eda3771 100644 --- a/tests/grep.rs +++ b/tests/grep.rs @@ -3,31 +3,27 @@ use assert_cmd::prelude::*; use std::process::Command; #[test] -fn grep_finds_matching_lines() -> Result<(), Box> { +fn grep_finds_matching_lines() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["grep", "dadasd", "./tests/files/debian_default.log"]) .assert() .success() .stdout(predicates::str::contains("dasda")); - - Ok(()) } #[test] -fn grep_is_case_insensitive() -> Result<(), Box> { +fn grep_is_case_insensitive() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["grep", "DADASD", "./tests/files/debian_default.log"]) .assert() .success() .stdout(predicates::str::contains("dasda")); - - Ok(()) } #[test] -fn grep_after_context() -> Result<(), Box> { +fn grep_after_context() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -40,12 +36,10 @@ fn grep_after_context() -> Result<(), Box> { .assert() .success() .stdout(predicates::str::contains("terminating background worker")); - - Ok(()) } #[test] -fn grep_before_context() -> Result<(), Box> { +fn grep_before_context() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -58,6 +52,4 @@ fn grep_before_context() -> Result<(), Box> { .assert() .success() .stdout(predicates::str::contains("syntax error")); - - Ok(()) } diff --git a/tests/help.rs b/tests/help.rs index d7e3c4e..9499ac1 100644 --- a/tests/help.rs +++ b/tests/help.rs @@ -3,19 +3,17 @@ use assert_cmd::prelude::*; // Add methods on commands use std::process::Command; // Run programs #[test] -fn base_help_with_options() -> Result<(), Box> { +fn base_help_with_options() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.arg("--help") .assert() .success() .stdout(predicates::str::contains("pgweasel [OPTIONS] ")); - - Ok(()) } #[test] -fn errors_command_help() -> Result<(), Box> { +fn errors_command_help() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["errors", "--help"]) @@ -24,12 +22,10 @@ fn errors_command_help() -> Result<(), Box> { .stdout(predicates::str::contains( "pgweasel errors [OPTIONS] ...", )); - - Ok(()) } #[test] -fn errors_command_with_sub_command_help() -> Result<(), Box> { +fn errors_command_with_sub_command_help() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["errors", "list", "--help"]) @@ -38,30 +34,24 @@ fn errors_command_with_sub_command_help() -> Result<(), Box...", )); - - Ok(()) } #[test] -fn slow_command_help_contains_treshold() -> Result<(), Box> { +fn slow_command_help_contains_treshold() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["slow", "--help"]) .assert() .success() .stdout(predicates::str::contains("slow ")); - - Ok(()) } #[test] -fn slow_command_help_contains_subcommand_top() -> Result<(), Box> { +fn slow_command_help_contains_subcommand_top() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["slow", "--help"]) .assert() .success() .stdout(predicates::str::contains("top")); - - Ok(()) } diff --git a/tests/locks.rs b/tests/locks.rs index 8d71a27..281694c 100644 --- a/tests/locks.rs +++ b/tests/locks.rs @@ -3,13 +3,11 @@ use assert_cmd::prelude::*; use std::process::Command; #[test] -fn simple_csv_slow_filter() -> Result<(), Box> { +fn simple_csv_slow_filter() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["locks", "./tests/files/locking.log"]) .assert() .success() .stdout(predicates::str::contains("2025-06-03 12:46:07.925")); - - Ok(()) } diff --git a/tests/slow.rs b/tests/slow.rs index 29080f1..b939450 100644 --- a/tests/slow.rs +++ b/tests/slow.rs @@ -4,43 +4,37 @@ use predicates::prelude::PredicateBooleanExt; // Add methods on commands use std::process::Command; // Run programs #[test] -fn simple_csv_slow_filter() -> Result<(), Box> { +fn simple_csv_slow_filter() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["slow", "1s", "./tests/files/csvlog_pg14.csv"]) .assert() .success() .stdout(predicates::str::contains("duration: 2722.543 ms")); - - Ok(()) } #[test] -fn simple_log_slow_filter() -> Result<(), Box> { +fn simple_log_slow_filter() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["slow", "25ms", "./tests/files/duration.log"]) .assert() .success() .stdout(predicates::str::contains("statement: WITH RECURSIVE")); - - Ok(()) } #[test] -fn aggregate_top_slow() -> Result<(), Box> { +fn aggregate_top_slow() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["slow", "top", "./tests/files/duration.log"]) .assert() .success() .stdout(predicates::str::contains("--- 25.761ms ---")); - - Ok(()) } #[test] -fn aggregate_top_slow_with_filter() -> Result<(), Box> { +fn aggregate_top_slow_with_filter() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args([ @@ -53,6 +47,4 @@ fn aggregate_top_slow_with_filter() -> Result<(), Box> { .assert() .success() .stdout(predicates::str::contains("025-05-21 11:01:10").not()); - - Ok(()) } diff --git a/tests/system.rs b/tests/system.rs index d30def9..fa9691c 100644 --- a/tests/system.rs +++ b/tests/system.rs @@ -4,7 +4,7 @@ use predicates::prelude::PredicateBooleanExt; use std::process::Command; // Run programs #[test] -fn simple_log_system() -> Result<(), Box> { +fn simple_log_system() { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); cmd.args(["system", "./tests/files/system_test.log"]) @@ -13,5 +13,4 @@ fn simple_log_system() -> Result<(), Box> { .stdout( predicates::str::contains("listening").and(predicates::str::contains("was shut down")), ); - Ok(()) } From 0e81986bfa1fbd47c25d29fdbdac5e979893e19c Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 18:14:59 +0300 Subject: [PATCH 13/15] Split oversized functions into helpers (clippy::too_many_lines) - cli.rs: extract errors_command() - main.rs: extract handle_errors_command() - output_results: extract compute_ranges(), process_with_context(), process_parallel() Behavior unchanged; ticks the too_many_lines and unnecessary_wraps tasks in REFACTOR.md. Co-Authored-By: Claude Opus 4.8 --- REFACTOR.md | 20 +-- src/cli.rs | 76 ++++++----- src/main.rs | 115 +++++++++-------- src/output_results/mod.rs | 258 ++++++++++++++++++++++---------------- 4 files changed, 266 insertions(+), 203 deletions(-) diff --git a/REFACTOR.md b/REFACTOR.md index 6d4cad0..e2f20df 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -28,9 +28,9 @@ Totals: 43 warnings across 8 lints (27 in tests, 16 in `src/`). ## Source code ### `too_many_lines` (3) — split into smaller functions -- [ ] `src/cli.rs:7` — 113/100 lines -- [ ] `src/main.rs:68` — `main()` 105/100 lines -- [ ] `src/output_results/mod.rs:17` — `output_results()` 130/100 lines +- [x] `src/cli.rs:7` — extracted `errors_command()` +- [x] `src/main.rs:68` — extracted `handle_errors_command()` +- [x] `src/output_results/mod.rs:17` — extracted `compute_ranges()`, `process_with_context()`, `process_parallel()` ### Numeric casts — `src/aggregators/error_histogram.rs:67-69` (6) Use checked/`try_into` conversions or document why the cast is safe (then `#[allow]`). @@ -49,13 +49,13 @@ Use checked/`try_into` conversions or document why the cast is safe (then `#[all ## Tests ### `unnecessary_wraps` (27) — drop `-> Result<(), Box>` from tests that never use `?` -- [ ] `tests/grep.rs:6, 18, 30, 48` -- [ ] `tests/locks.rs:6` -- [ ] `tests/system.rs:7` -- [ ] `tests/connections.rs:6` -- [ ] `tests/help.rs:6, 18, 32, 46, 58` -- [ ] `tests/slow.rs:7, 19, 31, 43` -- [ ] `tests/errors.rs:9, 21, 33, 67, 86, 103, 115, 129, 154, 173, 195` +- [x] `tests/grep.rs:6, 18, 30, 48` +- [x] `tests/locks.rs:6` +- [x] `tests/system.rs:7` +- [x] `tests/connections.rs:6` +- [x] `tests/help.rs:6, 18, 32, 46, 58` +- [x] `tests/slow.rs:7, 19, 31, 43` +- [x] `tests/errors.rs:9, 21, 33, 67, 86, 103, 115, 129, 154, 173, 195` ### `uninlined_format_args` (1) - [x] `tests/errors.rs:53` — inline the variable into the `format!` string diff --git a/src/cli.rs b/src/cli.rs index 80e65b9..5d19819 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -31,38 +31,7 @@ pub fn cli() -> Command { .value_parser(value_parser!(usize)), ) .subcommand_required(true) - .subcommand( - Command::new("errors") - .about("Show or summarize error messages") - .alias("error") - .alias("err") - .args_conflicts_with_subcommands(true) - .args(level_args()) - .args(filelist_args()) - .subcommand(Command::new("list") - .about("Default subcommand of error. Show error messages") - .args(level_args()) - .args(filelist_args())) - .subcommand(Command::new("top") - .about("Shows top most frequent error messages") - .args(level_args()) - .arg(arg!(--max ) - .short('m') - .help("Max number of top errors to show (default 20)") - .value_parser(value_parser!(usize)) - .default_value("20")) - .args(filelist_args())) - .subcommand(Command::new("hist") - .about("Show histogram of error occurrences over time") - .alias("histogram") - .args(level_args()) - .arg(arg!(--bucket ) - .short('b') - .help("Interval for histogram buckets, e.g. 10s, 1m, 1h. Defaults to 1h") - .value_parser(value_parser!(String)) - .default_value("1h")) - .args(filelist_args())) - ) + .subcommand(errors_command()) .subcommand( Command::new("locks") .alias("loc") @@ -120,6 +89,49 @@ pub fn cli() -> Command { ) } +fn errors_command() -> Command { + Command::new("errors") + .about("Show or summarize error messages") + .alias("error") + .alias("err") + .args_conflicts_with_subcommands(true) + .args(level_args()) + .args(filelist_args()) + .subcommand( + Command::new("list") + .about("Default subcommand of error. Show error messages") + .args(level_args()) + .args(filelist_args()), + ) + .subcommand( + Command::new("top") + .about("Shows top most frequent error messages") + .args(level_args()) + .arg( + arg!(--max ) + .short('m') + .help("Max number of top errors to show (default 20)") + .value_parser(value_parser!(usize)) + .default_value("20"), + ) + .args(filelist_args()), + ) + .subcommand( + Command::new("hist") + .about("Show histogram of error occurrences over time") + .alias("histogram") + .args(level_args()) + .arg( + arg!(--bucket ) + .short('b') + .help("Interval for histogram buckets, e.g. 10s, 1m, 1h. Defaults to 1h") + .value_parser(value_parser!(String)) + .default_value("1h"), + ) + .args(filelist_args()), + ) +} + fn level_args() -> Vec { vec![ arg!(--level ) diff --git a/src/main.rs b/src/main.rs index c67d546..ea755d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,59 +84,7 @@ fn main() -> Result<()> { match matches.subcommand() { Some(("errors", sub_matches)) => { - let error_command = sub_matches.subcommand().unwrap_or(("list", sub_matches)); - match error_command { - ("list", list_subcommand) => { - output_results( - converted_args, - *list_subcommand - .get_one::("level") - .unwrap_or(&Severity::Error), - &mut aggregators, - &filters, - )?; - } - ("top", top_subcommand) => { - let limit = *top_subcommand.get_one::("max").unwrap_or(&20); - aggregators.push(Box::new(ErrorFrequencyAggregator::new(limit))); - converted_args.print_details = false; - output_results( - converted_args, - *top_subcommand - .get_one::("level") - .unwrap_or(&Severity::Error), - &mut aggregators, - &filters, - )?; - } - ("hist", hist_subcommand) => { - // aggregators.push(Box::new(ErrorFrequencyAggregator::new())); - converted_args.print_details = false; - let mut interval = Duration::from_hours(1); - if let Some(interval_str) = hist_subcommand.get_one::("bucket") { - interval = parse_duration(interval_str)?; - } - aggregators.push(Box::new(ErrorHistogramAggregator::new(interval))); - debug!( - "Histogram severity: {:?}", - hist_subcommand - .get_one::("level") - .unwrap_or(&Severity::Error) - ); - debug!("Histogram interval: {interval:?}"); - output_results( - converted_args, - *hist_subcommand - .get_one::("level") - .unwrap_or(&Severity::Error), - &mut aggregators, - &filters, - )?; - } - (name, _) => { - error!("Unsupported subcommand `{name}`"); - } - } + handle_errors_command(sub_matches, converted_args, &mut aggregators, &filters)?; } Some(("locks", _)) => { filters.push(Box::new(crate::filters::LockingFilter::new())); @@ -184,3 +132,64 @@ fn main() -> Result<()> { Ok(()) } + +fn handle_errors_command( + sub_matches: &clap::ArgMatches, + mut converted_args: ConvertedArgs, + aggregators: &mut Vec>, + filters: &Vec>, +) -> Result<()> { + let error_command = sub_matches.subcommand().unwrap_or(("list", sub_matches)); + match error_command { + ("list", list_subcommand) => { + output_results( + converted_args, + *list_subcommand + .get_one::("level") + .unwrap_or(&Severity::Error), + aggregators, + filters, + )?; + } + ("top", top_subcommand) => { + let limit = *top_subcommand.get_one::("max").unwrap_or(&20); + aggregators.push(Box::new(ErrorFrequencyAggregator::new(limit))); + converted_args.print_details = false; + output_results( + converted_args, + *top_subcommand + .get_one::("level") + .unwrap_or(&Severity::Error), + aggregators, + filters, + )?; + } + ("hist", hist_subcommand) => { + converted_args.print_details = false; + let mut interval = Duration::from_hours(1); + if let Some(interval_str) = hist_subcommand.get_one::("bucket") { + interval = parse_duration(interval_str)?; + } + aggregators.push(Box::new(ErrorHistogramAggregator::new(interval))); + debug!( + "Histogram severity: {:?}", + hist_subcommand + .get_one::("level") + .unwrap_or(&Severity::Error) + ); + debug!("Histogram interval: {interval:?}"); + output_results( + converted_args, + *hist_subcommand + .get_one::("level") + .unwrap_or(&Severity::Error), + aggregators, + filters, + )?; + } + (name, _) => { + error!("Unsupported subcommand `{name}`"); + } + } + Ok(()) +} diff --git a/src/output_results/mod.rs b/src/output_results/mod.rs index 84c4dfb..70a5c52 100644 --- a/src/output_results/mod.rs +++ b/src/output_results/mod.rs @@ -41,139 +41,181 @@ pub fn output_results( let mmap = unsafe { MmapOptions::new().map(&file_with_path.file)? }; let bytes: &[u8] = &mmap; - let num_threads = rayon::current_num_threads(); - let chunk_size = bytes.len() / num_threads; - - let mut ranges = Vec::new(); - let mut start = 0; - if let Some(mask) = &converted_args.mask { let mask_filter = Box::new(FilterContains::new(mask.clone())); filter_container.filters.push(mask_filter); } - while start < bytes.len() { - let mut end = (start + chunk_size).min(bytes.len()); - - // Move end forward until a timestamp-starting line - if end < bytes.len() { - while end < bytes.len() { - if bytes[end] == b'\n' { - let next = end + 1; - if next < bytes.len() { - let line_end = bytes[next..] - .iter() - .position(|&b| (b == b'\n') && (b == b'\r')) - .map_or(bytes.len(), |p| next + p); - - if is_record_start(&bytes[next..line_end]) { - break; - } + debug!("File did read in: {:?}", timing.elapsed()); + + if converted_args.before_context > 0 || converted_args.after_context > 0 { + process_with_context( + bytes, + &filter_container, + aggregators, + converted_args.before_context, + converted_args.after_context, + )?; + continue; + } + + process_parallel( + bytes, + &filter_container, + aggregators, + converted_args.print_details, + timing, + )?; + } + Ok(()) +} + +/// Splits the file into per-thread byte ranges, snapping each chunk boundary +/// forward to the start of the next log record so records are never split. +fn compute_ranges(bytes: &[u8]) -> Vec> { + let num_threads = rayon::current_num_threads(); + let chunk_size = bytes.len() / num_threads; + + let mut ranges = Vec::new(); + let mut start = 0; + + while start < bytes.len() { + let mut end = (start + chunk_size).min(bytes.len()); + + // Move end forward until a timestamp-starting line + if end < bytes.len() { + while end < bytes.len() { + if bytes[end] == b'\n' { + let next = end + 1; + if next < bytes.len() { + let line_end = bytes[next..] + .iter() + .position(|&b| (b == b'\n') && (b == b'\r')) + .map_or(bytes.len(), |p| next + p); + + if is_record_start(&bytes[next..line_end]) { + break; } } - end += 1; } + end += 1; } - - ranges.push(start..end); - start = end + 1; } - debug!("File did read in: {:?}", timing.elapsed()); + ranges.push(start..end); + start = end + 1; + } - if converted_args.before_context > 0 || converted_args.after_context > 0 { - let before_n = converted_args.before_context; - let after_n = converted_args.after_context; - let records = collect_records(bytes); - let mut before_buf: std::collections::VecDeque<&[u8]> = - std::collections::VecDeque::new(); - let mut after_remaining: usize = 0; - - for record in records { - if let Some((severity, log_time)) = record_passes(record, &filter_container) { - for prev in &before_buf { - print!("{}", String::from_utf8_lossy(prev)); - } - before_buf.clear(); - print!("{}", String::from_utf8_lossy(record)); - aggragate_record( - aggregators, - record, - &filter_container.format, - severity, - log_time, - )?; - after_remaining = after_n; - } else if after_remaining > 0 { - print!("{}", String::from_utf8_lossy(record)); - after_remaining -= 1; - } else { - before_buf.push_back(record); - while before_buf.len() > before_n { - before_buf.pop_front(); - } - } + ranges +} + +/// Single-threaded path used when `-A`/`-B`/`-C` context lines are requested: +/// prints each matching record together with the surrounding context lines. +fn process_with_context( + bytes: &[u8], + filter_container: &FilterContainer, + aggregators: &mut Vec>, + before_n: usize, + after_n: usize, +) -> Result<()> { + let records = collect_records(bytes); + let mut before_buf: std::collections::VecDeque<&[u8]> = std::collections::VecDeque::new(); + let mut after_remaining: usize = 0; + + for record in records { + if let Some((severity, log_time)) = record_passes(record, filter_container) { + for prev in &before_buf { + print!("{}", String::from_utf8_lossy(prev)); } - for agg in &mut *aggregators { - agg.print(); + before_buf.clear(); + print!("{}", String::from_utf8_lossy(record)); + aggragate_record( + aggregators, + record, + &filter_container.format, + severity, + log_time, + )?; + after_remaining = after_n; + } else if after_remaining > 0 { + print!("{}", String::from_utf8_lossy(record)); + after_remaining -= 1; + } else { + before_buf.push_back(record); + while before_buf.len() > before_n { + before_buf.pop_front(); } - continue; } + } + for agg in &mut *aggregators { + agg.print(); + } + Ok(()) +} - let partials: Result>>> = ranges - .par_iter() - .map(|range| -> Result>> { - let mut local_aggregators: Vec> = - aggregators.iter().map(|a| a.boxed_clone()).collect(); - - let slice = &bytes[range.clone()]; - - let mut record_start = 0; - let mut offset = 0; - - for line in slice.split(|&b| b == b'\n') { - let line_len = line.len() + 1; // include '\n' - - if is_record_start(line) && offset != 0 { - let record = &slice[record_start..offset]; - // debug!("Processing record: {:?} start {} offset {} line_len {}", std::str::from_utf8(record), record_start, offset, line_len); - filter_record( - record, - &filter_container, - &mut local_aggregators, - converted_args.print_details, - )?; - record_start = offset; - } +/// Default path: processes each byte range in parallel with a per-range clone +/// of the aggregators, then merges the partial results back into `aggregators`. +fn process_parallel( + bytes: &[u8], + filter_container: &FilterContainer, + aggregators: &mut Vec>, + print_details: bool, + timing: Instant, +) -> Result<()> { + let ranges = compute_ranges(bytes); - offset += line_len; - } + let partials: Result>>> = ranges + .par_iter() + .map(|range| -> Result>> { + let mut local_aggregators: Vec> = + aggregators.iter().map(|a| a.boxed_clone()).collect(); + + let slice = &bytes[range.clone()]; + + let mut record_start = 0; + let mut offset = 0; - // last record in chunk - if record_start < slice.len() { + for line in slice.split(|&b| b == b'\n') { + let line_len = line.len() + 1; // include '\n' + + if is_record_start(line) && offset != 0 { + let record = &slice[record_start..offset]; filter_record( - &slice[record_start..slice.len()], - &filter_container, + record, + filter_container, &mut local_aggregators, - converted_args.print_details, + print_details, )?; + record_start = offset; } - Ok(local_aggregators) - }) - .collect(); - - debug!("Finished output in: {:?}", timing.elapsed()); - let partials = partials?; - for partial in partials { - for (i, aggregator) in partial.into_iter().enumerate() { - aggregators[i].merge_box(aggregator.as_ref()); + + offset += line_len; } + + // last record in chunk + if record_start < slice.len() { + filter_record( + &slice[record_start..slice.len()], + filter_container, + &mut local_aggregators, + print_details, + )?; + } + Ok(local_aggregators) + }) + .collect(); + + debug!("Finished output in: {:?}", timing.elapsed()); + let partials = partials?; + for partial in partials { + for (i, aggregator) in partial.into_iter().enumerate() { + aggregators[i].merge_box(aggregator.as_ref()); } - for agg in &mut *aggregators { - agg.print(); - } - debug!("Finished aggregating in: {:?}", timing.elapsed()); } + for agg in &mut *aggregators { + agg.print(); + } + debug!("Finished aggregating in: {:?}", timing.elapsed()); Ok(()) } From 78a752f33e36698d8bf561402128f47bfaac90a2 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 18:20:19 +0300 Subject: [PATCH 14/15] Replace numeric casts with cast-free arithmetic (clippy pedantic) - duration.rs: parse ns/us via Duration::from_secs_f64, matching the ms/s/min branches (also rounds instead of truncating). - error_histogram.rs: store bucket counts as usize and compute the bar width with integer rounding instead of float casts. Ticks the numeric-cast tasks in REFACTOR.md. Co-Authored-By: Claude Opus 4.8 --- REFACTOR.md | 16 +++++++++++----- src/aggregators/error_histogram.rs | 9 ++++----- src/duration.rs | 24 ++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/REFACTOR.md b/REFACTOR.md index e2f20df..1413caf 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -34,13 +34,19 @@ Totals: 43 warnings across 8 lints (27 in tests, 16 in `src/`). ### Numeric casts — `src/aggregators/error_histogram.rs:67-69` (6) Use checked/`try_into` conversions or document why the cast is safe (then `#[allow]`). -- [ ] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `usize` (line 67) -- [ ] `cast_precision_loss` ×2 — `i64` → `f64` (line 67) -- [ ] `cast_precision_loss` ×2 — `usize` → `f64` (lines 67, 69) +- [x] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `usize` (line 67) +- [x] `cast_precision_loss` ×2 — `i64` → `f64` (line 67) +- [x] `cast_precision_loss` ×2 — `usize` → `f64` (lines 67, 69) + +Resolved by storing bucket counts as `usize` and computing the bar width +with integer rounding instead of floats. ### Numeric casts — `src/duration.rs:47-48` (4) -- [ ] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `u64` (line 47) -- [ ] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `u64` (line 48) +- [x] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `u64` (line 47) +- [x] `cast_possible_truncation` / `cast_sign_loss` — `f64` → `u64` (line 48) + +Resolved by using `Duration::from_secs_f64` for the `ns`/`us` units, matching +the existing `ms`/`s`/`min` branches. ### Other - [ ] `needless_pass_by_value` — `src/filters/filter_contains_ci.rs:9` — `new(substring: String)` should take `&str` diff --git a/src/aggregators/error_histogram.rs b/src/aggregators/error_histogram.rs index a8cc149..1581b75 100644 --- a/src/aggregators/error_histogram.rs +++ b/src/aggregators/error_histogram.rs @@ -7,7 +7,7 @@ use crate::{aggregators::Aggregator, error::Result, format::Format, severity::Se #[derive(Clone, Default)] pub struct ErrorHistogramAggregator { bucket_width: Duration, - buckets: BTreeMap, + buckets: BTreeMap, } impl ErrorHistogramAggregator { @@ -64,10 +64,9 @@ impl Aggregator for ErrorHistogramAggregator { } for (&bucket, &count) in &self.buckets { - let filled = ((count as f64 / max_count as f64) * BAR_WIDTH as f64) - .round() - .clamp(0.0, BAR_WIDTH as f64) as usize; - + // Integer rounding of `count / max_count * BAR_WIDTH`. Since + // `count <= max_count`, `filled` is always within `[0, BAR_WIDTH]`. + let filled = (count * BAR_WIDTH + max_count / 2) / max_count; let empty = BAR_WIDTH - filled; let time = Local.timestamp_opt(bucket, 0).single(); diff --git a/src/duration.rs b/src/duration.rs index 9543f69..7a9276f 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -44,8 +44,8 @@ fn parse_duration_bytes(value: &str, unit: &[u8]) -> Option { let v: f64 = value.parse().ok()?; match unit { - b"ns" => Some(Duration::from_nanos(v as u64)), - b"us" => Some(Duration::from_micros(v as u64)), + b"ns" => Some(Duration::from_secs_f64(v / 1_000_000_000.0)), + b"us" => Some(Duration::from_secs_f64(v / 1_000_000.0)), b"ms" => Some(Duration::from_secs_f64(v / 1_000.0)), b"s" => Some(Duration::from_secs_f64(v)), b"m" | b"min" | b"minutes" => Some(Duration::from_secs_f64(v * 60.0)), @@ -64,6 +64,26 @@ mod test { assert_eq!(extract_duration(log), Some(Duration::from_micros(121_997))); } + #[test] + fn duration_units_parse_to_exact_durations() { + assert_eq!( + extract_duration(b"duration: 500 ns"), + Some(Duration::from_nanos(500)) + ); + assert_eq!( + extract_duration(b"duration: 250 us"), + Some(Duration::from_micros(250)) + ); + assert_eq!( + extract_duration(b"duration: 3.5 s"), + Some(Duration::from_secs_f64(3.5)) + ); + assert_eq!( + extract_duration(b"duration: 2 min"), + Some(Duration::from_secs(120)) + ); + } + #[test] fn simple_duration_extract_from_log() { let log = b"2025-05-21 11:00:40.296 UTC [675]: [3-1] db=postgres,user=cloudsqladmin,host=127.0.0.1 LOG: duration: 3.032 ms statement: SELECT extname, current_timestamp FROM pg_catalog.pg_extension UNION SELECT plugin, current_timestamp FROM pg_catalog.pg_replication_slots WHERE slot_type = 'logical' AND database = current_database();"; From e11bc302c89a7b3cb690cfc8f831c48e5a7dc720 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Sun, 14 Jun 2026 18:25:26 +0300 Subject: [PATCH 15/15] Fix remaining pedantic lints; codebase is now pedantic-clean - filter_contains_ci.rs: take &str in new() (needless_pass_by_value) - output_results.rs: elide lifetime on record_passes (elidable_lifetime_names) cargo clippy --all-targets -- -W clippy::pedantic now reports zero warnings. Co-Authored-By: Claude Opus 4.8 --- REFACTOR.md | 7 +++++-- src/filters/filter_contains_ci.rs | 2 +- src/main.rs | 4 +--- src/output_results/mod.rs | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/REFACTOR.md b/REFACTOR.md index 1413caf..069ea10 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -25,6 +25,9 @@ cargo clippy --all-targets -- -W clippy::pedantic Totals: 43 warnings across 8 lints (27 in tests, 16 in `src/`). +**All clear** — `cargo clippy --all-targets -- -W clippy::pedantic` now reports +zero warnings. + ## Source code ### `too_many_lines` (3) — split into smaller functions @@ -49,8 +52,8 @@ Resolved by using `Duration::from_secs_f64` for the `ns`/`us` units, matching the existing `ms`/`s`/`min` branches. ### Other -- [ ] `needless_pass_by_value` — `src/filters/filter_contains_ci.rs:9` — `new(substring: String)` should take `&str` -- [ ] `elidable_lifetime_names` — `src/output_results/mod.rs:279` — elide `'a` on `record_passes` (`&FilterContainer<'_>`) +- [x] `needless_pass_by_value` — `src/filters/filter_contains_ci.rs:9` — `new(substring: String)` should take `&str` +- [x] `elidable_lifetime_names` — `src/output_results/mod.rs:279` — elide `'a` on `record_passes` (`&FilterContainer<'_>`) ## Tests diff --git a/src/filters/filter_contains_ci.rs b/src/filters/filter_contains_ci.rs index 8374859..a5fe4e5 100644 --- a/src/filters/filter_contains_ci.rs +++ b/src/filters/filter_contains_ci.rs @@ -6,7 +6,7 @@ pub struct FilterContainsCi { } impl FilterContainsCi { - pub fn new(substring: String) -> Self { + pub fn new(substring: &str) -> Self { FilterContainsCi { substring_lower: substring.to_lowercase(), } diff --git a/src/main.rs b/src/main.rs index ea755d6..bc8db89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,9 +103,7 @@ fn main() -> Result<()> { let term = sub_matches .get_one::("TERM") .expect("TERM is required"); - filters.push(Box::new(crate::filters::FilterContainsCi::new( - term.clone(), - ))); + filters.push(Box::new(crate::filters::FilterContainsCi::new(term))); output_results(converted_args, Severity::Log, &mut aggregators, &filters)?; } Some(("peaks" | "stats", _)) => { diff --git a/src/output_results/mod.rs b/src/output_results/mod.rs index 70a5c52..274e96d 100644 --- a/src/output_results/mod.rs +++ b/src/output_results/mod.rs @@ -310,9 +310,9 @@ fn collect_records(bytes: &[u8]) -> Vec<&[u8]> { records } -fn record_passes<'a>( +fn record_passes( record: &[u8], - filter_container: &FilterContainer<'a>, + filter_container: &FilterContainer<'_>, ) -> Option<(Severity, DateTime)> { for filter in &filter_container.filters { if !filter.matches(record, &filter_container.format) {