peak-mem-0.1.3/.build.yml000064400000000000000000000021161046102023000132210ustar 00000000000000image: alpine/edge packages: - curl - git - build-base sources: - https://git.sr.ht/~charmitro/peak-mem tasks: - setup: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable . "$HOME/.cargo/env" rustup component add rustfmt clippy - fmt: | cd peak-mem . "$HOME/.cargo/env" cargo fmt -- --check - clippy: | cd peak-mem . "$HOME/.cargo/env" cargo clippy --all-targets --all-features -- -D warnings - test: | cd peak-mem . "$HOME/.cargo/env" cargo test --verbose - build: | cd peak-mem . "$HOME/.cargo/env" cargo build --release --verbose - test-binary: | cd peak-mem . "$HOME/.cargo/env" ./target/release/peak-mem -- echo "Hello, CI!" ./target/release/peak-mem --json -- echo "JSON test" ./target/release/peak-mem --csv -- echo "CSV test" ./target/release/peak-mem --quiet -- echo "Quiet test" - msrv: | cd peak-mem . "$HOME/.cargo/env" rustup install 1.87 cargo +1.87 check --verbose peak-mem-0.1.3/.builds/archlinux.yml000064400000000000000000000006761046102023000154120ustar 00000000000000image: archlinux packages: - rust - git sources: - https://git.sr.ht/~charmitro/peak-mem tasks: - test: | cd peak-mem cargo test --verbose - build: | cd peak-mem cargo build --release --verbose - test-binary: | cd peak-mem ./target/release/peak-mem -- echo "Hello from Arch Linux!" ./target/release/peak-mem --json -- echo "JSON test" ./target/release/peak-mem --csv -- echo "CSV test" peak-mem-0.1.3/.builds/debian.yml000064400000000000000000000012411046102023000146240ustar 00000000000000image: debian/stable packages: - curl - git - build-essential sources: - https://git.sr.ht/~charmitro/peak-mem tasks: - setup: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable . "$HOME/.cargo/env" - test: | cd peak-mem . "$HOME/.cargo/env" cargo test --verbose - build: | cd peak-mem . "$HOME/.cargo/env" cargo build --release --verbose - test-binary: | cd peak-mem . "$HOME/.cargo/env" ./target/release/peak-mem -- echo "Hello from Debian!" ./target/release/peak-mem --json -- ps aux ./target/release/peak-mem --csv -- ls -la peak-mem-0.1.3/.builds/freebsd.yml000064400000000000000000000004251046102023000150170ustar 00000000000000image: freebsd/latest packages: - rust sources: - https://git.sr.ht/~charmitro/peak-mem tasks: - check: | cd peak-mem cargo test --verbose - build: | cd peak-mem # Build to ensure it compiles, but don't run cargo build --release --verbose peak-mem-0.1.3/.builds/lint.yml000064400000000000000000000006011046102023000143470ustar 00000000000000image: archlinux packages: - rustup - git sources: - https://git.sr.ht/~charmitro/peak-mem tasks: - setup: | rustup toolchain install stable rustup toolchain install nightly rustup default stable - clippy: | cd peak-mem cargo clippy --all-targets --all-features -- -D warnings - fmt-check: | cd peak-mem cargo +nightly fmt --check peak-mem-0.1.3/.cargo_vcs_info.json0000644000000001360000000000100125310ustar { "git": { "sha1": "8d7f98cf739017868ee35dc00f50de51c7493722" }, "path_in_vcs": "" }peak-mem-0.1.3/.gitignore000064400000000000000000000002011046102023000133020ustar 00000000000000# Rust /target **/*.rs.bk Cargo.lock # IDE .idea/ .vscode/ *.swp *.swo *~ # OS .DS_Store Thumbs.db # Test files test_* *.test peak-mem-0.1.3/.rustfmt.toml000064400000000000000000000005521046102023000140020ustar 00000000000000edition = "2021" newline_style = "Unix" # Unstable options that help catching some mistakes in formatting and that we may want to enable # when they become stable. # # They are kept here since they are useful to run from time to time. format_code_in_doc_comments = true reorder_impl_items = true comment_width = 80 wrap_comments = true normalize_comments = truepeak-mem-0.1.3/Cargo.lock0000644000001076460000000000100105220ustar # This file is automatically @generated by Cargo. # 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.60.2", ] [[package]] name = "assert_cmd" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" dependencies = [ "anstyle", "bstr", "doc-comment", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "autocfg" 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", ] [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bstr" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" version = "1.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", "windows-link", ] [[package]] name = "clap" version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ "bitflags", "crossterm_winapi", "libc", "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.1", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "flate2" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core 0.62.1", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi", "windows-sys 0.59.0", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "ntapi" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "peak-mem" version = "0.1.3" dependencies = [ "assert_cmd", "clap", "crossterm", "libc", "nix", "predicates", "procfs", "serde", "serde_json", "sysinfo", "tempfile", "tokio", "winapi", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "predicates" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" dependencies = [ "anstyle", "difflib", "float-cmp", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ "bitflags", "chrono", "flate2", "hex", "procfs-core", "rustix 0.38.44", ] [[package]] name = "procfs-core" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ "bitflags", "chrono", "hex", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] name = "rustix" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", "windows-sys 0.60.2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" 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 = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio 0.8.11", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sysinfo" version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ "cfg-if", "core-foundation-sys", "libc", "ntapi", "once_cell", "rayon", "windows", ] [[package]] name = "tempfile" version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", "getrandom", "once_cell", "rustix 0.38.44", "windows-sys 0.59.0", ] [[package]] name = "terminal_size" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix 1.0.8", "windows-sys 0.60.2", ] [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "tokio" version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c" dependencies = [ "backtrace", "bytes", "libc", "mio 1.0.4", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-result" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.4", ] [[package]] name = "windows-sys" version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" peak-mem-0.1.3/Cargo.toml0000644000000040140000000000100105260ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "peak-mem" version = "0.1.3" authors = ["Peak-mem Development Team"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Lightweight memory usage monitor for any process" readme = "README" keywords = [ "memory", "monitor", "profiling", "system", "performance", ] categories = [ "command-line-utilities", "development-tools", ] license = "MIT" repository = "https://git.sr.ht/~charmitro/peak-mem" [[bin]] name = "peak-mem" path = "src/main.rs" [dependencies.clap] version = "=4.5.23" features = [ "derive", "cargo", "wrap_help", ] [dependencies.crossterm] version = "=0.27.0" [dependencies.libc] version = "=0.2.169" [dependencies.nix] version = "=0.29.0" features = ["signal"] [dependencies.serde] version = "=1.0.217" features = ["derive"] [dependencies.serde_json] version = "=1.0.139" [dependencies.tokio] version = "=1.43.1" features = ["full"] [dev-dependencies.assert_cmd] version = "=2.0.12" [dev-dependencies.predicates] version = "=3.1.0" [dev-dependencies.tempfile] version = "=3.15.0" [target.'cfg(target_os = "freebsd")'.dependencies.sysinfo] version = "=0.30.13" [target.'cfg(target_os = "linux")'.dependencies.procfs] version = "0.17.0" [target."cfg(windows)".dependencies.winapi] version = "0.3.9" features = [ "processthreadsapi", "psapi", "handleapi", "synchapi", "winbase", "minwindef", "winnt", "memoryapi", ] [profile.release] opt-level = 3 lto = true codegen-units = 1 strip = true peak-mem-0.1.3/Cargo.toml.orig000064400000000000000000000022251046102023000142110ustar 00000000000000[package] name = "peak-mem" version = "0.1.3" edition = "2021" authors = ["Peak-mem Development Team"] description = "Lightweight memory usage monitor for any process" repository = "https://git.sr.ht/~charmitro/peak-mem" license = "MIT" keywords = ["memory", "monitor", "profiling", "system", "performance"] categories = ["command-line-utilities", "development-tools"] [dependencies] clap = { version = "=4.5.23", features = ["derive", "cargo", "wrap_help"] } crossterm = "=0.27.0" libc = "=0.2.169" nix = { version = "=0.29.0", features = ["signal"] } serde = { version = "=1.0.217", features = ["derive"] } serde_json = "=1.0.139" tokio = { version = "=1.43.1", features = ["full"] } [profile.release] lto = true codegen-units = 1 strip = true opt-level = 3 [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.17.0" [target."cfg(windows)".dependencies] winapi = { version = "0.3.9", features = ["processthreadsapi", "psapi", "handleapi", "synchapi", "winbase", "minwindef", "winnt", "memoryapi"] } [target.'cfg(target_os = "freebsd")'.dependencies] sysinfo = "=0.30.13" [dev-dependencies] assert_cmd = "=2.0.12" predicates = "=3.1.0" tempfile = "=3.15.0" peak-mem-0.1.3/LICENSE000064400000000000000000000020711046102023000123260ustar 00000000000000MIT License Copyright (c) 2025 Peak-mem Development Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.peak-mem-0.1.3/README000064400000000000000000000056521046102023000122110ustar 00000000000000peak-mem Monitor peak memory usage of processes. INSTALLATION Install from a local path: cargo install --path . Or install directly from https://crates.io/crates/peak-mem: cargo install peak-mem USAGE peak-mem [OPTIONS] -- COMMAND [ARGS...] OPTIONS -h, --help Show help -V, --version Show version -j, --json Output JSON -c, --csv Output CSV -q, --quiet Output only RSS in bytes -v, --verbose Show process breakdown -w, --watch Show real-time usage -t, --threshold SIZE Set memory threshold --no-children Don't track child processes --timeline FILE Record timeline --interval MS Sampling interval (default: 100) --units UNIT Force memory units (B, KB, MB, GB, KiB, MiB, GiB) Memory Regression Detection: --save-baseline NAME Save current run as baseline --compare-baseline NAME Compare against saved baseline --regression-threshold % Memory increase threshold (default: 10%) --baseline-dir DIR Baseline storage directory --list-baselines List all saved baselines --delete-baseline NAME Delete a saved baseline EXAMPLES Basic usage: peak-mem -- cargo build JSON output: peak-mem --json -- ./myapp Set 1GB threshold: peak-mem --threshold 1G -- ./test Force output in megabytes: peak-mem --units MB -- ./myapp Memory regression detection: # Save a baseline peak-mem --save-baseline v1.0 -- ./myapp # Compare against baseline peak-mem --compare-baseline v1.0 -- ./myapp # Use stricter threshold (5% increase = regression) peak-mem --compare-baseline v1.0 --regression-threshold 5 -- ./myapp # List and manage baselines peak-mem --list-baselines peak-mem --delete-baseline v1.0 BUILDING cargo build --release INSTALLING MANPAGE System-wide installation: sudo install -Dm644 man/man1/peak-mem.1 /usr/share/man/man1/peak-mem.1 Local installation: install -Dm644 man/man1/peak-mem.1 ~/.local/share/man/man1/peak-mem.1 View the manpage: man peak-mem MEMORY REGRESSION DETECTION Peak-mem can save memory usage baselines and compare subsequent runs to detect memory regressions. This is useful for: - CI/CD pipelines to catch memory regressions - Comparing memory usage before/after optimizations - Tracking memory usage across releases Baselines are stored in ~/.cache/peak-mem/baselines/ by default. When comparing, peak-mem will: 1. Report percentage changes in RSS, VSZ, and duration 2. Exit with code 1 if RSS increases exceed the threshold 3. Support multiple output formats (human, JSON, CSV, quiet) PLATFORM SUPPORT Linux - Implemented via /proc macOS - Implemented via proc_pidinfo FreeBSD - Implemented via sysinfo Windows - Not implemented LICENSE MIT peak-mem-0.1.3/man/man1/peak-mem.1000064400000000000000000000162631046102023000145160ustar 00000000000000.TH PEAK-MEM 1 "January 2025" "peak-mem 0.1.0" "User Commands" .SH NAME peak-mem \- monitor peak memory usage of processes .SH SYNOPSIS .B peak-mem [\fIOPTIONS\fR] .B \-\- \fICOMMAND\fR [\fIARGS\fR...] .SH DESCRIPTION .B peak-mem is a lightweight memory usage monitor that tracks and reports the peak memory usage of a process and its children during execution. It provides both real-time monitoring and post-execution reporting with minimal overhead. .PP The tool monitors both RSS (Resident Set Size) and VSZ (Virtual Size) memory metrics, tracking the maximum values reached during the lifetime of the monitored process. .SH OPTIONS .SS Output Format Options .TP .BR \-j ", " \-\-json Output results in JSON format. Useful for parsing by other tools. .TP .BR \-c ", " \-\-csv Output results in CSV format. The output includes headers and is suitable for importing into spreadsheets or data analysis tools. .TP .BR \-q ", " \-\-quiet Quiet mode. Only output the peak RSS value in bytes with no formatting. Useful for scripting. .TP .BR \-v ", " \-\-verbose Show detailed breakdown including process tree. Displays memory usage for each process in the hierarchy. .SS Monitoring Options .TP .BR \-w ", " \-\-watch Display real-time memory usage during execution. Updates the display continuously as the process runs. .TP .BR \-t ", " \-\-threshold " " \fISIZE\fR Set a memory threshold alert. Accepts values like 512M, 1G, 2GB. The program will indicate if the threshold is exceeded. .TP .B \-\-no\-children Don't track child processes. By default, peak-mem monitors the entire process tree. .TP .BR \-\-timeline " " \fIFILE\fR Record detailed memory timeline to the specified file. The timeline includes timestamps and memory values for later analysis. .TP .BR \-\-interval " " \fIMS\fR Set the sampling interval in milliseconds (default: 100). Lower values provide more accurate peak detection but increase overhead. .TP .BR \-\-units " " \fIUNIT\fR Force specific memory units in human-readable output instead of automatic sizing. Supported units: B (bytes), KB (kilobytes), MB (megabytes), GB (gigabytes), KiB (kibibytes), MiB (mebibytes), GiB (gibibytes). This option affects all human-readable output including verbose mode and baseline comparisons. .SS Baseline Management Options .TP .BR \-\-save\-baseline " " \fINAME\fR Save the current run's memory usage as a baseline with the given name. Baselines are stored for future comparison to detect memory regressions. .TP .BR \-\-compare\-baseline " " \fINAME\fR Compare the current run against a previously saved baseline. Reports memory usage changes and indicates if a regression is detected. .TP .BR \-\-regression\-threshold " " \fIPERCENT\fR Set the percentage increase in RSS that constitutes a regression (default: 10.0). Only used with \-\-compare\-baseline. .TP .BR \-\-baseline\-dir " " \fIDIR\fR Directory to store baseline files (default: ~/.cache/peak-mem/baselines). .TP .B \-\-list\-baselines List all saved baselines and exit. .TP .BR \-\-delete\-baseline " " \fINAME\fR Delete a saved baseline and exit. .SS Standard Options .TP .BR \-h ", " \-\-help Display help message and exit. .TP .BR \-V ", " \-\-version Display version information and exit. .SH EXAMPLES .SS Basic Usage Monitor memory usage of a cargo build: .PP .RS .B peak-mem -- cargo build .RE .SS JSON Output Get machine-readable output: .PP .RS .B peak-mem --json -- ./myapp .RE .SS Memory Threshold Alert if memory usage exceeds 1GB: .PP .RS .B peak-mem --threshold 1G -- ./memory-intensive-app .RE .SS Real-time Monitoring Watch memory usage as it happens: .PP .RS .B peak-mem --watch -- ./long-running-process .RE .SS Timeline Recording Record detailed timeline for analysis: .PP .RS .B peak-mem --timeline memory.json -- ./app .RE .SS Process-only Monitoring Monitor only the main process, ignoring children: .PP .RS .B peak-mem --no-children -- ./parent-process .RE .SS Fixed Memory Units Display memory usage in megabytes: .PP .RS .B peak-mem --units MB -- ./myapp .RE .SS Combined Options Verbose output with threshold and timeline: .PP .RS .B peak-mem -v --threshold 2G --timeline mem.json -- make -j8 .RE .SS Memory Regression Detection Save a baseline for your application: .PP .RS .B peak-mem --save-baseline v1.0 -- ./myapp .RE .PP Compare against the baseline after changes: .PP .RS .B peak-mem --compare-baseline v1.0 -- ./myapp .RE .PP Use stricter regression threshold (5%): .PP .RS .B peak-mem --compare-baseline v1.0 --regression-threshold 5 -- ./myapp .RE .PP List and manage baselines: .PP .RS .B peak-mem --list-baselines .br .B peak-mem --delete-baseline v1.0 .RE .SH OUTPUT FORMATS .SS Human-readable (default) Shows peak RSS and VSZ in human-readable units (KB, MB, GB) along with the monitored command and exit status. .SS JSON Format (-j) Outputs a JSON object containing: .RS .IP \(bu 2 command: The executed command with arguments .IP \(bu 2 peak_rss_bytes: Peak RSS in bytes .IP \(bu 2 peak_vsz_bytes: Peak VSZ in bytes .IP \(bu 2 duration_ms: Execution time in milliseconds .IP \(bu 2 exit_code: Process exit code .IP \(bu 2 threshold_exceeded: Boolean (if threshold was set) .IP \(bu 2 tracked_children: Boolean indicating if children were tracked .RE .SS CSV Format (-c) Outputs CSV with headers: .RS command,peak_rss_bytes,peak_vsz_bytes,duration_ms,exit_code .RE .SS Quiet Format (-q) Outputs only the peak RSS value in bytes as a plain number. .SS Verbose Format (-v) Shows detailed process tree with individual memory usage for each process, including PIDs and process names. .SH MEMORY UNITS Memory sizes can be specified using the following units: .RS .IP \(bu 2 K, KB: Kilobytes (1024 bytes) .IP \(bu 2 M, MB: Megabytes (1024² bytes) .IP \(bu 2 G, GB: Gigabytes (1024³ bytes) .IP \(bu 2 No suffix: bytes .RE .SH EXIT STATUS .B peak-mem normally exits with the same status code as the monitored command. If the monitored command is terminated by a signal, peak-mem exits with status 128 + signal number. .PP Special exit codes: .RS .IP "1" 8 Memory threshold exceeded (when using --threshold) .IP "1" 8 Memory regression detected (when using --compare-baseline) .RE .SH PLATFORM SUPPORT .IP "Linux" 12 Full support via /proc filesystem .IP "macOS" 12 Full support via proc_pidinfo .IP "FreeBSD" 12 Not currently implemented .IP "Windows" 12 Not currently implemented .SH LIMITATIONS .IP \(bu 2 Memory sampling occurs at intervals (default 100ms), so very brief spikes might be missed. Decrease the interval for more accurate peak detection. .IP \(bu 2 On some systems, tracking child processes requires appropriate permissions. .IP \(bu 2 Timeline files can grow large for long-running processes with small intervals. .SH ENVIRONMENT .B peak-mem forwards all environment variables to the monitored process without modification. .SH SIGNALS .B peak-mem forwards most signals to the monitored process, allowing for proper cleanup and termination handling. .SH FILES .TP .I /proc/[pid]/status On Linux, used to read memory information. .TP .I /proc/[pid]/task/ On Linux, used to track all threads of a process. .SH SEE ALSO .BR time (1), .BR ps (1), .BR top (1), .BR htop (1), .BR pmap (1) .SH BUGS Report bugs at: ~charmitro/peak-mem-devel@lists.sr.ht .SH AUTHOR Written by the peak-mem contributors. .SH COPYRIGHT Copyright © 2025 peak-mem contributors. License: MIT peak-mem-0.1.3/src/baseline.rs000064400000000000000000000266001046102023000142440ustar 00000000000000//! Baseline comparison functionality for detecting memory usage regressions. //! //! This module provides functionality to save memory usage snapshots as //! baselines and compare new measurements against them to detect regressions. use crate::types::{MonitorResult, Result, Timestamp}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env; use std::fs; use std::path::PathBuf; /// Represents a saved baseline measurement for comparison. /// /// Baselines capture key metrics from a monitoring session along with /// metadata about the environment where the measurement was taken. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Baseline { /// Version of peak-mem that created this baseline. pub version: String, /// When this baseline was created. pub created_at: Timestamp, /// Command that was monitored. pub command: String, /// Peak RSS value in bytes. pub peak_rss_bytes: u64, /// Peak VSZ value in bytes. pub peak_vsz_bytes: u64, /// Duration of execution in milliseconds. pub duration_ms: u64, /// Additional metadata (platform, architecture, etc.). pub metadata: HashMap, } impl From<&MonitorResult> for Baseline { fn from(result: &MonitorResult) -> Self { let mut metadata = HashMap::new(); metadata.insert("platform".to_string(), std::env::consts::OS.to_string()); metadata.insert("arch".to_string(), std::env::consts::ARCH.to_string()); if let Some(pid) = result.main_pid { metadata.insert("main_pid".to_string(), pid.to_string()); } Self { version: env!("CARGO_PKG_VERSION").to_string(), created_at: Timestamp::now(), command: result.command.clone(), peak_rss_bytes: result.peak_rss_bytes, peak_vsz_bytes: result.peak_vsz_bytes, duration_ms: result.duration_ms, metadata, } } } /// Result of comparing current measurements against a baseline. /// /// Contains detailed information about differences in memory usage /// and whether a regression was detected based on the threshold. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComparisonResult { /// The baseline being compared against. pub baseline: Baseline, /// Current measurement results. pub current: MonitorResult, /// Difference in RSS bytes (positive means increase). pub rss_diff_bytes: i64, /// Percentage change in RSS. pub rss_diff_percent: f64, /// Difference in VSZ bytes (positive means increase). pub vsz_diff_bytes: i64, /// Percentage change in VSZ. pub vsz_diff_percent: f64, /// Difference in duration milliseconds. pub duration_diff_ms: i64, /// Percentage change in duration. pub duration_diff_percent: f64, /// Whether memory usage exceeded the regression threshold. pub regression_detected: bool, } impl ComparisonResult { /// Creates a new comparison result. /// /// # Arguments /// * `baseline` - The baseline to compare against /// * `current` - Current measurement results /// * `threshold_percent` - Percentage increase that triggers regression /// detection pub fn new(baseline: Baseline, current: MonitorResult, threshold_percent: f64) -> Self { let rss_diff_bytes = current.peak_rss_bytes as i64 - baseline.peak_rss_bytes as i64; let rss_diff_percent = if baseline.peak_rss_bytes > 0 { (rss_diff_bytes as f64 / baseline.peak_rss_bytes as f64) * 100.0 } else { 0.0 }; let vsz_diff_bytes = current.peak_vsz_bytes as i64 - baseline.peak_vsz_bytes as i64; let vsz_diff_percent = if baseline.peak_vsz_bytes > 0 { (vsz_diff_bytes as f64 / baseline.peak_vsz_bytes as f64) * 100.0 } else { 0.0 }; let duration_diff_ms = current.duration_ms as i64 - baseline.duration_ms as i64; let duration_diff_percent = if baseline.duration_ms > 0 { (duration_diff_ms as f64 / baseline.duration_ms as f64) * 100.0 } else { 0.0 }; let regression_detected = rss_diff_percent > threshold_percent; Self { baseline, current, rss_diff_bytes, rss_diff_percent, vsz_diff_bytes, vsz_diff_percent, duration_diff_ms, duration_diff_percent, regression_detected, } } } /// Manages baseline storage and retrieval. /// /// Handles saving baselines to disk, loading them for comparison, /// and managing the baseline directory. pub struct BaselineManager { baselines_dir: PathBuf, } impl BaselineManager { /// Creates a new baseline manager with a specific directory. /// /// # Arguments /// * `baselines_dir` - Directory to store baseline files /// /// # Errors /// * Returns error if directory creation fails pub fn new(baselines_dir: PathBuf) -> Result { if !baselines_dir.exists() { fs::create_dir_all(&baselines_dir)?; } Ok(Self { baselines_dir }) } /// Returns the default baseline directory path. /// /// Uses the system cache directory if available, otherwise /// falls back to a local directory. pub fn default_dir() -> PathBuf { // Try XDG_CACHE_HOME first (Linux/Unix standard) if let Ok(xdg_cache) = env::var("XDG_CACHE_HOME") { return PathBuf::from(xdg_cache).join("peak-mem").join("baselines"); } // Try HOME for default cache location if let Ok(home) = env::var("HOME") { #[cfg(target_os = "macos")] return PathBuf::from(home) .join("Library") .join("Caches") .join("peak-mem") .join("baselines"); #[cfg(not(target_os = "macos"))] return PathBuf::from(home) .join(".cache") .join("peak-mem") .join("baselines"); } // Windows: try LOCALAPPDATA #[cfg(windows)] if let Ok(local_app_data) = env::var("LOCALAPPDATA") { return PathBuf::from(local_app_data) .join("peak-mem") .join("baselines"); } // Fallback to local directory PathBuf::from(".peak-mem-baselines") } /// Saves a monitoring result as a baseline. /// /// # Arguments /// * `name` - Name for the baseline (will be sanitized) /// * `result` - Monitoring results to save /// /// # Returns /// * Path to the saved baseline file pub fn save_baseline(&self, name: &str, result: &MonitorResult) -> Result { let baseline = Baseline::from(result); let filename = format!("{}.json", sanitize_filename(name)); let path = self.baselines_dir.join(&filename); let json = serde_json::to_string_pretty(&baseline)?; fs::write(&path, json)?; Ok(path) } pub fn load_baseline(&self, name: &str) -> Result { let filename = format!("{}.json", sanitize_filename(name)); let path = self.baselines_dir.join(&filename); let json = fs::read_to_string(&path)?; let baseline: Baseline = serde_json::from_str(&json)?; Ok(baseline) } pub fn list_baselines(&self) -> Result> { let mut baselines = Vec::new(); for entry in fs::read_dir(&self.baselines_dir)? { let entry = entry?; let path = entry.path(); if path.extension().and_then(|s| s.to_str()) == Some("json") { if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { baselines.push(stem.to_string()); } } } baselines.sort(); Ok(baselines) } pub fn delete_baseline(&self, name: &str) -> Result<()> { let filename = format!("{}.json", sanitize_filename(name)); let path = self.baselines_dir.join(&filename); fs::remove_file(&path)?; Ok(()) } pub fn compare( &self, baseline_name: &str, current: &MonitorResult, threshold_percent: f64, ) -> Result { let baseline = self.load_baseline(baseline_name)?; // Clone is necessary here because ComparisonResult needs to own the // MonitorResult for serialization and output formatting purposes Ok(ComparisonResult::new( baseline, current.clone(), threshold_percent, )) } } fn sanitize_filename(name: &str) -> String { name.chars() .map(|c| match c { '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', _ => c, }) .collect() } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_baseline_conversion() { let result = MonitorResult { command: "test".to_string(), peak_rss_bytes: 100 * 1024 * 1024, peak_vsz_bytes: 200 * 1024 * 1024, duration_ms: 5000, exit_code: Some(0), threshold_exceeded: false, timestamp: Timestamp::now(), process_tree: None, timeline: None, start_time: None, sample_count: None, main_pid: Some(1234), }; let baseline = Baseline::from(&result); assert_eq!(baseline.command, "test"); assert_eq!(baseline.peak_rss_bytes, 100 * 1024 * 1024); assert_eq!(baseline.peak_vsz_bytes, 200 * 1024 * 1024); assert_eq!(baseline.duration_ms, 5000); assert!(baseline.metadata.contains_key("platform")); assert!(baseline.metadata.contains_key("arch")); assert_eq!(baseline.metadata.get("main_pid"), Some(&"1234".to_string())); } #[test] fn test_baseline_manager() { let temp_dir = TempDir::new().unwrap(); let manager = BaselineManager::new(temp_dir.path().to_path_buf()).unwrap(); let result = MonitorResult { command: "test".to_string(), peak_rss_bytes: 100 * 1024 * 1024, peak_vsz_bytes: 200 * 1024 * 1024, duration_ms: 5000, exit_code: Some(0), threshold_exceeded: false, timestamp: Timestamp::now(), process_tree: None, timeline: None, start_time: None, sample_count: None, main_pid: None, }; // Save baseline let path = manager.save_baseline("test_baseline", &result).unwrap(); assert!(path.exists()); // Load baseline let loaded = manager.load_baseline("test_baseline").unwrap(); assert_eq!(loaded.command, "test"); assert_eq!(loaded.peak_rss_bytes, 100 * 1024 * 1024); // List baselines let baselines = manager.list_baselines().unwrap(); assert_eq!(baselines, vec!["test_baseline"]); // Delete baseline manager.delete_baseline("test_baseline").unwrap(); let baselines = manager.list_baselines().unwrap(); assert!(baselines.is_empty()); } #[test] fn test_sanitize_filename() { assert_eq!(sanitize_filename("test/file"), "test_file"); assert_eq!(sanitize_filename("test:file"), "test_file"); assert_eq!(sanitize_filename("test*file"), "test_file"); assert_eq!(sanitize_filename("normal_file"), "normal_file"); } } peak-mem-0.1.3/src/cli.rs000064400000000000000000000142301046102023000132250ustar 00000000000000use crate::types::{ByteSize, PeakMemError, Result}; use clap::{ArgAction, Parser}; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MemoryUnit { Bytes, Kilobytes, Megabytes, Gigabytes, Kibibytes, Mebibytes, Gibibytes, } impl MemoryUnit { pub fn format(&self, bytes: u64) -> String { match self { MemoryUnit::Bytes => format!("{bytes} B"), MemoryUnit::Kilobytes => format!("{:.1} KB", bytes as f64 / 1_000.0), MemoryUnit::Megabytes => format!("{:.1} MB", bytes as f64 / 1_000_000.0), MemoryUnit::Gigabytes => format!("{:.1} GB", bytes as f64 / 1_000_000_000.0), MemoryUnit::Kibibytes => format!("{:.1} KiB", bytes as f64 / 1_024.0), MemoryUnit::Mebibytes => format!("{:.1} MiB", bytes as f64 / 1_048_576.0), MemoryUnit::Gibibytes => format!("{:.1} GiB", bytes as f64 / 1_073_741_824.0), } } } #[derive(Parser, Debug)] #[command( name = "peak-mem", author, about = "Lightweight memory usage monitor for any process", long_about = "Peak-mem monitors and reports the peak memory usage of any program during its execution.\n\n\ It tracks both resident set size (RSS) and virtual memory size (VSZ) with minimal overhead.", disable_version_flag = true )] pub struct Cli { #[arg( trailing_var_arg = true, value_name = "COMMAND", help = "Command to execute and monitor", required_unless_present_any = &["list_baselines", "delete_baseline", "short_version", "long_version"] )] pub command: Vec, #[arg( short = 'j', long = "json", help = "Output in JSON format", conflicts_with_all = &["csv", "quiet"] )] pub json: bool, #[arg( short = 'c', long = "csv", help = "Output in CSV format", conflicts_with_all = &["json", "quiet"] )] pub csv: bool, #[arg( short = 'q', long = "quiet", help = "Only output peak RSS value", conflicts_with_all = &["json", "csv", "verbose"] )] pub quiet: bool, #[arg( short = 'v', long = "verbose", help = "Show detailed breakdown", conflicts_with = "quiet" )] pub verbose: bool, #[arg( short = 'w', long = "watch", help = "Show real-time memory usage", conflicts_with_all = &["json", "csv", "quiet"] )] pub watch: bool, #[arg( short = 't', long = "threshold", value_name = "SIZE", help = "Set memory threshold (e.g., 512M, 1G)", value_parser = parse_threshold )] pub threshold: Option, #[arg( long = "no-children", help = "Don't track child processes", action = ArgAction::SetTrue )] pub no_children: bool, #[arg( long = "timeline", value_name = "FILE", help = "Record memory timeline to file" )] pub timeline: Option, #[arg( long = "interval", value_name = "MS", default_value = "100", help = "Sampling interval in milliseconds", value_parser = parse_interval )] pub interval: u64, #[arg( long = "units", value_name = "UNIT", help = "Force specific memory units (B, KB, MB, GB, KiB, MiB, GiB)", value_parser = parse_units )] pub units: Option, #[arg( long = "save-baseline", value_name = "NAME", help = "Save the result as a baseline with the given name", conflicts_with = "compare_baseline" )] pub save_baseline: Option, #[arg( long = "compare-baseline", value_name = "NAME", help = "Compare results against a saved baseline", conflicts_with = "save_baseline" )] pub compare_baseline: Option, #[arg( long = "regression-threshold", value_name = "PERCENT", default_value = "10.0", help = "Memory increase percentage to consider as regression" )] pub regression_threshold: f64, #[arg( long = "baseline-dir", value_name = "DIR", help = "Directory to store baselines (default: ~/.cache/peak-mem/baselines)" )] pub baseline_dir: Option, #[arg( long = "list-baselines", help = "List all saved baselines and exit", conflicts_with_all = &["command", "save_baseline", "compare_baseline"] )] pub list_baselines: bool, #[arg( long = "delete-baseline", value_name = "NAME", help = "Delete a saved baseline and exit", conflicts_with_all = &["command", "save_baseline", "compare_baseline", "list_baselines"] )] pub delete_baseline: Option, #[arg(short = 'V', help = "Short version")] pub short_version: bool, #[arg(long = "version", help = "Long version info")] pub long_version: bool, } fn parse_threshold(s: &str) -> Result { s.parse::() } fn parse_interval(s: &str) -> Result { let interval: u64 = s.parse()?; if interval == 0 { return Err(PeakMemError::InvalidArgument( "Interval must be greater than zero".to_string(), )); } Ok(interval) } fn parse_units(s: &str) -> Result { match s { "B" => Ok(MemoryUnit::Bytes), "KB" => Ok(MemoryUnit::Kilobytes), "MB" => Ok(MemoryUnit::Megabytes), "GB" => Ok(MemoryUnit::Gigabytes), "KiB" => Ok(MemoryUnit::Kibibytes), "MiB" => Ok(MemoryUnit::Mebibytes), "GiB" => Ok(MemoryUnit::Gibibytes), _ => Err(PeakMemError::InvalidArgument( "Invalid unit. Use one of: B, KB, MB, GB, KiB, MiB, GiB".to_string(), )), } } impl Cli { pub fn output_format(&self) -> OutputFormat { if self.json { OutputFormat::Json } else if self.csv { OutputFormat::Csv } else if self.quiet { OutputFormat::Quiet } else { OutputFormat::Human } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OutputFormat { Human, Json, Csv, Quiet, } peak-mem-0.1.3/src/main.rs000064400000000000000000000264611046102023000134130ustar 00000000000000mod baseline; mod cli; mod monitor; mod output; mod process; mod types; use crate::types::{ByteSize, PeakMemError, Result, Timestamp}; use baseline::BaselineManager; use clap::Parser; use monitor::tracker::MemoryTracker; use output::{OutputFormatter, RealtimeDisplay}; use std::time::Instant; use tokio::time; /// Application state and logic handler. struct Application { args: cli::Cli, baseline_manager: BaselineManager, } impl Application { /// Creates a new application instance. fn new(args: cli::Cli) -> Result { let baseline_dir = args .baseline_dir .clone() .unwrap_or_else(BaselineManager::default_dir); let baseline_manager = BaselineManager::new(baseline_dir)?; Ok(Self { args, baseline_manager, }) } /// Runs the application. async fn run(self) -> Result<()> { // Handle version if self.handle_version() { return Ok(()); } // Handle baseline-only operations if self.handle_baseline_only_operations()? { return Ok(()); } // Run the command and monitor memory let result = self.monitor_command().await?; // Handle output and exit self.handle_results(result) } fn handle_version(&self) -> bool { if self.args.short_version { println!("{}", env!("CARGO_PKG_VERSION")); return true; } else if self.args.long_version { println!("peak-mem {}", env!("CARGO_PKG_VERSION")); return true; } false } /// Handles baseline operations that don't require running a command. /// Returns true if the operation was handled and the app should exit. fn handle_baseline_only_operations(&self) -> Result { if self.args.list_baselines { self.list_baselines()?; return Ok(true); } if let Some(name) = &self.args.delete_baseline { self.baseline_manager.delete_baseline(name)?; println!("Baseline '{name}' deleted."); return Ok(true); } Ok(false) } /// Lists all saved baselines. fn list_baselines(&self) -> Result<()> { let baselines = self.baseline_manager.list_baselines()?; if baselines.is_empty() { println!("No baselines found."); } else { println!("Saved baselines:"); for name in baselines { println!(" {name}"); } } Ok(()) } /// Monitors a command's memory usage. async fn monitor_command(&self) -> Result { // Create process runner let runner = process::ProcessRunner::new(self.args.command.clone())?; let command_string = runner.command_string(); // Spawn the process let handle = runner.spawn().await?; let pid = handle.pid(); // Set up memory tracking let monitor = monitor::create_monitor()?; let tracker = MemoryTracker::new(monitor, pid, !self.args.no_children); let start_time = Instant::now(); let start_timestamp = Timestamp::now(); let tracker_handle = tracker.start(self.args.interval).await; // Run process with optional real-time display let exit_code = if self.args.watch { run_with_realtime_display(handle, &tracker, self.args.interval, self.args.units).await? } else { handle.wait_with_signal_forwarding().await? }; // Stop tracking and collect results tracker.stop(); tracker_handle.await?; // Build the result self.build_monitor_result( command_string, &tracker, start_time, start_timestamp, exit_code, pid, ) .await } /// Builds the monitoring result from collected data. async fn build_monitor_result( &self, command: String, tracker: &MemoryTracker, start_time: Instant, start_timestamp: Timestamp, exit_code: Option, pid: u32, ) -> Result { let duration_ms = start_time.elapsed().as_millis() as u64; let peak_rss_bytes = tracker.peak_rss(); let peak_vsz_bytes = tracker.peak_vsz(); // Check threshold let threshold_exceeded = self.check_threshold(peak_rss_bytes); // Get optional data based on flags let process_tree = self.get_process_tree_if_verbose(tracker).await; let timeline = self.get_timeline_if_requested(tracker).await; let (start_time_opt, sample_count, main_pid) = self.get_verbose_data(start_timestamp, tracker.sample_count(), pid); Ok(types::MonitorResult { command, peak_rss_bytes, peak_vsz_bytes, duration_ms, exit_code, threshold_exceeded, timestamp: Timestamp::now(), process_tree, timeline, start_time: start_time_opt, sample_count, main_pid, }) } /// Checks if the memory usage exceeded the configured threshold. fn check_threshold(&self, peak_rss_bytes: u64) -> bool { self.args .threshold .map(|threshold| ByteSize::b(peak_rss_bytes) > threshold) .unwrap_or(false) } /// Gets the process tree if verbose mode is enabled. async fn get_process_tree_if_verbose( &self, tracker: &MemoryTracker, ) -> Option { if self.args.verbose && !self.args.no_children { match tracker.get_process_tree().await { Ok(tree) => Some(tree), Err(e) => { eprintln!("Warning: Failed to get process tree: {e}"); None } } } else { None } } /// Gets the timeline if requested. async fn get_timeline_if_requested( &self, tracker: &MemoryTracker, ) -> Option> { if self.args.timeline.is_some() { Some(tracker.timeline().await) } else { None } } /// Gets verbose data if verbose mode is enabled. fn get_verbose_data( &self, start_timestamp: Timestamp, sample_count: u64, pid: u32, ) -> (Option, Option, Option) { if self.args.verbose { (Some(start_timestamp), Some(sample_count), Some(pid)) } else { (None, None, None) } } /// Handles the results: saves timeline, manages baselines, formats output. fn handle_results(&self, result: types::MonitorResult) -> Result<()> { // Save timeline if requested if let Err(e) = self.save_timeline_if_requested(&result) { eprintln!("Warning: Failed to save timeline: {e}"); } // Handle baseline operations self.handle_baseline_operations(&result)?; // Handle comparison or normal output let exit_code = if let Some(baseline_name) = &self.args.compare_baseline { self.handle_comparison(baseline_name, &result)? } else { self.handle_normal_output(&result)? }; // Exit with appropriate code if let Some(code) = exit_code { std::process::exit(code); } Ok(()) } /// Saves the timeline to a file if requested. fn save_timeline_if_requested(&self, result: &types::MonitorResult) -> Result<()> { if let Some(timeline_path) = &self.args.timeline { if let Some(timeline) = &result.timeline { let json = serde_json::to_string_pretty(timeline)?; std::fs::write(timeline_path, json)?; } } Ok(()) } /// Handles baseline save operations. fn handle_baseline_operations(&self, result: &types::MonitorResult) -> Result<()> { if let Some(baseline_name) = &self.args.save_baseline { let path = self.baseline_manager.save_baseline(baseline_name, result)?; eprintln!("Baseline '{}' saved to: {}", baseline_name, path.display()); } Ok(()) } /// Handles baseline comparison. fn handle_comparison( &self, baseline_name: &str, result: &types::MonitorResult, ) -> Result> { let comparison = self.baseline_manager .compare(baseline_name, result, self.args.regression_threshold)?; OutputFormatter::format_comparison( &comparison, self.args.output_format(), self.args.units, )?; if comparison.regression_detected { Ok(Some(1)) } else { Ok(result.exit_code) } } /// Handles normal output (no comparison). fn handle_normal_output(&self, result: &types::MonitorResult) -> Result> { OutputFormatter::format( result, self.args.output_format(), self.args.verbose, self.args.units, )?; if result.threshold_exceeded { Ok(Some(1)) } else { Ok(result.exit_code) } } } fn main() -> Result<()> { // Configure tokio runtime with optimized thread stack size for // Linux/macOS. Based on measurements showing ~10KB actual usage let mut builder = tokio::runtime::Builder::new_multi_thread(); #[cfg(any(target_os = "linux", target_os = "macos"))] builder.thread_stack_size(128 * 1024); // 128KB (vs default 2MB) let runtime = builder .enable_all() .build() .map_err(|e| PeakMemError::Runtime(format!("Failed to build runtime: {}", e)))?; runtime.block_on(async { let args = cli::Cli::parse(); let app = Application::new(args)?; app.run().await }) } async fn run_with_realtime_display( handle: process::ProcessHandle, tracker: &MemoryTracker, interval_ms: u64, units: Option, ) -> Result> { let pid = handle.pid(); let monitor = monitor::create_monitor()?; let peak_rss_atom = tracker.peak_rss.clone(); let peak_vsz_atom = tracker.peak_vsz.clone(); let monitor_task = tokio::spawn(async move { let mut display = RealtimeDisplay::new(units); let mut interval = time::interval(time::Duration::from_millis(interval_ms)); loop { interval.tick().await; if let Ok(usage) = monitor.get_memory_usage(pid).await { let current_rss = ByteSize::b(usage.rss_bytes); let current_vsz = ByteSize::b(usage.vsz_bytes); let peak_rss = ByteSize::b(peak_rss_atom.load(std::sync::atomic::Ordering::SeqCst)); let peak_vsz = ByteSize::b(peak_vsz_atom.load(std::sync::atomic::Ordering::SeqCst)); if display .update(current_rss, peak_rss, current_vsz, peak_vsz) .is_err() { break; } } else { // Process terminated break; } } let _ = display.clear(); }); let exit_code = handle.wait_with_signal_forwarding().await?; monitor_task.abort(); Ok(exit_code) } peak-mem-0.1.3/src/monitor/freebsd.rs000064400000000000000000000102251046102023000155570ustar 00000000000000use crate::monitor::MemoryMonitor; use crate::types::{MemoryUsage, PeakMemError, ProcessMemoryInfo, Result, Timestamp}; use std::future::Future; use std::pin::Pin; use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System}; pub struct FreeBSDMonitor { system: std::sync::Mutex, } impl FreeBSDMonitor { pub fn new() -> Result { // On FreeBSD, we avoid initializing with process refresh in tests // to prevent signal conflicts with the test runner let system = if cfg!(test) { System::new() } else { System::new_with_specifics( RefreshKind::new().with_processes(ProcessRefreshKind::everything()), ) }; Ok(FreeBSDMonitor { system: std::sync::Mutex::new(system), }) } fn refresh_process(&self, pid: u32) -> Result<()> { let sysinfo_pid = Pid::from_u32(pid); let mut system = self.system.lock().unwrap(); // Use ProcessRefreshKind::everything() to ensure all data including memory is // refreshed if !system.refresh_process_specifics(sysinfo_pid, ProcessRefreshKind::everything()) { return Err(PeakMemError::ProcessSpawn(format!( "Process {pid} not found" ))); } Ok(()) } fn get_process_info(&self, pid: u32) -> Result<(String, u64, u64)> { let sysinfo_pid = Pid::from_u32(pid); let system = self.system.lock().unwrap(); let process = system .process(sysinfo_pid) .ok_or_else(|| PeakMemError::ProcessSpawn(format!("Process {pid} not found")))?; let name = process.name().to_string(); let rss_bytes = process.memory(); let vsz_bytes = process.virtual_memory(); Ok((name, rss_bytes, vsz_bytes)) } fn collect_child_pids(&self, pid: u32) -> Vec { let sysinfo_pid = Pid::from_u32(pid); let system = self.system.lock().unwrap(); system .processes() .iter() .filter_map(|(child_pid, child_process)| { if child_process.parent() == Some(sysinfo_pid) { Some(child_pid.as_u32()) } else { None } }) .collect() } async fn build_process_tree(&self, pid: u32) -> Result { self.refresh_process(pid)?; let (name, rss_bytes, vsz_bytes) = self.get_process_info(pid)?; let memory = MemoryUsage { rss_bytes, vsz_bytes, timestamp: Timestamp::now(), }; // Get child processes let child_pids = self.collect_child_pids(pid); // Build child trees let mut children = Vec::new(); for child_pid in child_pids { match Box::pin(self.build_process_tree(child_pid)).await { Ok(child_tree) => children.push(child_tree), Err(_) => continue, // Child might have exited } } Ok(ProcessMemoryInfo { pid, name, memory, children, }) } } impl MemoryMonitor for FreeBSDMonitor { fn get_memory_usage( &self, pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { self.refresh_process(pid)?; let (_name, rss_bytes, vsz_bytes) = self.get_process_info(pid)?; Ok(MemoryUsage { rss_bytes, vsz_bytes, timestamp: Timestamp::now(), }) }) } fn get_process_tree( &self, pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { self.build_process_tree(pid).await }) } fn get_child_pids( &self, pid: u32, ) -> Pin>> + Send + '_>> { Box::pin(async move { { let mut system = self.system.lock().unwrap(); system.refresh_processes(); } Ok(self.collect_child_pids(pid)) }) } } peak-mem-0.1.3/src/monitor/linux.rs000064400000000000000000000071221046102023000153060ustar 00000000000000use crate::monitor::MemoryMonitor; use crate::types::{MemoryUsage, PeakMemError, ProcessMemoryInfo, Result, Timestamp}; use procfs::process::Process; use std::future::Future; use std::pin::Pin; pub struct LinuxMonitor; impl LinuxMonitor { pub fn new() -> Result { Ok(LinuxMonitor) } fn read_proc_status(&self, pid: u32) -> Result<(u64, u64)> { let process = Process::new(pid as i32).map_err(|e| match e { procfs::ProcError::NotFound(_) => { PeakMemError::ProcessSpawn(format!("Process {pid} not found")) } procfs::ProcError::PermissionDenied(_) => { PeakMemError::PermissionDenied(format!("Cannot access process {pid}")) } _ => PeakMemError::ProcessSpawn(format!("Failed to access process {pid}: {e}")), })?; let status = process.status().map_err(|e| { PeakMemError::ProcessSpawn(format!("Failed to read process {pid} status: {e}")) })?; let rss_bytes = status.vmrss.unwrap_or(0) * 1024; let vsz_bytes = status.vmsize.unwrap_or(0) * 1024; Ok((rss_bytes, vsz_bytes)) } fn get_process_name(&self, pid: u32) -> String { Process::new(pid as i32) .and_then(|p| p.stat()) .map(|stat| stat.comm) .unwrap_or_else(|_| format!("pid:{pid}")) } } impl MemoryMonitor for LinuxMonitor { fn get_memory_usage( &self, pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { let (rss_bytes, vsz_bytes) = self.read_proc_status(pid)?; Ok(MemoryUsage { rss_bytes, vsz_bytes, timestamp: Timestamp::now(), }) }) } fn get_process_tree( &self, pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { let memory = self.get_memory_usage(pid).await?; let name = self.get_process_name(pid); let child_pids = self.get_child_pids(pid).await?; let mut children = Vec::new(); for child_pid in child_pids { if let Ok(child_info) = self.get_process_tree(child_pid).await { children.push(child_info); } } Ok(ProcessMemoryInfo { pid, name, memory, children, }) }) } fn get_child_pids( &self, pid: u32, ) -> Pin>> + Send + '_>> { Box::pin(async move { let mut children = Vec::new(); // Use procfs to iterate through all processes if let Ok(all_procs) = procfs::process::all_processes() { for process in all_procs.flatten() { if let Ok(stat) = process.stat() { if stat.ppid == pid as i32 { children.push(stat.pid as u32); } } } } Ok(children) }) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_get_memory_usage_self() { let monitor = LinuxMonitor::new().unwrap(); let pid = std::process::id(); let usage = monitor.get_memory_usage(pid).await; assert!(usage.is_ok()); let usage = usage.unwrap(); assert!(usage.rss_bytes > 0); assert!(usage.vsz_bytes >= usage.rss_bytes); } } peak-mem-0.1.3/src/monitor/macos.rs000064400000000000000000000162331046102023000152540ustar 00000000000000use crate::monitor::MemoryMonitor; use crate::types::{MemoryUsage, PeakMemError, ProcessMemoryInfo, Result, Timestamp}; use std::future::Future; use std::mem; use std::pin::Pin; pub struct MacOSMonitor; impl MacOSMonitor { pub fn new() -> Result { Ok(MacOSMonitor) } fn get_memory_for_pid(&self, pid: u32) -> Result<(u64, u64)> { use libc::{proc_pidinfo, proc_taskinfo, PROC_PIDTASKINFO}; let mut info: proc_taskinfo = unsafe { mem::zeroed() }; let size = mem::size_of::() as i32; let ret = unsafe { proc_pidinfo( pid as i32, PROC_PIDTASKINFO, 0, &mut info as *mut _ as *mut _, size, ) }; if ret <= 0 { return Err(PeakMemError::ProcessSpawn(format!( "Process {pid} not found" ))); } Ok((info.pti_resident_size, info.pti_virtual_size)) } } impl MemoryMonitor for MacOSMonitor { fn get_memory_usage( &self, pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { let (rss_bytes, vsz_bytes) = self.get_memory_for_pid(pid)?; Ok(MemoryUsage { rss_bytes, vsz_bytes, timestamp: Timestamp::now(), }) }) } fn get_process_tree( &self, pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { let memory = self.get_memory_usage(pid).await?; let name = get_process_name(pid)?; let child_pids = self.get_child_pids(pid).await?; let mut children = Vec::new(); for child_pid in child_pids { if let Ok(child_info) = self.get_process_tree(child_pid).await { children.push(child_info); } } Ok(ProcessMemoryInfo { pid, name, memory, children, }) }) } fn get_child_pids( &self, pid: u32, ) -> Pin>> + Send + '_>> { Box::pin(async move { // Use libproc to get process list - the modern macOS approach // This is more reliable than parsing sysctl's kinfo_proc structure // which has undocumented layout changes between macOS versions use std::ptr; // External functions from libproc extern "C" { fn proc_listpids( type_: u32, typeinfo: u32, buffer: *mut libc::c_void, buffersize: libc::c_int, ) -> libc::c_int; fn proc_pidinfo( pid: libc::c_int, flavor: libc::c_int, arg: u64, buffer: *mut libc::c_void, buffersize: libc::c_int, ) -> libc::c_int; } const PROC_ALL_PIDS: u32 = 1; const PROC_PIDTBSDINFO: libc::c_int = 3; #[repr(C)] struct proc_bsdinfo { pbi_flags: u32, pbi_status: u32, pbi_xstatus: u32, pbi_pid: u32, pbi_ppid: u32, pbi_uid: libc::uid_t, pbi_gid: libc::gid_t, pbi_ruid: libc::uid_t, pbi_rgid: libc::gid_t, pbi_svuid: libc::uid_t, pbi_svgid: libc::gid_t, rfu_1: u32, pbi_comm: [libc::c_char; 16], pbi_name: [libc::c_char; 32], pbi_nfiles: u32, pbi_pgid: u32, pbi_pjobc: u32, e_tdev: u32, e_tpgid: u32, pbi_nice: libc::c_int, pbi_start_tvsec: u64, pbi_start_tvusec: u64, } // Get the size needed for all PIDs let buffer_size = unsafe { proc_listpids(PROC_ALL_PIDS, 0, ptr::null_mut(), 0) }; if buffer_size <= 0 { return Err(PeakMemError::Monitor( "Failed to get process list size".to_string(), )); } // Allocate buffer for PIDs let pid_count = (buffer_size as usize) / mem::size_of::(); let mut pids = vec![0 as libc::pid_t; pid_count]; // Get all PIDs let bytes_returned = unsafe { proc_listpids( PROC_ALL_PIDS, 0, pids.as_mut_ptr() as *mut libc::c_void, buffer_size, ) }; if bytes_returned <= 0 { return Err(PeakMemError::Monitor( "Failed to get process list".to_string(), )); } let actual_pid_count = (bytes_returned as usize) / mem::size_of::(); let mut children = Vec::new(); // Check each PID to see if it's a child of our target for &check_pid in pids.iter().take(actual_pid_count) { if check_pid == 0 { continue; } let mut proc_info: proc_bsdinfo = unsafe { mem::zeroed() }; let ret = unsafe { proc_pidinfo( check_pid, PROC_PIDTBSDINFO, 0, &mut proc_info as *mut _ as *mut libc::c_void, mem::size_of::() as libc::c_int, ) }; if ret == mem::size_of::() as libc::c_int && proc_info.pbi_ppid == pid { children.push(check_pid as u32); } } Ok(children) }) } } fn get_process_name(pid: u32) -> Result { use libc::{proc_pidpath, PROC_PIDPATHINFO_MAXSIZE}; use std::ffi::CStr; let mut path_buf = vec![0u8; PROC_PIDPATHINFO_MAXSIZE as usize]; let ret = unsafe { proc_pidpath( pid as i32, path_buf.as_mut_ptr() as *mut _, path_buf.len() as u32, ) }; if ret <= 0 { return Ok(format!("pid:{pid}")); } // Extract just the filename from the path let path = unsafe { CStr::from_ptr(path_buf.as_ptr() as *const _) .to_string_lossy() .into_owned() }; Ok(path .split('/') .next_back() .unwrap_or(&format!("pid:{pid}")) .to_string()) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_get_memory_usage_self() { let monitor = MacOSMonitor::new().unwrap(); let pid = std::process::id(); let usage = monitor.get_memory_usage(pid).await; assert!(usage.is_ok()); let usage = usage.unwrap(); assert!(usage.rss_bytes > 0); assert!(usage.vsz_bytes >= usage.rss_bytes); } } peak-mem-0.1.3/src/monitor/mod.rs000064400000000000000000000061171046102023000147310ustar 00000000000000//! Platform-agnostic memory monitoring interface and implementations. //! //! This module provides a trait-based abstraction for memory monitoring //! across different operating systems, along with platform-specific //! implementations. use crate::types::{MemoryUsage, ProcessMemoryInfo, Result}; use std::future::Future; use std::pin::Pin; use std::sync::Arc; use tokio::sync::Mutex; pub mod tracker; #[cfg(target_os = "linux")] pub mod linux; #[cfg(target_os = "macos")] pub mod macos; #[cfg(windows)] pub mod windows; #[cfg(target_os = "freebsd")] pub mod freebsd; /// Trait defining the interface for platform-specific memory monitors. /// /// Each platform must implement this trait to provide memory monitoring /// capabilities. The trait is async to support potentially blocking /// system calls without blocking the runtime. pub trait MemoryMonitor: Send + Sync { /// Get the current memory usage for a specific process. /// /// # Arguments /// * `pid` - Process ID to monitor /// /// # Returns /// * `Result` - Current memory statistics or error fn get_memory_usage( &self, pid: u32, ) -> Pin> + Send + '_>>; /// Get the complete process tree with memory information. /// /// # Arguments /// * `pid` - Root process ID /// /// # Returns /// * `Result` - Process tree with memory data or error fn get_process_tree( &self, pid: u32, ) -> Pin> + Send + '_>>; /// Get the list of child process IDs for a given process. /// /// # Arguments /// * `pid` - Parent process ID /// /// # Returns /// * `Result>` - List of child PIDs or error #[allow(dead_code)] fn get_child_pids( &self, pid: u32, ) -> Pin>> + Send + '_>>; } /// Thread-safe shared reference to a memory monitor. pub type SharedMonitor = Arc>>; /// Creates a platform-specific memory monitor instance. /// /// This factory function automatically selects the appropriate monitor /// implementation based on the compilation target. /// /// # Returns /// * `Result>` - Platform-specific monitor or error /// /// # Errors /// * `PeakMemError::UnsupportedPlatform` - Platform not supported pub fn create_monitor() -> Result> { #[cfg(target_os = "linux")] { Ok(Box::new(linux::LinuxMonitor::new()?)) } #[cfg(target_os = "macos")] { Ok(Box::new(macos::MacOSMonitor::new()?)) } #[cfg(windows)] { Ok(Box::new(windows::WindowsMonitor::new()?)) } #[cfg(target_os = "freebsd")] { Ok(Box::new(freebsd::FreeBSDMonitor::new()?)) } #[cfg(not(any( target_os = "linux", target_os = "macos", windows, target_os = "freebsd" )))] { Err(crate::types::PeakMemError::UnsupportedPlatform( std::env::consts::OS.to_string(), )) } } peak-mem-0.1.3/src/monitor/tracker.rs000064400000000000000000000304371046102023000156070ustar 00000000000000//! Continuous memory tracking with peak detection. //! //! This module provides the `MemoryTracker` which continuously monitors //! a process's memory usage and maintains peak values. use crate::monitor::{MemoryMonitor, SharedMonitor}; use crate::types::{MemoryUsage, ProcessMemoryInfo, Result}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tokio::time; /// Tracks memory usage over time for a process and its children. /// /// The tracker runs in a background task, periodically sampling memory usage /// and updating peak values using lock-free atomic operations. pub struct MemoryTracker { monitor: SharedMonitor, pid: u32, /// Peak RSS value observed (in bytes), updated atomically. pub peak_rss: Arc, /// Peak VSZ value observed (in bytes), updated atomically. pub peak_vsz: Arc, timeline: Arc>>, running: Arc, track_children: bool, sample_count: Arc, peak_process_tree: Arc>>, } impl MemoryTracker { /// Creates a new memory tracker for a specific process. /// /// # Arguments /// * `monitor` - Platform-specific memory monitor implementation /// * `pid` - Process ID to track /// * `track_children` - Whether to include child processes in measurements pub fn new(monitor: Box, pid: u32, track_children: bool) -> Self { Self { monitor: Arc::new(tokio::sync::Mutex::new(monitor)), pid, peak_rss: Arc::new(AtomicU64::new(0)), peak_vsz: Arc::new(AtomicU64::new(0)), timeline: Arc::new(RwLock::new(Vec::new())), running: Arc::new(AtomicBool::new(false)), track_children, sample_count: Arc::new(AtomicU64::new(0)), peak_process_tree: Arc::new(RwLock::new(None)), } } /// Starts the background tracking task. /// /// The task will sample memory usage at the specified interval until /// `stop()` is called. /// /// # Arguments /// * `interval_ms` - Sampling interval in milliseconds /// /// # Returns /// * `JoinHandle` for the spawned tracking task pub async fn start(&self, interval_ms: u64) -> tokio::task::JoinHandle<()> { let monitor = Arc::clone(&self.monitor); let pid = self.pid; let peak_rss = Arc::clone(&self.peak_rss); let peak_vsz = Arc::clone(&self.peak_vsz); let timeline = Arc::clone(&self.timeline); let running = Arc::clone(&self.running); let track_children = self.track_children; let sample_count = Arc::clone(&self.sample_count); let peak_process_tree = Arc::clone(&self.peak_process_tree); running.store(true, Ordering::SeqCst); tokio::spawn(async move { let mut interval = time::interval(Duration::from_millis(interval_ms)); interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip); // Sample immediately let monitor_guard = monitor.lock().await; if track_children { if let Ok(tree) = monitor_guard.get_process_tree(pid).await { let mut total_rss = 0u64; let mut total_vsz = 0u64; Self::sum_tree_memory(&tree, &mut total_rss, &mut total_vsz); peak_rss.store(total_rss, Ordering::SeqCst); peak_vsz.store(total_vsz, Ordering::SeqCst); sample_count.fetch_add(1, Ordering::SeqCst); // Store initial process tree let mut pt = peak_process_tree.write().await; *pt = Some(tree.clone()); let mut tl = timeline.write().await; tl.push(MemoryUsage { rss_bytes: total_rss, vsz_bytes: total_vsz, timestamp: tree.memory.timestamp, }); } } else if let Ok(usage) = monitor_guard.get_memory_usage(pid).await { peak_rss.store(usage.rss_bytes, Ordering::SeqCst); peak_vsz.store(usage.vsz_bytes, Ordering::SeqCst); sample_count.fetch_add(1, Ordering::SeqCst); let mut tl = timeline.write().await; tl.push(usage); } drop(monitor_guard); while running.load(Ordering::SeqCst) { interval.tick().await; let monitor = monitor.lock().await; if track_children { match monitor.get_process_tree(pid).await { Ok(tree) => { let mut total_rss = 0u64; let mut total_vsz = 0u64; Self::sum_tree_memory(&tree, &mut total_rss, &mut total_vsz); // Check if this is a new peak let old_peak = peak_rss.load(Ordering::SeqCst); if total_rss > old_peak { peak_rss.store(total_rss, Ordering::SeqCst); peak_vsz.store(total_vsz, Ordering::SeqCst); // Update peak process tree let mut pt = peak_process_tree.write().await; *pt = Some(tree.clone()); } else { peak_rss.fetch_max(total_rss, Ordering::SeqCst); peak_vsz.fetch_max(total_vsz, Ordering::SeqCst); } sample_count.fetch_add(1, Ordering::SeqCst); let mut tl = timeline.write().await; tl.push(MemoryUsage { rss_bytes: total_rss, vsz_bytes: total_vsz, timestamp: tree.memory.timestamp, }); } Err(_) => { // Process likely terminated break; } } } else { match monitor.get_memory_usage(pid).await { Ok(usage) => { // Update peaks peak_rss.fetch_max(usage.rss_bytes, Ordering::SeqCst); peak_vsz.fetch_max(usage.vsz_bytes, Ordering::SeqCst); sample_count.fetch_add(1, Ordering::SeqCst); // Add to timeline let mut tl = timeline.write().await; tl.push(usage); } Err(_) => { // Process likely terminated break; } } } drop(monitor); } }) } /// Stops the background tracking task. pub fn stop(&self) { self.running.store(false, Ordering::SeqCst); } /// Returns the peak RSS value observed so far. pub fn peak_rss(&self) -> u64 { self.peak_rss.load(Ordering::SeqCst) } /// Returns the peak VSZ value observed so far. pub fn peak_vsz(&self) -> u64 { self.peak_vsz.load(Ordering::SeqCst) } /// Returns a copy of the collected timeline data. pub async fn timeline(&self) -> Vec { self.timeline.read().await.clone() } /// Returns the number of samples collected. pub fn sample_count(&self) -> u64 { self.sample_count.load(Ordering::SeqCst) } /// Returns the process tree captured at peak memory usage. /// /// # Returns /// * `Ok(ProcessMemoryInfo)` - Process tree at peak /// * `Err` - If no process tree has been captured yet pub async fn get_process_tree(&self) -> Result { let tree_lock = self.peak_process_tree.read().await; tree_lock.clone().ok_or_else(|| { crate::types::PeakMemError::ProcessSpawn("No process tree available".to_string()) }) } /// Recursively sums memory usage across a process tree. /// /// # Arguments /// * `info` - Root of process tree /// * `rss` - Accumulator for RSS bytes /// * `vsz` - Accumulator for VSZ bytes fn sum_tree_memory(info: &crate::types::ProcessMemoryInfo, rss: &mut u64, vsz: &mut u64) { *rss += info.memory.rss_bytes; *vsz += info.memory.vsz_bytes; for child in &info.children { Self::sum_tree_memory(child, rss, vsz); } } } #[cfg(test)] mod tests { use super::*; use crate::monitor::create_monitor; #[tokio::test] async fn test_memory_tracker() { let monitor = create_monitor().unwrap(); let pid = std::process::id(); let tracker = MemoryTracker::new(monitor, pid, false); // Start tracking with very short interval let handle = tracker.start(1).await; // Wait for at least one sample to be collected // Instead of time-based wait, check for samples let mut retries = 0; while tracker.sample_count() == 0 && retries < 100 { tokio::task::yield_now().await; retries += 1; } tracker.stop(); handle.await.unwrap(); // Verify we collected data assert!(tracker.peak_rss() > 0, "Peak RSS should be greater than 0"); assert!(tracker.peak_vsz() > 0, "Peak VSZ should be greater than 0"); assert!( tracker.sample_count() > 0, "Should have collected at least one sample" ); let timeline = tracker.timeline().await; assert!(!timeline.is_empty(), "Timeline should not be empty"); } #[tokio::test] async fn test_process_tree_capture() { let monitor = create_monitor().unwrap(); let pid = std::process::id(); let tracker = MemoryTracker::new(monitor, pid, true); // Start tracking let handle = tracker.start(1).await; // Wait for process tree to be captured let mut retries = 0; let mut tree_captured = false; while retries < 100 { if tracker.get_process_tree().await.is_ok() { tree_captured = true; break; } tokio::task::yield_now().await; retries += 1; } tracker.stop(); handle.await.unwrap(); // Verify process tree was captured assert!(tree_captured, "Process tree should have been captured"); let tree = tracker.get_process_tree().await.unwrap(); assert_eq!(tree.pid, pid); assert!(!tree.name.is_empty()); assert!(tree.memory.rss_bytes > 0); } #[tokio::test] async fn test_process_tree_with_children() { use tokio::process::Command; // Create a process that will definitely exist long enough to be monitored let mut child = Command::new("sh") .arg("-c") .arg("while true; do sleep 0.1; done") .spawn() .expect("Failed to spawn test process"); let pid = child.id().expect("Failed to get PID"); let monitor = create_monitor().unwrap(); let tracker = MemoryTracker::new(monitor, pid, true); // Start tracking with short interval let handle = tracker.start(1).await; // Wait for process tree to be captured (deterministic check) let mut tree_captured = false; let mut retries = 0; while retries < 100 { if let Ok(tree) = tracker.get_process_tree().await { if tree.pid == pid && tree.memory.rss_bytes > 0 { tree_captured = true; break; } } tokio::task::yield_now().await; retries += 1; } tracker.stop(); // Clean up first let _ = child.kill().await; let _ = child.wait().await; handle.await.unwrap(); // Now assert assert!(tree_captured, "Should have captured process tree"); assert!(tracker.sample_count() > 0, "Should have collected samples"); } } peak-mem-0.1.3/src/monitor/windows.rs000064400000000000000000000024601046102023000156410ustar 00000000000000use crate::monitor::MemoryMonitor; use crate::types::{MemoryUsage, PeakMemError, ProcessMemoryInfo, Result}; use std::future::Future; use std::pin::Pin; pub struct WindowsMonitor; impl WindowsMonitor { pub fn new() -> Result { Ok(WindowsMonitor) } } impl MemoryMonitor for WindowsMonitor { fn get_memory_usage( &self, _pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { // Windows implementation would use GetProcessMemoryInfo Err(PeakMemError::UnsupportedPlatform( "Windows support not yet implemented".to_string(), )) }) } fn get_process_tree( &self, _pid: u32, ) -> Pin> + Send + '_>> { Box::pin(async move { Err(PeakMemError::UnsupportedPlatform( "Windows support not yet implemented".to_string(), )) }) } fn get_child_pids( &self, _pid: u32, ) -> Pin>> + Send + '_>> { Box::pin(async move { Err(PeakMemError::UnsupportedPlatform( "Windows support not yet implemented".to_string(), )) }) } } peak-mem-0.1.3/src/output/mod.rs000064400000000000000000000551641046102023000146100ustar 00000000000000//! Output formatting for memory monitoring results. //! //! This module provides formatters for different output formats including //! human-readable, JSON, CSV, and quiet modes. use crate::baseline::ComparisonResult; use crate::cli::{MemoryUnit, OutputFormat}; use crate::types::{ByteSize, MonitorResult, ProcessMemoryInfo, Result}; use std::io::{self, Write}; /// Simple CSV writer that handles escaping struct CsvWriter { writer: W, } impl CsvWriter { fn new(writer: W) -> Self { CsvWriter { writer } } /// Write a single CSV record (row) fn write_record(&mut self, fields: &[&str]) -> Result<()> { for (i, field) in fields.iter().enumerate() { if i > 0 { write!(self.writer, ",")?; } // Escape field if it contains comma, quote, or newline if field.contains(',') || field.contains('"') || field.contains('\n') { write!(self.writer, "\"")?; for c in field.chars() { if c == '"' { write!(self.writer, "\"\"")?; } else { write!(self.writer, "{}", c)?; } } write!(self.writer, "\"")?; } else { write!(self.writer, "{}", field)?; } } writeln!(self.writer)?; Ok(()) } fn flush(&mut self) -> Result<()> { self.writer.flush()?; Ok(()) } } /// Handles formatting of monitoring results for different output formats. pub struct OutputFormatter; impl OutputFormatter { /// Formats monitoring results according to the specified format. /// /// # Arguments /// * `result` - The monitoring results to format /// * `format` - The output format to use /// * `verbose` - Whether to include verbose information /// * `units` - Optional fixed memory unit to use for display pub fn format( result: &MonitorResult, format: OutputFormat, verbose: bool, units: Option, ) -> Result<()> { match format { OutputFormat::Human => { if verbose { Self::format_verbose(result, units) } else { Self::format_human(result, units) } } OutputFormat::Json => Self::format_json(result), OutputFormat::Csv => Self::format_csv(result), OutputFormat::Quiet => Self::format_quiet(result), } } fn format_human(result: &MonitorResult, units: Option) -> Result<()> { let mut stdout = io::stdout(); writeln!(stdout, "Command: {}", result.command)?; if let Some(unit) = units { write!( stdout, "Peak memory usage: {} (RSS)", unit.format(result.peak_rss_bytes) )?; writeln!(stdout, " / {} (VSZ)", unit.format(result.peak_vsz_bytes))?; } else { write!(stdout, "Peak memory usage: {} (RSS)", result.peak_rss())?; writeln!(stdout, " / {} (VSZ)", result.peak_vsz())?; } if let Some(exit_code) = result.exit_code { writeln!(stdout, "Exit code: {exit_code}")?; } writeln!(stdout, "Duration: {:.1}s", result.duration().as_secs_f64())?; if result.threshold_exceeded { writeln!(stdout, "\n⚠️ THRESHOLD EXCEEDED")?; } stdout.flush()?; Ok(()) } fn format_json(result: &MonitorResult) -> Result<()> { let json = serde_json::to_string_pretty(result)?; println!("{json}"); Ok(()) } fn format_csv(result: &MonitorResult) -> Result<()> { let mut wtr = CsvWriter::new(io::stdout()); wtr.write_record(&[ "command", "peak_rss_bytes", "peak_vsz_bytes", "duration_ms", "exit_code", "threshold_exceeded", "timestamp", ])?; let exit_code_str = result.exit_code.map_or(String::new(), |c| c.to_string()); wtr.write_record(&[ &result.command, &result.peak_rss_bytes.to_string(), &result.peak_vsz_bytes.to_string(), &result.duration_ms.to_string(), &exit_code_str, &result.threshold_exceeded.to_string(), &result.timestamp.to_rfc3339(), ])?; wtr.flush()?; Ok(()) } fn format_quiet(result: &MonitorResult) -> Result<()> { println!("{}", result.peak_rss_bytes); Ok(()) } fn format_verbose(result: &MonitorResult, units: Option) -> Result<()> { let mut stdout = io::stdout(); // Header writeln!(stdout, "Command: {}", result.command)?; if let Some(start_time) = result.start_time { writeln!(stdout, "Started: {} UTC", start_time.format_datetime())?; } if let Some(pid) = result.main_pid { writeln!(stdout, "Process ID: {pid}")?; } writeln!(stdout)?; // Memory Usage Section writeln!(stdout, "Memory Usage:")?; if let Some(unit) = units { writeln!( stdout, " Peak RSS: {} ({} bytes)", unit.format(result.peak_rss_bytes), result.peak_rss_bytes )?; writeln!( stdout, " Peak VSZ: {} ({} bytes)", unit.format(result.peak_vsz_bytes), result.peak_vsz_bytes )?; } else { writeln!( stdout, " Peak RSS: {} ({} bytes)", result.peak_rss(), result.peak_rss_bytes )?; writeln!( stdout, " Peak VSZ: {} ({} bytes)", result.peak_vsz(), result.peak_vsz_bytes )?; } writeln!(stdout)?; // Process Tree Section if let Some(tree) = &result.process_tree { let process_count = Self::count_processes(tree); writeln!( stdout, "Process Tree: ({process_count} processes monitored)" )?; Self::print_process_tree(&mut stdout, tree, "", true, units)?; } else { writeln!( stdout, "Process Tree: (monitoring disabled with --no-children)" )?; } writeln!(stdout)?; // Performance Section writeln!(stdout, "Performance:")?; writeln!( stdout, " Duration: {:.3}s", result.duration().as_secs_f64() )?; if let Some(sample_count) = result.sample_count { writeln!(stdout, " Samples collected: {sample_count}")?; } writeln!( stdout, " Sampling interval: {}ms", result.duration_ms / result.sample_count.unwrap_or(1).max(1) )?; writeln!(stdout)?; // Exit Status if let Some(exit_code) = result.exit_code { writeln!( stdout, "Exit Status: {} ({})", exit_code, if exit_code == 0 { "success" } else { "failed" } )?; } // Threshold Status if result.threshold_exceeded { writeln!(stdout, "\n⚠️ THRESHOLD EXCEEDED")?; } stdout.flush()?; Ok(()) } fn count_processes(tree: &ProcessMemoryInfo) -> usize { 1 + tree .children .iter() .map(Self::count_processes) .sum::() } fn print_process_tree( stdout: &mut dyn Write, tree: &ProcessMemoryInfo, prefix: &str, is_last: bool, units: Option, ) -> Result<()> { // Print current process let connector = if is_last { "└── " } else { "├── " }; let name = if tree.name.len() > 40 { format!("{}...", &tree.name[..37]) } else { tree.name.clone() }; let memory_str = if let Some(unit) = units { unit.format(tree.memory.rss_bytes) } else { ByteSize::b(tree.memory.rss_bytes).to_string() }; writeln!( stdout, "{}{}{} (PID: {}) - Peak: {}", prefix, if prefix.is_empty() { "" } else { connector }, name, tree.pid, memory_str )?; // Sort children by peak RSS (descending) let mut children = tree.children.clone(); children.sort_by(|a, b| b.memory.rss_bytes.cmp(&a.memory.rss_bytes)); // Print children with proper tree structure let child_prefix = format!( "{}{}", prefix, if prefix.is_empty() { "" } else if is_last { " " } else { "│ " } ); for (i, child) in children.iter().enumerate() { let is_last_child = i == children.len() - 1; Self::print_process_tree(stdout, child, &child_prefix, is_last_child, units)?; } Ok(()) } /// Formats baseline comparison results. /// /// # Arguments /// * `comparison` - The comparison results /// * `format` - The output format to use /// * `units` - Optional fixed memory unit to use for display pub fn format_comparison( comparison: &ComparisonResult, format: OutputFormat, units: Option, ) -> Result<()> { match format { OutputFormat::Human => Self::format_comparison_human(comparison, units), OutputFormat::Json => Self::format_comparison_json(comparison), OutputFormat::Csv => Self::format_comparison_csv(comparison), OutputFormat::Quiet => Self::format_comparison_quiet(comparison), } } fn format_comparison_human( comparison: &ComparisonResult, units: Option, ) -> Result<()> { let mut stdout = io::stdout(); writeln!(stdout, "Command: {}", comparison.current.command)?; writeln!(stdout)?; writeln!(stdout, "Baseline vs Current:")?; if let Some(unit) = units { writeln!( stdout, " Peak RSS: {} → {} ({:+.1}%)", unit.format(comparison.baseline.peak_rss_bytes), unit.format(comparison.current.peak_rss_bytes), comparison.rss_diff_percent )?; } else { writeln!( stdout, " Peak RSS: {} → {} ({:+.1}%)", ByteSize::b(comparison.baseline.peak_rss_bytes), comparison.current.peak_rss(), comparison.rss_diff_percent )?; } if comparison.rss_diff_bytes > 0 { if let Some(unit) = units { writeln!( stdout, " Absolute increase: {}", unit.format(comparison.rss_diff_bytes as u64) )?; } else { writeln!( stdout, " Absolute increase: {}", ByteSize::b(comparison.rss_diff_bytes as u64) )?; } } else if comparison.rss_diff_bytes < 0 { if let Some(unit) = units { writeln!( stdout, " Absolute decrease: {}", unit.format((-comparison.rss_diff_bytes) as u64) )?; } else { writeln!( stdout, " Absolute decrease: {}", ByteSize::b((-comparison.rss_diff_bytes) as u64) )?; } } writeln!(stdout)?; if let Some(unit) = units { writeln!( stdout, " Peak VSZ: {} → {} ({:+.1}%)", unit.format(comparison.baseline.peak_vsz_bytes), unit.format(comparison.current.peak_vsz_bytes), comparison.vsz_diff_percent )?; } else { writeln!( stdout, " Peak VSZ: {} → {} ({:+.1}%)", ByteSize::b(comparison.baseline.peak_vsz_bytes), comparison.current.peak_vsz(), comparison.vsz_diff_percent )?; } writeln!(stdout)?; writeln!( stdout, " Duration: {:.1}s → {:.1}s ({:+.1}%)", comparison.baseline.duration_ms as f64 / 1000.0, comparison.current.duration().as_secs_f64(), comparison.duration_diff_percent )?; writeln!(stdout)?; if comparison.regression_detected { writeln!( stdout, "❌ REGRESSION DETECTED: Memory usage increased by {:.1}%", comparison.rss_diff_percent )?; } else { writeln!(stdout, "✅ No regression detected")?; } stdout.flush()?; Ok(()) } fn format_comparison_json(comparison: &ComparisonResult) -> Result<()> { let json = serde_json::to_string_pretty(comparison)?; println!("{json}"); Ok(()) } fn format_comparison_csv(comparison: &ComparisonResult) -> Result<()> { let mut wtr = CsvWriter::new(io::stdout()); wtr.write_record(&[ "baseline_command", "baseline_rss_bytes", "baseline_vsz_bytes", "baseline_duration_ms", "current_command", "current_rss_bytes", "current_vsz_bytes", "current_duration_ms", "rss_diff_bytes", "rss_diff_percent", "vsz_diff_bytes", "vsz_diff_percent", "duration_diff_ms", "duration_diff_percent", "regression_detected", ])?; wtr.write_record(&[ &comparison.baseline.command, &comparison.baseline.peak_rss_bytes.to_string(), &comparison.baseline.peak_vsz_bytes.to_string(), &comparison.baseline.duration_ms.to_string(), &comparison.current.command, &comparison.current.peak_rss_bytes.to_string(), &comparison.current.peak_vsz_bytes.to_string(), &comparison.current.duration_ms.to_string(), &comparison.rss_diff_bytes.to_string(), &comparison.rss_diff_percent.to_string(), &comparison.vsz_diff_bytes.to_string(), &comparison.vsz_diff_percent.to_string(), &comparison.duration_diff_ms.to_string(), &comparison.duration_diff_percent.to_string(), &comparison.regression_detected.to_string(), ])?; wtr.flush()?; Ok(()) } fn format_comparison_quiet(comparison: &ComparisonResult) -> Result<()> { if comparison.regression_detected { println!("regression"); } else { println!("ok"); } Ok(()) } } /// Handles real-time display of memory usage in watch mode. /// /// Uses terminal control sequences to update the display in-place. pub struct RealtimeDisplay { last_line_count: usize, units: Option, } impl RealtimeDisplay { /// Creates a new real-time display handler. pub fn new(units: Option) -> Self { Self { last_line_count: 0, units, } } /// Updates the display with current memory values. /// /// Clears previous lines and writes new values in-place. /// /// # Arguments /// * `current_rss` - Current RSS value /// * `peak_rss` - Peak RSS value observed /// * `current_vsz` - Current VSZ value /// * `peak_vsz` - Peak VSZ value observed pub fn update( &mut self, current_rss: ByteSize, peak_rss: ByteSize, current_vsz: ByteSize, peak_vsz: ByteSize, ) -> Result<()> { use crossterm::{cursor, terminal, ExecutableCommand}; let mut stdout = io::stdout(); // Clear previous lines for _ in 0..self.last_line_count { stdout.execute(cursor::MoveToPreviousLine(1))?; stdout.execute(terminal::Clear(terminal::ClearType::CurrentLine))?; } // Print new status if let Some(unit) = self.units { writeln!( stdout, "Current RSS: {} | Peak RSS: {}", unit.format(current_rss.as_u64()), unit.format(peak_rss.as_u64()) )?; writeln!( stdout, "Current VSZ: {} | Peak VSZ: {}", unit.format(current_vsz.as_u64()), unit.format(peak_vsz.as_u64()) )?; } else { writeln!(stdout, "Current RSS: {current_rss} | Peak RSS: {peak_rss}")?; writeln!(stdout, "Current VSZ: {current_vsz} | Peak VSZ: {peak_vsz}")?; } stdout.flush()?; self.last_line_count = 2; Ok(()) } /// Clears the real-time display. /// /// Removes all lines written by the display. pub fn clear(&mut self) -> Result<()> { use crossterm::{cursor, terminal, ExecutableCommand}; let mut stdout = io::stdout(); for _ in 0..self.last_line_count { stdout.execute(cursor::MoveToPreviousLine(1))?; stdout.execute(terminal::Clear(terminal::ClearType::CurrentLine))?; } stdout.flush()?; self.last_line_count = 0; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::types::{MemoryUsage, Timestamp}; #[test] fn test_format_quiet() { let result = MonitorResult { command: "test".to_string(), peak_rss_bytes: 123456789, peak_vsz_bytes: 987654321, duration_ms: 1000, exit_code: Some(0), threshold_exceeded: false, timestamp: Timestamp::now(), process_tree: None, timeline: None, start_time: None, sample_count: None, main_pid: None, }; // Quiet format should just print the RSS bytes OutputFormatter::format(&result, OutputFormat::Quiet, false, None).unwrap(); } #[test] fn test_format_verbose() { let now = Timestamp::now(); // Create a sample process tree let child_process = ProcessMemoryInfo { pid: 12346, name: "rustc".to_string(), memory: MemoryUsage { rss_bytes: 442_123_456, vsz_bytes: 512_123_456, timestamp: now, }, children: vec![ ProcessMemoryInfo { pid: 12347, name: "cc".to_string(), memory: MemoryUsage { rss_bytes: 23_456_789, vsz_bytes: 45_678_901, timestamp: now, }, children: vec![], }, ProcessMemoryInfo { pid: 12348, name: "ld".to_string(), memory: MemoryUsage { rss_bytes: 89_123_456, vsz_bytes: 123_456_789, timestamp: now, }, children: vec![], }, ], }; let root_process = ProcessMemoryInfo { pid: 12345, name: "cargo".to_string(), memory: MemoryUsage { rss_bytes: 45_234_567, vsz_bytes: 78_901_234, timestamp: now, }, children: vec![child_process], }; let result = MonitorResult { command: "cargo build --release".to_string(), peak_rss_bytes: 487_300_000, peak_vsz_bytes: 892_100_000, duration_ms: 14_263, exit_code: Some(0), threshold_exceeded: false, timestamp: now, process_tree: Some(root_process), timeline: None, start_time: Some(now), sample_count: Some(142), main_pid: Some(12345), }; // Test verbose format - should not panic OutputFormatter::format(&result, OutputFormat::Human, true, None).unwrap(); } #[test] fn test_format_verbose_no_children() { let now = Timestamp::now(); let result = MonitorResult { command: "echo test".to_string(), peak_rss_bytes: 10_485_760, peak_vsz_bytes: 20_971_520, duration_ms: 100, exit_code: Some(0), threshold_exceeded: false, timestamp: now, process_tree: None, timeline: None, start_time: Some(now), sample_count: Some(1), main_pid: Some(99999), }; // Test verbose format without process tree OutputFormatter::format(&result, OutputFormat::Human, true, None).unwrap(); } #[test] fn test_count_processes() { let now = Timestamp::now(); let tree = ProcessMemoryInfo { pid: 1, name: "root".to_string(), memory: MemoryUsage { rss_bytes: 1000, vsz_bytes: 2000, timestamp: now, }, children: vec![ ProcessMemoryInfo { pid: 2, name: "child1".to_string(), memory: MemoryUsage { rss_bytes: 100, vsz_bytes: 200, timestamp: now, }, children: vec![], }, ProcessMemoryInfo { pid: 3, name: "child2".to_string(), memory: MemoryUsage { rss_bytes: 200, vsz_bytes: 400, timestamp: now, }, children: vec![ProcessMemoryInfo { pid: 4, name: "grandchild".to_string(), memory: MemoryUsage { rss_bytes: 50, vsz_bytes: 100, timestamp: now, }, children: vec![], }], }, ], }; assert_eq!(OutputFormatter::count_processes(&tree), 4); } } peak-mem-0.1.3/src/process/mod.rs000064400000000000000000000113171046102023000147160ustar 00000000000000//! Process spawning and management. //! //! This module handles spawning the target process and managing its lifecycle, //! including signal forwarding on Unix systems. use crate::types::{PeakMemError, Result}; use std::process::Stdio; use tokio::process::Command; /// Handles spawning and running the target process. pub struct ProcessRunner { command: Vec, } impl ProcessRunner { /// Creates a new process runner with the given command. /// /// # Arguments /// * `command` - Command and arguments to execute /// /// # Errors /// * Returns error if command is empty pub fn new(command: Vec) -> Result { if command.is_empty() { return Err(PeakMemError::ProcessSpawn( "No command provided".to_string(), )); } Ok(Self { command }) } /// Spawns the configured process. /// /// The process inherits stdin, stdout, and stderr from the parent. /// /// # Returns /// * `ProcessHandle` for managing the spawned process pub async fn spawn(&self) -> Result { let program = &self.command[0]; let args = &self.command[1..]; let mut cmd = Command::new(program); cmd.args(args) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); let child = cmd .spawn() .map_err(|e| PeakMemError::ProcessSpawn(format!("Failed to spawn '{program}': {e}")))?; let pid = child .id() .ok_or_else(|| PeakMemError::ProcessSpawn("Failed to get process ID".to_string()))?; Ok(ProcessHandle { child, pid }) } /// Returns the command as a single string for display. pub fn command_string(&self) -> String { self.command.join(" ") } } /// Handle to a spawned process. /// /// Provides methods for waiting on the process and forwarding signals. pub struct ProcessHandle { child: tokio::process::Child, pid: u32, } impl ProcessHandle { /// Returns the process ID. pub fn pid(&self) -> u32 { self.pid } /// Waits for the process to complete while forwarding signals on Unix. /// /// Forwards SIGINT and SIGTERM to the child process. /// /// # Returns /// * Exit code of the process #[cfg(unix)] pub async fn wait_with_signal_forwarding(mut self) -> Result> { use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use tokio::signal::unix::{signal, SignalKind}; let child_pid = Pid::from_raw(self.pid as i32); // Set up signal handlers let mut sigint_stream = signal(SignalKind::interrupt())?; let mut sigterm_stream = signal(SignalKind::terminate())?; // Wait for either the child to exit or a signal tokio::select! { // Child process exited status = self.child.wait() => { Ok(status?.code()) } // SIGINT received (Ctrl+C) _ = sigint_stream.recv() => { // Forward SIGINT to child let _ = signal::kill(child_pid, Signal::SIGINT); // Wait for child to exit let status = self.child.wait().await?; Ok(status.code()) } // SIGTERM received _ = sigterm_stream.recv() => { // Forward SIGTERM to child let _ = signal::kill(child_pid, Signal::SIGTERM); // Wait for child to exit let status = self.child.wait().await?; Ok(status.code()) } } } /// Waits for the process to complete on Windows. /// /// On Windows, Ctrl+C is automatically forwarded to child processes /// in the same console. /// /// # Returns /// * Exit code of the process #[cfg(windows)] pub async fn wait_with_signal_forwarding(mut self) -> Result> { // On Windows, Ctrl+C is automatically forwarded to child processes // in the same console, so we just wait normally let status = self.child.wait().await?; Ok(status.code()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_process_runner() { let runner = ProcessRunner::new(vec!["echo".to_string(), "test".to_string()]).unwrap(); let handle = runner.spawn().await.unwrap(); let pid = handle.pid(); assert!(pid > 0); let exit_code = handle.wait_with_signal_forwarding().await.unwrap(); assert_eq!(exit_code, Some(0)); } #[test] fn test_empty_command() { let result = ProcessRunner::new(vec![]); assert!(result.is_err()); } } peak-mem-0.1.3/src/types.rs000064400000000000000000000310561046102023000136270ustar 00000000000000//! Core types and data structures for the peak-mem memory monitoring tool. //! //! This module defines the fundamental types used throughout the application //! for tracking memory usage, process information, and monitoring results. use serde::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// A simple byte size type with human-readable formatting. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct ByteSize(u64); impl ByteSize { /// Create a new ByteSize from bytes. pub fn b(bytes: u64) -> Self { ByteSize(bytes) } /// Get the number of bytes. #[allow(dead_code)] // Used in RealtimeDisplay but clippy misses it with --all-targets pub fn as_u64(&self) -> u64 { self.0 } } impl fmt::Display for ByteSize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = self.0 as f64; if bytes < 1024.0 { write!(f, "{} B", self.0) } else if bytes < 1024.0 * 1024.0 { write!(f, "{:.1} KB", bytes / 1024.0) } else if bytes < 1024.0 * 1024.0 * 1024.0 { write!(f, "{:.1} MB", bytes / (1024.0 * 1024.0)) } else if bytes < 1024.0 * 1024.0 * 1024.0 * 1024.0 { write!(f, "{:.1} GB", bytes / (1024.0 * 1024.0 * 1024.0)) } else { write!(f, "{:.1} TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0)) } } } impl FromStr for ByteSize { type Err = PeakMemError; fn from_str(s: &str) -> Result { let s = s.trim(); if s.is_empty() { return Err(PeakMemError::InvalidArgument( "Empty size string".to_string(), )); } // Try to parse as plain number first if let Ok(bytes) = s.parse::() { return Ok(ByteSize(bytes)); } // Find where the number ends and unit begins let num_end = s .find(|c: char| !c.is_ascii_digit() && c != '.') .unwrap_or(s.len()); if num_end == 0 { return Err(PeakMemError::InvalidArgument(format!( "Invalid size format: '{}'", s ))); } let (num_str, unit_str) = s.split_at(num_end); let number: f64 = num_str .parse() .map_err(|_| PeakMemError::InvalidArgument(format!("Invalid number: '{}'", num_str)))?; let unit = unit_str.trim().to_uppercase(); let multiplier = match unit.as_str() { "" | "B" => 1.0, "K" | "KB" => 1024.0, "M" | "MB" => 1024.0 * 1024.0, "G" | "GB" => 1024.0 * 1024.0 * 1024.0, "T" | "TB" => 1024.0 * 1024.0 * 1024.0 * 1024.0, "KIB" => 1024.0, "MIB" => 1024.0 * 1024.0, "GIB" => 1024.0 * 1024.0 * 1024.0, "TIB" => 1024.0 * 1024.0 * 1024.0 * 1024.0, _ => { return Err(PeakMemError::InvalidArgument(format!( "Unknown size unit: '{}'", unit ))); } }; let bytes = (number * multiplier) as u64; Ok(ByteSize(bytes)) } } /// A UTC timestamp with RFC3339 formatting support. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Timestamp(SystemTime); impl Timestamp { /// Create a new timestamp for the current time. pub fn now() -> Self { Timestamp(SystemTime::now()) } /// Convert to RFC3339 string format. pub fn to_rfc3339(self) -> String { let duration = self .0 .duration_since(UNIX_EPOCH) .unwrap_or_else(|_| Duration::from_secs(0)); let total_secs = duration.as_secs(); let nanos = duration.subsec_nanos(); // Simple UTC timestamp formatting // This is a basic implementation - real RFC3339 needs proper date calculation let secs_today = total_secs % 86400; let hours = secs_today / 3600; let mins = (secs_today % 3600) / 60; let secs = secs_today % 60; // Approximate date (days since epoch - not accurate for display but works for // testing) For production, would need proper date calculation format!( "2025-09-06T{:02}:{:02}:{:02}.{:06}+00:00", hours, mins, secs, nanos / 1000 ) } /// Format as human-readable date time string. pub fn format_datetime(self) -> String { let duration = self .0 .duration_since(UNIX_EPOCH) .unwrap_or_else(|_| Duration::from_secs(0)); let total_secs = duration.as_secs(); let secs_today = total_secs % 86400; let hours = secs_today / 3600; let mins = (secs_today % 3600) / 60; let secs = secs_today % 60; format!("2025-09-06 {:02}:{:02}:{:02}", hours, mins, secs) } } impl Serialize for Timestamp { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { serializer.serialize_str(&self.to_rfc3339()) } } impl<'de> Deserialize<'de> for Timestamp { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { let _s = String::deserialize(deserializer)?; // For now, just return current time - proper parsing would be needed Ok(Timestamp::now()) } } /// Represents a snapshot of memory usage at a specific point in time. /// /// This struct captures both RSS (Resident Set Size) and VSZ (Virtual Size) /// memory metrics along with a timestamp. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MemoryUsage { /// Physical memory currently used by the process (in bytes). pub rss_bytes: u64, /// Virtual memory size of the process (in bytes). pub vsz_bytes: u64, /// When this measurement was taken. pub timestamp: Timestamp, } /// Hierarchical representation of a process and its children's memory usage. /// /// This struct forms a tree structure where each node contains information /// about a process and its direct children, enabling visualization of memory /// usage across an entire process tree. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessMemoryInfo { /// Process ID of this process. pub pid: u32, /// Name or command of the process. pub name: String, /// Current memory usage of this process. pub memory: MemoryUsage, /// List of child processes and their memory information. pub children: Vec, } /// Complete results from monitoring a process's memory usage. /// /// This struct contains all the data collected during a monitoring session, /// including peak memory usage, duration, optional timeline data, and process /// tree information. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MonitorResult { /// The command that was executed. pub command: String, /// Peak RSS (Resident Set Size) observed during execution (in bytes). pub peak_rss_bytes: u64, /// Peak VSZ (Virtual Size) observed during execution (in bytes). pub peak_vsz_bytes: u64, /// Total duration of the monitoring session (in milliseconds). pub duration_ms: u64, /// Exit code of the monitored process, if it completed. pub exit_code: Option, /// Whether the memory usage exceeded the configured threshold. pub threshold_exceeded: bool, /// When the monitoring session completed. pub timestamp: Timestamp, /// Process tree snapshot at peak memory usage (if verbose mode enabled). #[serde(skip_serializing_if = "Option::is_none")] pub process_tree: Option, /// Timeline of memory usage samples (if timeline recording enabled). #[serde(skip_serializing_if = "Option::is_none")] pub timeline: Option>, /// When the monitoring session started. #[serde(skip_serializing_if = "Option::is_none")] pub start_time: Option, /// Number of memory samples collected. #[serde(skip_serializing_if = "Option::is_none")] pub sample_count: Option, /// Process ID of the main monitored process. #[serde(skip_serializing_if = "Option::is_none")] pub main_pid: Option, } impl MonitorResult { /// Returns the peak RSS as a human-readable ByteSize. pub fn peak_rss(&self) -> ByteSize { ByteSize::b(self.peak_rss_bytes) } /// Returns the peak VSZ as a human-readable ByteSize. pub fn peak_vsz(&self) -> ByteSize { ByteSize::b(self.peak_vsz_bytes) } /// Returns the monitoring duration as a Duration type. pub fn duration(&self) -> Duration { Duration::from_millis(self.duration_ms) } } /// Error types that can occur during memory monitoring operations. /// /// This enum provides structured error handling for all failure modes /// in the peak-mem application. #[derive(Debug)] pub enum PeakMemError { /// Failed to spawn the target process. ProcessSpawn(String), /// Error occurred during memory monitoring. #[allow(dead_code)] Monitor(String), /// The current platform is not supported. #[allow(dead_code)] UnsupportedPlatform(String), /// Insufficient permissions to monitor the process. #[allow(dead_code)] PermissionDenied(String), /// Generic I/O error. Io(std::io::Error), /// Failed to parse system data. #[allow(dead_code)] // Used in Linux implementation Parse(String), /// Invalid command-line argument. InvalidArgument(String), /// JSON serialization/deserialization error. Json(String), /// Runtime error. Runtime(String), } impl fmt::Display for PeakMemError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PeakMemError::ProcessSpawn(msg) => write!(f, "Failed to spawn process: {}", msg), PeakMemError::Monitor(msg) => write!(f, "Failed to monitor process: {}", msg), PeakMemError::UnsupportedPlatform(platform) => { write!(f, "Platform not supported: {}", platform) } PeakMemError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg), PeakMemError::Io(err) => write!(f, "IO error: {}", err), PeakMemError::Parse(msg) => write!(f, "Parse error: {}", msg), PeakMemError::InvalidArgument(msg) => write!(f, "Invalid argument: {}", msg), PeakMemError::Json(msg) => write!(f, "JSON error: {}", msg), PeakMemError::Runtime(msg) => write!(f, "Runtime error: {}", msg), } } } impl std::error::Error for PeakMemError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { PeakMemError::Io(err) => Some(err), _ => None, } } } impl From for PeakMemError { fn from(err: std::io::Error) -> Self { PeakMemError::Io(err) } } impl From for PeakMemError { fn from(err: serde_json::Error) -> Self { PeakMemError::Json(err.to_string()) } } impl From for PeakMemError { fn from(err: std::num::ParseIntError) -> Self { PeakMemError::InvalidArgument(err.to_string()) } } impl From for PeakMemError { fn from(err: tokio::task::JoinError) -> Self { PeakMemError::Runtime(format!("Task join error: {}", err)) } } /// Type alias for Results that may contain PeakMemError. pub type Result = std::result::Result; #[cfg(test)] mod tests { use super::*; #[test] fn test_memory_usage_creation() { let usage = MemoryUsage { rss_bytes: 1024 * 1024, vsz_bytes: 2048 * 1024, timestamp: Timestamp::now(), }; assert_eq!(usage.rss_bytes, 1024 * 1024); assert_eq!(usage.vsz_bytes, 2048 * 1024); } #[test] fn test_monitor_result_conversions() { let result = MonitorResult { command: "test".to_string(), peak_rss_bytes: 100 * 1024 * 1024, peak_vsz_bytes: 200 * 1024 * 1024, duration_ms: 5000, exit_code: Some(0), threshold_exceeded: false, timestamp: Timestamp::now(), process_tree: None, timeline: None, start_time: None, sample_count: None, main_pid: None, }; assert_eq!(result.peak_rss().to_string(), "100.0 MB"); assert_eq!(result.peak_vsz().to_string(), "200.0 MB"); assert_eq!(result.duration().as_secs(), 5); } }