diff --git a/Cargo.lock b/Cargo.lock index 9d10c2ef..18a42cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,21 +3,22 @@ version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.3.4", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -29,12 +30,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -46,9 +41,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -61,44 +56,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "aperf" @@ -137,9 +132,11 @@ dependencies = [ "perf-event2", "procfs", "regex", + "rmcp", "rustc-demangle", - "rustix 0.38.28", + "rustix 0.38.44", "ryu", + "schemars 0.8.22", "serde", "serde_json", "serde_urlencoded", @@ -160,30 +157,49 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.1.1" +version = "1.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11382bd8ac4c6c182a9775990935f96c916a865f1414486595f18eb8cfa9d90b" +checksum = "517aa062d8bd9015ee23d6daa5e1c1372328412fdae4e6c4c1be9b69c6ad37a2" dependencies = [ "aws-credential-types", - "aws-http", "aws-runtime", "aws-sdk-sso", "aws-sdk-ssooidc", @@ -193,25 +209,26 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", "fastrand", "hex", - "http", - "hyper", - "ring", + "http 1.4.0", + "sha1", "time", "tokio", "tracing", + "url", "zeroize", ] [[package]] name = "aws-credential-types" -version = "1.1.1" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a1629320d319dc715c6189b172349186557e209d2a7b893ff3d14efd33a47c" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -220,117 +237,130 @@ dependencies = [ ] [[package]] -name = "aws-http" -version = "0.60.1" +name = "aws-lc-rs" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e4199d5d62ab09be6a64650c06cc5c4aa45806fed4c74bc4a5c8eaf039a6fa" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http", - "http-body", - "pin-project-lite", - "tracing", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", ] [[package]] name = "aws-runtime" -version = "1.1.1" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87116d357c905b53f1828d15366363fd27b330a0393cbef349e653f686d36bad" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" dependencies = [ "aws-credential-types", - "aws-http", "aws-sigv4", "aws-smithy-async", "aws-smithy-http", + "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", + "bytes", + "bytes-utils", "fastrand", - "http", + "http 1.4.0", + "http-body 1.0.1", "percent-encoding", + "pin-project-lite", "tracing", "uuid", ] [[package]] name = "aws-sdk-sso" -version = "1.9.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9d9a8ac4cdb8df39f9777fd41e15a9ae0d0b622b00909ae0322b4d2f9e6ac8" +checksum = "9f4055e6099b2ec264abdc0d9bbfffce306c1601809275c861594779a0b04b45" dependencies = [ "aws-credential-types", - "aws-http", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "http", - "once_cell", + "fastrand", + "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.9.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ba4a42aa91acecd5ca43b330b5c8eb7f8808d720b6a6f796a35faa302fc73d" +checksum = "02f009ba0284c5d696425fd7b4dcc5b189f5726f4041b7a5794daecb3a68d598" dependencies = [ "aws-credential-types", - "aws-http", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", "bytes", - "http", - "once_cell", + "fastrand", + "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.9.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3c7c3dcec7cccd24a13953eedf0f2964c2d728d22112744274cf0098ad2e35" +checksum = "6aa6622798e19e6a76b690562085dd4771c736cd48343464a53ab4ae2f2c9f84" dependencies = [ "aws-credential-types", - "aws-http", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-smithy-xml", "aws-types", - "http", - "once_cell", + "fastrand", + "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.1.1" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d222297ca90209dc62245f0a490355795f29de362eb5c19caea4f7f55fe69078" +checksum = "b7083fb918b38474ac65ffbf8a69fc8792d36879f4ac5f1667b43aec61efe9a5" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -340,8 +370,8 @@ dependencies = [ "form_urlencoded", "hex", "hmac", - "http", - "once_cell", + "http 0.2.12", + "http 1.4.0", "percent-encoding", "sha2", "time", @@ -350,9 +380,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.1.1" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9f65000917e3aa94c259d67fe01fa9e4cd456187d026067d642436e6311a81" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -361,38 +391,74 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.1" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e816425a6b9caea4929ac97d0cb33674849bd5f0086418abc0d02c63f7a1bf" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "bytes-utils", "futures-core", - "http", - "http-body", - "once_cell", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", "tracing", ] +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2", + "http 1.4.0", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower", + "tracing", +] + [[package]] name = "aws-smithy-json" -version = "0.60.7" +version = "0.62.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +checksum = "517089205f18ab4adc5a3e02888cb139bbbbb2e168eac9f396216925d1fbeaf5" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", ] +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + [[package]] name = "aws-smithy-query" -version = "0.60.7" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -400,56 +466,84 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.1.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da5b0a3617390e769576321816112f711c13d7e1114685e022505cf51fe5e48" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", - "once_cell", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", - "rustls", "tokio", "tracing", ] [[package]] name = "aws-smithy-runtime-api" -version = "1.1.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2404c9eb08bfe9af255945254d9afc69a367b7ee008b8db75c05e3bca485fc65" +checksum = "dc117c179ecf39a62a0a3f49f600e9ac26a7ad7dd172177999f83933af776c32" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", - "http", + "http 0.2.12", + "http 1.4.0", "pin-project-lite", "tokio", "tracing", "zeroize", ] +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + [[package]] name = "aws-smithy-types" -version = "1.2.13" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042" +checksum = "056b66dbce2f81cc0c1e2b05bb402eb58f8a3530479d650efadd5bbae9a4050b" dependencies = [ "base64-simd", "bytes", "bytes-utils", - "http", - "http-body", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", "itoa", "num-integer", "pin-project-lite", @@ -461,33 +555,33 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.1.1" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5d5ee29077e0fcd5ddd0c227b521a33aaf02434b7cdba1c55eec5c1f18ac47" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", - "http", "rustc_version", "tracing", ] [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64-simd" @@ -516,9 +610,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -529,29 +623,38 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "build_html" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b01f54cbdd56298a506b086691594ded3b68dcbc9437adc87c616a35e7fc89" +checksum = "f1829197343f1874c1979c988a5e75c2539c4645792d87494286ce2cb6dc890d" [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -561,9 +664,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytes-utils" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ "bytes", "either", @@ -571,15 +674,21 @@ dependencies = [ [[package]] name = "c-enum" -version = "0.1.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b71931af9c2a7756e9eed1b6ac105c31588a44b496423b41412e506f0f47e5" +checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8" [[package]] name = "cc" -version = "1.0.92" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] [[package]] name = "cfb" @@ -594,9 +703,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -606,24 +715,23 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-link", ] [[package]] name = "clap" -version = "4.5.45" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -631,9 +739,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -643,52 +751,63 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.65" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "termcolor", - "unicode-width", + "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -696,9 +815,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpp_demangle" @@ -711,9 +830,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] @@ -735,57 +863,46 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "once_cell", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" -dependencies = [ - "cfg-if", - "once_cell", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -814,31 +931,40 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -856,66 +982,66 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] -name = "cxx" -version = "1.0.94" +name = "ctutils" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", + "cmov", ] [[package]] -name = "cxx-build" -version = "1.0.94" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.115", + "darling_core", + "darling_macro", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.94" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] [[package]] -name = "cxxbridge-macro" -version = "1.0.94" +name = "darling_macro" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "proc-macro2", + "darling_core", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] name = "dashmap" -version = "5.5.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", - "hashbrown", + "crossbeam-utils", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -923,22 +1049,33 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] [[package]] -name = "derivative" -version = "2.2.0" +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "proc-macro2", "quote", - "syn 1.0.99", + "rustc_version", + "syn 2.0.117", + "unicode-xid", ] [[package]] @@ -953,71 +1090,125 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.8.0" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "env_filter" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", +] [[package]] name = "env_logger" -version = "0.10.0" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ + "env_filter", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.1.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.48.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1029,20 +1220,32 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1055,9 +1258,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1065,15 +1268,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1082,36 +1285,47 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1145,9 +1359,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1155,8 +1369,33 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.33.0" +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ @@ -1165,16 +1404,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -1184,15 +1423,30 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -1202,9 +1456,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1214,11 +1468,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", ] [[package]] @@ -1232,97 +1486,268 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] [[package]] -name = "httparse" -version = "1.8.0" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] [[package]] -name = "httpdate" -version = "1.0.2" +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hybrid-array" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] [[package]] name = "hyper" -version = "0.14.28" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", "h2", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.7", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "futures-util", - "http", + "http 1.4.0", "hyper", - "log", + "hyper-util", "rustls", "rustls-native-certs", "tokio", "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "cxx", - "cxx-build", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1349,12 +1774,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1368,9 +1795,9 @@ dependencies = [ [[package]] name = "inferno" -version = "0.11.19" +version = "0.11.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9" +checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" dependencies = [ "ahash", "clap", @@ -1395,7 +1822,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.11.1", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -1407,33 +1834,27 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.10" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", - "io-lifetimes", - "rustix 0.37.18", - "windows-sys 0.48.0", + "libc", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1446,30 +1867,49 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", + "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linear-map" @@ -1477,15 +1917,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - [[package]] name = "linux-perf-data" version = "0.12.0" @@ -1507,7 +1938,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa8fc7e83909ea3b9e2784591655637d3401f2f16014f9d8d6e23ccd138e665f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.11.1", "byteorder", "memchr", "thiserror 2.0.18", @@ -1515,23 +1946,28 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.6" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1552,29 +1988,30 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "log4rs" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c" dependencies = [ "anyhow", "arc-swap", "chrono", - "derivative", + "derive_more", "fnv", "humantime", "libc", "log", "log-mdc", - "once_cell", + "mock_instant", "parking_lot", "rand", "serde", "serde-value", "serde_json", "serde_yaml", - "thiserror 1.0.69", + "thiserror 2.0.18", "thread-id", "typemap-ors", + "unicode-segmentation", "winapi", ] @@ -1596,18 +2033,18 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.0" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.6.5" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1620,11 +2057,12 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", + "simd-adler32", ] [[package]] @@ -1641,15 +2079,21 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "mock_instant" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" + [[package]] name = "new_mime_guess" version = "4.0.4" @@ -1675,7 +2119,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -1683,18 +2127,18 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.7" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-format" @@ -1708,38 +2152,27 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -1763,21 +2196,21 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "ordered-float" @@ -1790,15 +2223,15 @@ dependencies = [ [[package]] name = "outref" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1806,49 +2239,57 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", - "windows-targets 0.48.0", + "windows-link", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf-event-data" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963b5dfa83fa6c28af556b19fc258be88823de8af982469747c6594f48a8b9ba" +checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.11.1", + "c-enum", "perf-event-open-sys2", ] [[package]] name = "perf-event-open-sys2" -version = "5.0.3" +version = "5.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83659418a76adf99b97b487d6e4e73ea33e5d5dffb006fdd4aec8033995aaad3" +checksum = "d9c25955321465255e437600b54296983fab1feac2cd0c38958adeb26dbae49e" dependencies = [ "libc", + "memoffset", ] [[package]] name = "perf-event2" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e25284c4a86df3bccd1818dee1fe9390424fce73a58fe2f19957f2cc5de8b28" +checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.11.1", "c-enum", "libc", "memmap2", @@ -1858,9 +2299,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1868,6 +2309,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1876,13 +2326,23 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1926,7 +2386,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1940,29 +2400,40 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -1970,62 +2441,60 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] name = "rayon" -version = "1.5.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", ] [[package]] -name = "redox_syscall" -version = "0.3.5" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ - "bitflags 1.3.2", + "ref-cast-impl", ] [[package]] -name = "redox_syscall" -version = "0.4.1" +name = "ref-cast-impl" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ - "bitflags 1.3.2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2053,9 +2522,9 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" @@ -2065,28 +2534,62 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rgb" -version = "0.8.36" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ "bytemuck", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rmcp" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" +dependencies = [ + "async-trait", + "base64", + "chrono", + "futures", + "pastey", + "pin-project-lite", + "rmcp-macros", + "schemars 1.2.1", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "rmcp-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -2095,88 +2598,91 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.37.18" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "errno", - "io-lifetimes", "libc", - "linux-raw-sys 0.3.6", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "0.38.28" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.4.12", - "windows-sys 0.52.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "log", - "ring", + "aws-lc-rs", + "once_cell", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "base64", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", + "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ruzstd" @@ -2189,9 +2695,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2204,58 +2710,91 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.61.2", ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "schemars" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] [[package]] -name = "scratch" -version = "1.0.7" +name = "schemars" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive 1.2.1", + "serde", + "serde_json", +] [[package]] -name = "sct" -version = "0.7.1" +name = "schemars_derive" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ - "ring", - "untrusted", + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sdd" -version = "1.7.0" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f05a494052771fc5bd0619742363b5e24e5ad72ab3111ec2e27925b8edc5f3" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" [[package]] name = "security-framework" -version = "2.7.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -2264,9 +2803,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2274,9 +2813,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2315,18 +2854,31 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2343,9 +2895,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.30" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", @@ -2356,11 +2908,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.1.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -2370,31 +2923,48 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", ] +[[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.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -2402,9 +2972,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.8.11", @@ -2413,54 +2983,42 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] -name = "slab" -version = "0.4.7" +name = "simd-adler32" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" -dependencies = [ - "autocfg", -] +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "smallvec" -version = "1.9.0" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "socket2" -version = "0.4.7" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" -dependencies = [ - "libc", - "winapi", -] +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2475,9 +3033,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "str_stack" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" +checksum = "7f446288b699d66d0fd2e30d1cfe7869194312524b3b9252594868ed26ef056a" [[package]] name = "strsim" @@ -2497,24 +3055,24 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 1.0.99", + "syn 1.0.109", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2523,33 +3081,45 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.115" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sysctl" -version = "0.5.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99d037b2bef227ab8963f4b0acc33ecbb1f9a2e7365add7789372b387ec19e1" +checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "byteorder", + "enum-as-inner", "libc", - "thiserror 1.0.69", + "thiserror 2.0.18", "walkdir", ] [[package]] name = "sysinfo" -version = "0.26.2" +version = "0.26.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae2421f3e16b3afd4aa692d23b83d0ba42ee9b0081d5deeb7d21428d7195fb1" +checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2562,9 +3132,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.40" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -2582,24 +3152,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", - "redox_syscall 0.4.1", - "rustix 0.38.28", - "windows-sys 0.52.0", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -2628,7 +3189,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -2639,34 +3200,33 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] name = "thread-id" -version = "4.2.2" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +checksum = "2010d27add3f3240c1fef7959f46c814487b216baee662af53be645ba7831c07" dependencies = [ "libc", - "winapi", + "windows-sys 0.61.2", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -2674,22 +3234,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2701,42 +3261,52 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e482e368cf7efa2c8b570f476e5b9fd9fd5e9b9219fc567832b05f13511091" dependencies = [ - "rustix 0.38.28", + "rustix 0.38.44", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio 1.1.1", + "mio 1.2.0", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2744,29 +3314,44 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2775,29 +3360,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" @@ -2820,33 +3405,39 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unsafe-any-ors" @@ -2869,29 +3460,51 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "urlencoding" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.3.2" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "vergen" -version = "8.3.1" +version = "8.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" dependencies = [ "anyhow", "cfg-if", @@ -2901,9 +3514,9 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vsimd" @@ -2913,61 +3526,65 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi", "winapi-util", ] [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.83" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen 0.57.1", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.83" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn 1.0.99", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2975,22 +3592,59 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 1.0.99", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] name = "winapi" @@ -3010,11 +3664,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -3025,18 +3679,38 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.50.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6041b3f84485c21b57acdc0fee4f4f0c93f426053dc05fa5d6fc262537bbff" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.48.0", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-link" -version = "0.1.3" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "windows-link" @@ -3045,16 +3719,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-link", ] [[package]] @@ -3063,7 +3742,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -3072,16 +3751,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.52.6", ] [[package]] @@ -3090,225 +3769,238 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "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.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "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", + "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_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[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.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[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.36.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.53.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" +name = "windows_i686_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" -version = "0.48.0" +name = "windows_x86_64_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "windows_i686_msvc" -version = "0.52.0" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_msvc" -version = "0.53.0" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xattr" -version = "1.0.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", + "rustix 1.1.4", ] [[package]] @@ -3317,29 +4009,111 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", + "synstructure", ] [[package]] name = "zeroize" -version = "1.5.7" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 97869069..f0e499e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,8 +62,10 @@ log4rs = "1.3.0" tdigest = "0.2" csv = "1.2" csv-to-html = "0.5.10" -numeric-sort = "0.1.5" -regex = "1.12.3" +numeric-sort = "0.1" +regex = "1" +rmcp = { version = "1.5", features = ["server", "transport-io"], optional = true } +schemars = { version = "0.8", optional = true } [target.'cfg(target_os = "linux")'.dependencies] sysinfo = "0.26.2" @@ -80,5 +82,6 @@ gimli = { version = "0.33", default-features = false, features = ["read-core", " lzma-rs = "0.3" [features] -default = [] +default = ["mcp-server"] hotline = [] +mcp-server = ["dep:rmcp", "dep:schemars"] diff --git a/README.md b/README.md index f684c500..838eb455 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,31 @@ Shell to generate completions for [possible values: bash, elvish, fish, powershe Install the auto complete script using sudo, or specify a download path +## MCP Server (AI Assistant Integration) + +APerf includes a built-in [MCP](https://modelcontextprotocol.io/) server that lets AI assistants (Kiro, Claude Desktop, etc.) record data, generate reports, and analyze performance metrics interactively. + +```bash +# Start the MCP server (used by AI clients, not run manually) +aperf server --mcp +``` + +**Kiro setup** — add to `~/.kiro/settings/mcp.json`: + +```json +{ + "mcpServers": { + "aperf-mcp": { + "command": "/path/to/aperf", + "args": ["server", "--mcp"], + "disabled": false + } + } +} +``` + +The server exposes 8 tools: `load_report`, `get_metrics`, `get_metric_values`, `get_analytical_findings`, `get_statistical_findings`, `get_flamegraph`, `record`, and `generate_report`. See [MCP Server docs](./docs/MCP-SERVER.md) for full details. + ## APerf Issues? > [!WARNING] @@ -329,6 +354,7 @@ sudo sysctl -w kernel.kptr_restrict=0 - [Dependencies Reference](./docs/DEPENDENCIES.md#aperf-dependencies) - [Development Guide](./docs/DEVELOPMENT.md) - [Example Usage](./docs/EXAMPLE.md#aperf-example) +- [MCP Server (AI Assistant Integration)](./docs/MCP-SERVER.md) - [Running on EKS](./docs/README-EKS.md#running-aperf-on-ekskubernetes) ## Security diff --git a/docs/MCP-SERVER.md b/docs/MCP-SERVER.md new file mode 100644 index 00000000..fe889a57 --- /dev/null +++ b/docs/MCP-SERVER.md @@ -0,0 +1,197 @@ +# APerf MCP Server + +APerf includes a built-in [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that lets AI assistants record performance data, generate reports, and analyze APerf reports interactively. + +## Usage + +```bash +aperf server --mcp +``` + +This starts the MCP server on stdio. It's designed to be launched by an MCP client (Kiro, Claude Desktop, etc.), not run manually. + +## MCP Client Configuration + +### Kiro (`~/.kiro/settings/mcp.json`) + +```json +{ + "mcpServers": { + "aperf-mcp": { + "command": "/path/to/aperf", + "args": ["server", "--mcp"], + "disabled": false + } + } +} +``` + +Replace `/path/to/aperf` with the actual binary path. + +## Available Tools + +### Read-only tools (report analysis) + +#### `load_report` + +Load an APerf report and return metadata + available metrics. Must be called first before other analysis tools. + +| Parameter | Type | Description | +|---|---|---| +| `report_path` | string | Absolute or relative path to the APerf report directory | + +#### `get_metrics` + +Query available metrics from the loaded report. Supports filtering. + +| Parameter | Type | Description | +|---|---|---| +| `file_name` | string (optional) | JS filename, e.g. `"cpu_utilization.js"` | +| `category` | string (optional) | Data category, e.g. `"meminfo"` | + +#### `get_metric_values` + +Retrieve actual data values for a specific metric. + +| Parameter | Type | Description | +|---|---|---| +| `metric_name` | string | Name of the metric (e.g. `"user"`, `"system"`) | +| `file_name` | string (optional) | JS filename containing the metric | +| `category` | string (optional) | Category/data_name (e.g. `"cpu_utilization"`) | +| `output_type` | string (optional) | Output format (default: `summary`, see below) | +| `cpu_ids` | string[] (optional) | Filter to specific CPUs. If omitted, averages across all. | +| `run_id` | string (optional) | Specific run ID (default: all runs) | +| `from_time` | int (optional) | Start of time range in seconds. Negative = relative to end (e.g. -60 = last 60s) | +| `to_time` | int (optional) | End of time range in seconds. Negative = relative to end (e.g. -10 = stop 10s before end) | + +**Output types:** + +| Type | Description | Token cost | +|------|-------------|------------| +| `summary` | Smart text with all stats (avg/min/max/std/p50/p90/p99), trend direction, spike and drop detection | Minimal (default) | +| `stats` | Aggregated statistics only (avg, std, min, max, p50, p90, p99, p99.9) | Minimal | +| `timeseries` | Full raw time-series data with timestamps and values | High | +| `compact` | Delta-encoded notation with run-length encoding | ~80% less than timeseries | +| `downsampled` | Fixed 50 buckets with min/avg/max per bucket | Bounded | + +#### `get_analytical_findings` + +Get analytical findings from the report's analytical engine. Findings represent rule-based detections (regressions, improvements, configuration mismatches). Sorted by absolute score (severity) descending. + +| Parameter | Type | Description | +|---|---|---| +| `offset` | int (optional) | Start index for pagination, default 0 | +| `limit` | int (optional) | Max results per page, default 50 | + +#### `get_statistical_findings` + +Get statistical findings (metric stat deltas between runs). Computes the percentage change of each time-series metric's statistics compared to the base run (first run). Sorted by absolute delta descending. Requires a multi-run report. + +| Parameter | Type | Description | +|---|---|---| +| `offset` | int (optional) | Start index for pagination, default 0 | +| `limit` | int (optional) | Max results per page, default 50 | +| `stat` | string (optional) | Filter by stat type: `avg`, `std`, `min`, `max`, `p50`, `p90`, `p99`, `p99_9` | +| `data_type` | string (optional) | Filter by data category (e.g. `cpu_utilization`, `meminfo`) | +| `min_delta_pct` | float (optional) | Minimum absolute delta percentage to include (e.g. 5.0) | + +#### `get_flamegraph` + +Query flamegraph data from the loaded report. Returns top functions by default (sorted by percentage), with optional regex filtering to search for specific functions. Supports normal, reverse, diff, and reverse-diff modes. Returns data for all runs by default, or specific runs if `run_id` is provided. In diff/reverse-diff mode, `run_id` must contain exactly 2 run IDs. + +| Parameter | Type | Description | +|---|---|---| +| `flamegraph_type` | string (optional) | `normal` (default), `reverse`, `diff`, or `reverse-diff` | +| `run_id` | string[] (optional) | Run ID(s). Normal/reverse: omit for all runs, or list specific ones. Diff: exactly 2 IDs `[base, comparison]`, or omit for first two. | +| `limit` | int (optional) | Max functions to return per run, default 30 | +| `min_pct` | float (optional) | Minimum percentage threshold (default 0.1). In diff mode, min absolute delta % (default 0.5). | +| `filter` | string (optional) | Regex filter for function names (case-insensitive). Example: `"compact\|migrate"` | + +**Examples:** + +``` +# Top 10 hottest functions across all runs +get_flamegraph(limit=10) + +# Search for compaction-related functions +get_flamegraph(filter="compact|kcompactd|migrate") + +# Reverse flamegraph for a specific run, filtered +get_flamegraph(flamegraph_type="reverse", run_id=["run1"], filter="lock") + +# Diff between two runs — what got hotter/cooler +get_flamegraph(flamegraph_type="diff") + +# Diff with explicit run IDs, filtered +get_flamegraph(flamegraph_type="diff", run_id=["run1", "run2"], filter="compact|writeback") + +# Reverse diff +get_flamegraph(flamegraph_type="reverse-diff", run_id=["run1", "run2"]) +``` + +### Write tools (data collection and report generation) + +#### `record` + +Record performance data on the current system. Runs `aperf record` as a subprocess. + +| Parameter | Type | Description | +|---|---|---| +| `run_name` | string (optional) | Name of the run (default: `aperf_`) | +| `interval` | int (optional) | Collection interval in seconds (default: 1) | +| `period` | int (optional) | Recording duration in seconds (default: 10) | +| `profile` | bool (optional) | Enable CPU profiling using perf (default: false) | +| `perf_frequency` | int (optional) | Perf profiling frequency in Hz (default: 99) | +| `memory_allocation` | bool (optional) | Collect memory allocation data (default: false) | +| `dont_collect` | string (optional) | Comma-separated data types to skip | +| `collect_only` | string (optional) | Comma-separated data types to collect exclusively | +| `profile_java` | string (optional) | Profile JVMs — empty for all, or comma-separated PIDs/names | +| `pmu_config` | string (optional) | Path to custom PMU config file | + +> Requires Linux and appropriate kernel permissions. See the main README for details. + +#### `generate_report` + +Generate an HTML report from one or more recorded APerf runs. For multi-run comparison, provide multiple run paths — the first run is used as the base for statistical findings. + +| Parameter | Type | Description | +|---|---|---| +| `runs` | string[] | Paths to run directories or archives (at least one required) | +| `name` | string (optional) | Report name (default: `aperf_report_`) | + +## Building + +The MCP server is included in the default `aperf` build: + +```bash +cargo build --release +# Binary at target/release/aperf +# Usage: aperf server --mcp +``` + +## Architecture + +The MCP server code lives in `src/server/mcp/`: + +``` +src/server/ +├── mod.rs ← Server subcommand dispatch +└── mcp/ + ├── mod.rs ← MCP entry point (tokio runtime + stdio transport) + ├── tools.rs ← Tool definitions (#[tool_router] / #[tool_handler]) + ├── report.rs ← Report loading, validation, metric extraction + ├── metadata.rs ← systeminfo.js parsing + └── js_parser.rs ← JS variable prefix stripping + JSON extraction +``` + +MCP dependencies (`rmcp`, `schemars`) are feature-gated behind the `mcp` feature (enabled by default). + +## Running Tests + +```bash +# All MCP server tests +cargo test --lib server + +# Specific test +cargo test --lib server::mcp::tools::tests::test_build_record_args_full +``` diff --git a/src/bin/aperf.rs b/src/bin/aperf.rs index 032b3849..88dab0a3 100644 --- a/src/bin/aperf.rs +++ b/src/bin/aperf.rs @@ -1,6 +1,8 @@ use anyhow::Result; use aperf::completions::{setup_shell_completions, SetupShellCompletions}; use aperf::report::{report, Report}; +#[cfg(feature = "mcp-server")] +use aperf::server::Server; use aperf::{PDError, APERF_RUNLOG, APERF_TMP}; use clap::{CommandFactory, Parser, Subcommand}; use log::LevelFilter; @@ -54,6 +56,10 @@ enum Commands { /// Setup shell completions for APerf commands. SetupShellCompletions(SetupShellCompletions), + + /// Start a server (e.g. MCP for AI assistant integration). + #[cfg(feature = "mcp-server")] + Server(Server), } fn init_logger(verbose: u8, runlog: &PathBuf) -> Result<()> { @@ -101,6 +107,12 @@ fn init_logger(verbose: u8, runlog: &PathBuf) -> Result<()> { fn main() -> Result<()> { let cli = Cli::parse(); + // Server subcommand doesn't need temp dirs or log4rs — handle it early. + #[cfg(feature = "mcp-server")] + if let Commands::Server(ref s) = cli.command { + return aperf::server::run_server(s); + } + let tmp_dir = TempBuilder::new() .prefix("aperf-tmp-") .tempdir_in(&cli.tmp_dir)?; @@ -123,6 +135,9 @@ fn main() -> Result<()> { Commands::CustomPMU(r) => custom_pmu(&r), Commands::SetupShellCompletions(r) => setup_shell_completions(&r, &mut Cli::command()), + + #[cfg(feature = "mcp-server")] + Commands::Server(_) => unreachable!(), // handled above }?; fs::remove_dir_all(tmp_dir_path_buf)?; Ok(()) diff --git a/src/computations/mod.rs b/src/computations/mod.rs index 9cf3807a..50f6e4bb 100644 --- a/src/computations/mod.rs +++ b/src/computations/mod.rs @@ -3,6 +3,7 @@ use strum_macros::Display; /// Different statistics of the values contained in a Series #[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(default)] pub struct Statistics { #[serde(serialize_with = "serialize_f64_fixed2")] pub avg: f64, diff --git a/src/data/common/data_formats.rs b/src/data/common/data_formats.rs index 47ef70b5..99068ad7 100644 --- a/src/data/common/data_formats.rs +++ b/src/data/common/data_formats.rs @@ -69,6 +69,7 @@ pub struct TimeSeriesData { /// A map from the metric name to the metric's contents. pub metrics: HashMap, /// A list of all metric names to provide ordering for the graphs in the frontend. + #[serde(default)] pub sorted_metric_names: Vec, } @@ -114,6 +115,7 @@ pub struct Series { #[serde(serialize_with = "serialize_f64_vec_fixed2")] pub values: Vec, /// Indicate whether the series is aggregate. + #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub is_aggregate: bool, } diff --git a/src/lib.rs b/src/lib.rs index 53695832..a68a3550 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,8 @@ pub mod profiling; #[cfg(target_os = "linux")] pub mod record; pub mod report; +#[cfg(feature = "mcp-server")] +pub mod server; pub mod visualizer; use crate::analytics::{AnalyticalEngine, DataFindings}; diff --git a/src/server/mcp/js_parser.rs b/src/server/mcp/js_parser.rs new file mode 100644 index 00000000..4fe36452 --- /dev/null +++ b/src/server/mcp/js_parser.rs @@ -0,0 +1,122 @@ +//! Parse APerf JS report files. +//! +//! APerf report files are JavaScript files containing a single variable assignment: +//! `processed_foo_data = { ... JSON ... }` +//! or findings: +//! `foo_findings = { ... JSON ... }` +//! +//! This module strips the variable prefix and parses the JSON payload. + +use regex::Regex; + +/// Strip the JS variable assignment prefix and return the raw JSON string. +/// +/// Handles patterns like: +/// `processed_cpu_utilization_data = {...};` +/// `cpu_utilization_findings = {...}` +/// `runs_raw = [...]` +pub fn extract_json_from_js(content: &str) -> Option { + for line in content.lines() { + let trimmed = line.trim(); + if let Some(idx) = trimmed.find('=') { + let after_eq = trimmed[idx + 1..].trim(); + let json_str = after_eq.trim_end_matches(';').trim(); + if json_str.starts_with('{') || json_str.starts_with('[') { + return Some(json_str.to_string()); + } + } + } + None +} + +/// Extract all variable assignments from a JS file. +/// Returns Vec<(variable_name, json_string)>. +#[allow(dead_code)] +pub fn extract_all_variables(content: &str) -> Vec<(String, String)> { + let mut results = Vec::new(); + for line in content.lines() { + let trimmed = line.trim(); + if let Some(idx) = trimmed.find('=') { + let var_name = trimmed[..idx].trim().to_string(); + let after_eq = trimmed[idx + 1..].trim(); + let json_str = after_eq.trim_end_matches(';').trim(); + if json_str.starts_with('{') || json_str.starts_with('[') { + results.push((var_name, json_str.to_string())); + } + } + } + results +} + +/// Find all `*_findings` variables in a JS file content. +/// Returns Vec<(category_name, json_string)> where category_name has `_findings` stripped. +pub fn extract_findings_variables(content: &str) -> Vec<(String, String)> { + let re = Regex::new(r"(\w+_findings)\s*=\s*").unwrap(); + let mut results = Vec::new(); + + for line in content.lines() { + let trimmed = line.trim(); + if let Some(caps) = re.captures(trimmed) { + let var_name = caps.get(1).unwrap().as_str(); + let category = var_name.trim_end_matches("_findings").to_string(); + if let Some(idx) = trimmed.find('=') { + let after_eq = trimmed[idx + 1..].trim(); + let json_str = after_eq.trim_end_matches(';').trim(); + if json_str.starts_with('{') { + results.push((category, json_str.to_string())); + } + } + } + } + results +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_json_processed_data() { + let content = r#"processed_cpu_utilization_data = {"data_name":"cpu_utilization","data_format":"time_series"}"#; + let json = extract_json_from_js(content).unwrap(); + assert!(json.starts_with('{')); + assert!(json.contains("cpu_utilization")); + } + + #[test] + fn test_extract_json_with_semicolon() { + let content = r#"processed_foo_data = {"key":"value"};"#; + let json = extract_json_from_js(content).unwrap(); + assert_eq!(json, r#"{"key":"value"}"#); + } + + #[test] + fn test_extract_json_array() { + let content = r#"runs_raw = ["run1","run2"]"#; + let json = extract_json_from_js(content).unwrap(); + assert_eq!(json, r#"["run1","run2"]"#); + } + + #[test] + fn test_extract_all_variables() { + let content = "processed_foo_data = {\"a\":1}\nfoo_findings = {\"b\":2}\n"; + let vars = extract_all_variables(content); + assert_eq!(vars.len(), 2); + assert_eq!(vars[0].0, "processed_foo_data"); + assert_eq!(vars[1].0, "foo_findings"); + } + + #[test] + fn test_extract_findings_variables() { + let content = "cpu_utilization_findings = {\"per_run_findings\":{}}\n"; + let findings = extract_findings_variables(content); + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].0, "cpu_utilization"); + } + + #[test] + fn test_no_json_returns_none() { + let content = "// just a comment\n"; + assert!(extract_json_from_js(content).is_none()); + } +} diff --git a/src/server/mcp/metadata.rs b/src/server/mcp/metadata.rs new file mode 100644 index 00000000..0619b2d2 --- /dev/null +++ b/src/server/mcp/metadata.rs @@ -0,0 +1,198 @@ +//! Extract metadata from APerf report systeminfo.js files. + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::path::Path; + +use super::js_parser; + +/// Metadata for a single run within a report. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RunMetadata { + pub run_id: String, + pub alias: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub instance_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cpu_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub os: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub kernel: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub region: Option, +} + +/// Top-level report metadata extracted from systeminfo.js. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ReportMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub run_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub run_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub run_aliases: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub runs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub date_readable: Option, +} + +/// Extract metadata from a systeminfo.js file. +pub fn extract_metadata(systeminfo_path: &Path, report_path: &str) -> Result { + let content = std::fs::read_to_string(systeminfo_path) + .with_context(|| format!("Failed to read {}", systeminfo_path.display()))?; + + let json_str = js_parser::extract_json_from_js(&content) + .with_context(|| "No JSON found in systeminfo.js")?; + + let data: Value = + serde_json::from_str(&json_str).with_context(|| "Failed to parse systeminfo.js JSON")?; + + let mut metadata = ReportMetadata::default(); + + if let Some(runs_obj) = data.get("runs").and_then(|v| v.as_object()) { + let run_ids: Vec = runs_obj.keys().cloned().collect(); + metadata.run_count = Some(run_ids.len()); + + let mut run_aliases = serde_json::Map::new(); + let mut runs_meta = Vec::new(); + + for (idx, run_id) in run_ids.iter().enumerate() { + let alias = format!("run{}", idx + 1); + run_aliases.insert(alias.clone(), Value::String(run_id.clone())); + + let mut run_meta = RunMetadata { + run_id: run_id.clone(), + alias, + instance_type: None, + cpu_count: None, + os: None, + kernel: None, + region: None, + }; + + if let Some(run_data) = runs_obj.get(run_id) { + extract_run_fields(run_data, &mut run_meta); + } + + runs_meta.push(run_meta); + } + + metadata.run_ids = Some(run_ids); + metadata.run_aliases = Some(run_aliases); + metadata.runs = Some(runs_meta); + } + + // Extract timestamp from report path (pattern: YYYYMMDDTHHMMSS) + extract_timestamp(report_path, &mut metadata); + + Ok(metadata) +} + +fn extract_run_fields(run_data: &Value, run_meta: &mut RunMetadata) { + let key_values = run_data + .pointer("/key_value_groups//key_values") + .and_then(|v| v.as_object()); + + if let Some(kv) = key_values { + run_meta.instance_type = kv + .get("Instance Type") + .and_then(|v| v.as_str()) + .map(String::from); + run_meta.cpu_count = kv.get("CPUs").and_then(|v| v.as_str()).map(String::from); + run_meta.kernel = kv + .get("Kernel Version") + .and_then(|v| v.as_str()) + .map(String::from); + run_meta.region = kv.get("Region").and_then(|v| v.as_str()).map(String::from); + + let sys_name = kv.get("System Name").and_then(|v| v.as_str()); + let os_ver = kv.get("OS Version").and_then(|v| v.as_str()); + if let (Some(name), Some(ver)) = (sys_name, os_ver) { + run_meta.os = Some(format!("{} {}", name, ver)); + } + } +} + +fn extract_timestamp(report_path: &str, metadata: &mut ReportMetadata) { + let re = regex::Regex::new(r"(\d{8})T(\d{6})").unwrap(); + if let Some(caps) = re.captures(report_path) { + let date_str = &caps[1]; + let time_str = &caps[2]; + let formatted_date = format!("{}-{}-{}", &date_str[..4], &date_str[4..6], &date_str[6..8]); + let formatted_time = format!("{}:{}:{}", &time_str[..2], &time_str[2..4], &time_str[4..6]); + metadata.timestamp = Some(format!("{} {}", formatted_date, formatted_time)); + + // Human-readable date + let months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + if let Ok(month_num) = date_str[4..6].parse::() { + if (1..=12).contains(&month_num) { + if let Ok(day) = date_str[6..8].parse::() { + metadata.date_readable = Some(format!( + "{} {:02}, {}", + months[month_num - 1], + day, + &date_str[..4] + )); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn test_data_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/mcp_test_data/single_run_report/data/js") + } + + #[test] + fn test_extract_metadata_single_run() { + let systeminfo_path = test_data_dir().join("systeminfo.js"); + if !systeminfo_path.exists() { + return; // skip if test data not available + } + let metadata = extract_metadata(&systeminfo_path, "/some/path").unwrap(); + assert_eq!(metadata.run_count, Some(1)); + let runs = metadata.runs.unwrap(); + assert_eq!(runs.len(), 1); + assert_eq!(runs[0].instance_type.as_deref(), Some("m8g.24xlarge")); + assert_eq!(runs[0].cpu_count.as_deref(), Some("96")); + } + + #[test] + fn test_extract_timestamp() { + let mut metadata = ReportMetadata::default(); + extract_timestamp("/path/to/20240115T143022_report", &mut metadata); + assert_eq!(metadata.timestamp.as_deref(), Some("2024-01-15 14:30:22")); + assert_eq!(metadata.date_readable.as_deref(), Some("January 15, 2024")); + } + + #[test] + fn test_extract_timestamp_no_match() { + let mut metadata = ReportMetadata::default(); + extract_timestamp("/path/to/some_report", &mut metadata); + assert!(metadata.timestamp.is_none()); + } +} diff --git a/src/server/mcp/mod.rs b/src/server/mcp/mod.rs new file mode 100644 index 00000000..617745fd --- /dev/null +++ b/src/server/mcp/mod.rs @@ -0,0 +1,23 @@ +//! MCP (Model Context Protocol) server for APerf report analysis. + +mod js_parser; +mod metadata; +mod report; +mod tools; + +use anyhow::Result; +use rmcp::ServiceExt; + +/// Entry point: start the MCP server on stdio. +pub fn run_mcp_server() -> Result<()> { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(async { + let service = tools::AperfMcpServer::new() + .serve(rmcp::transport::stdio()) + .await + .map_err(|e| anyhow::anyhow!("Failed to start MCP server: {}", e))?; + + service.waiting().await?; + Ok(()) + }) +} diff --git a/src/server/mcp/report.rs b/src/server/mcp/report.rs new file mode 100644 index 00000000..f6680755 --- /dev/null +++ b/src/server/mcp/report.rs @@ -0,0 +1,284 @@ +//! Report loading and validation. + +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use super::js_parser; +use super::metadata::{self, ReportMetadata}; + +/// Information about a single metric within a file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricInfo { + pub name: String, + #[serde(rename = "type")] + pub metric_type: String, +} + +/// Metrics extracted from a single JS file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileMetrics { + pub filename: String, + pub metrics: Vec, +} + +/// The loaded report state. +#[derive(Debug, Clone)] +pub struct LoadedReport { + pub report_path: String, + pub data_dir: PathBuf, + pub metadata: ReportMetadata, + pub metrics: HashMap, +} + +/// Validate that a path is a valid APerf report directory. +pub fn validate_report_path(report_path: &str) -> Result { + let path = if report_path.starts_with('~') { + let home = std::env::var("HOME").unwrap_or_default(); + PathBuf::from(report_path.replacen('~', &home, 1)) + } else { + PathBuf::from(report_path) + }; + + if !path.exists() { + bail!("Report path does not exist: {}", report_path); + } + if !path.is_dir() { + bail!("Report path is not a directory: {}", report_path); + } + + let data_dir = path.join("data").join("js"); + if !data_dir.exists() { + bail!( + "Not a valid APerf report. Missing data/js directory at: {}", + data_dir.display() + ); + } + if !data_dir.is_dir() { + bail!( + "data/js path exists but is not a directory: {}", + data_dir.display() + ); + } + + // Check required files + let required = ["runs.js", "systeminfo.js"]; + for filename in &required { + let file_path = data_dir.join(filename); + if !file_path.exists() { + bail!("Missing required file in report: {}", filename); + } + } + + // Check minimum file count + let js_count = std::fs::read_dir(&data_dir) + .with_context(|| format!("Cannot list files in {}", data_dir.display()))? + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension() == Some(std::ffi::OsStr::new("js"))) + .count(); + + if js_count < 3 { + bail!( + "Report appears incomplete. Found only {} JS files in data/js directory.", + js_count + ); + } + + Ok(data_dir) +} + +/// Load a report: validate, extract metadata, optionally extract metrics. +pub fn load_report(report_path: &str, include_metrics: bool) -> Result { + let data_dir = validate_report_path(report_path)?; + + let systeminfo_path = data_dir.join("systeminfo.js"); + let metadata = metadata::extract_metadata(&systeminfo_path, report_path)?; + + let metrics = if include_metrics { + extract_all_metrics(&data_dir)? + } else { + HashMap::new() + }; + + Ok(LoadedReport { + report_path: report_path.to_string(), + data_dir, + metadata, + metrics, + }) +} + +/// Extract metrics from all JS files in the data directory. +pub fn extract_all_metrics(data_dir: &Path) -> Result> { + let exclude = ["runs.js", "version.js", "systeminfo.js"]; + let mut metrics_by_file = HashMap::new(); + + let mut entries: Vec<_> = std::fs::read_dir(data_dir)? + .filter_map(|e| e.ok()) + .filter(|e| { + let name = e.file_name().to_string_lossy().to_string(); + name.ends_with(".js") && !exclude.contains(&name.as_str()) + }) + .collect(); + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + let filename = entry.file_name().to_string_lossy().to_string(); + + match extract_metrics_from_file(&path) { + Ok(Some((data_name, file_metrics))) => { + metrics_by_file.insert(data_name, file_metrics); + } + Ok(None) => {} // no metrics in this file + Err(e) => { + log::warn!("Error extracting metrics from {}: {}", filename, e); + } + } + } + + Ok(metrics_by_file) +} + +/// Extract metrics from a single JS file. +/// Returns Ok(Some((data_name, FileMetrics))) or Ok(None) if no metrics found. +fn extract_metrics_from_file(path: &Path) -> Result> { + let content = std::fs::read_to_string(path)?; + let filename = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Find the processed_*_data variable + let json_str = match js_parser::extract_json_from_js(&content) { + Some(s) => s, + None => return Ok(None), + }; + + let data: Value = match serde_json::from_str(&json_str) { + Ok(v) => v, + Err(_) => return Ok(None), // gracefully skip unparseable files + }; + + let data_name = data + .get("data_name") + .and_then(|v| v.as_str()) + .unwrap_or_else(|| { + path.file_stem() + .unwrap_or_default() + .to_str() + .unwrap_or("unknown") + }) + .to_string(); + + // Extract metric names from the first run + let metrics = extract_metric_names_from_data(&data); + if metrics.is_empty() { + return Ok(None); + } + + Ok(Some((data_name, FileMetrics { filename, metrics }))) +} + +/// Extract metric names and types from parsed data. +fn extract_metric_names_from_data(data: &Value) -> Vec { + let mut metrics = Vec::new(); + + let runs = match data.get("runs").and_then(|v| v.as_object()) { + Some(r) => r, + None => return metrics, + }; + + // Use first run to discover metric structure + let first_run = match runs.values().next() { + Some(r) => r, + None => return metrics, + }; + + if let Some(metrics_obj) = first_run.get("metrics").and_then(|v| v.as_object()) { + for (name, value) in metrics_obj { + let metric_type = if value.get("series").is_some() { + "time_series" + } else if value.get("value").is_some() { + "scalar" + } else if value.is_object() { + "object" + } else if value.is_array() { + "array" + } else if value.is_number() { + "number" + } else if value.is_string() { + "string" + } else { + "unknown" + }; + + metrics.push(MetricInfo { + name: name.clone(), + metric_type: metric_type.to_string(), + }); + } + } + + metrics +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn test_data_base() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/mcp_test_data") + } + + #[test] + fn test_validate_report_path_valid() { + let report = test_data_base().join("single_run_report"); + if !report.exists() { + return; + } + let result = validate_report_path(report.to_str().unwrap()); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_report_path_missing() { + let result = validate_report_path("/nonexistent/path"); + assert!(result.is_err()); + } + + #[test] + fn test_load_report_no_metrics() { + let report = test_data_base().join("single_run_report"); + if !report.exists() { + return; + } + let loaded = load_report(report.to_str().unwrap(), false).unwrap(); + assert!(loaded.metrics.is_empty()); + assert!(loaded.metadata.run_count.is_some()); + } + + #[test] + fn test_extract_metrics_from_cpu_file() { + let path = test_data_base().join("metric_files/cpu_utilization.js"); + if !path.exists() { + return; + } + let result = extract_metrics_from_file(&path).unwrap(); + assert!(result.is_some()); + let (name, file_metrics) = result.unwrap(); + assert_eq!(name, "cpu_utilization"); + assert!(!file_metrics.metrics.is_empty()); + // Should have user, system, softirq, nice etc. + let metric_names: Vec<&str> = file_metrics + .metrics + .iter() + .map(|m| m.name.as_str()) + .collect(); + assert!(metric_names.contains(&"user")); + } +} diff --git a/src/server/mcp/tools.rs b/src/server/mcp/tools.rs new file mode 100644 index 00000000..870b5d5a --- /dev/null +++ b/src/server/mcp/tools.rs @@ -0,0 +1,2745 @@ +//! MCP server implementation using rmcp 1.5. + +use rmcp::{ + handler::server::wrapper::Parameters, + model::{CallToolResult, Content, ServerCapabilities, ServerInfo}, + schemars, tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler, +}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::data::common::data_formats::{AperfData, DataFormat, ProcessedData}; +use crate::data::common::processed_data_accessor::ProcessedDataAccessor; + +use super::report::{self, LoadedReport}; + +/// Server state holding the currently loaded report. +#[derive(Debug, Default)] +struct ServerState { + report: Option, +} + +/// The APerf MCP server. +#[derive(Clone)] +pub struct AperfMcpServer { + state: Arc>, +} + +// --------------------------------------------------------------------------- +// Tool parameter structs +// --------------------------------------------------------------------------- + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars(description = "Load an APerf report from a directory path")] +pub struct LoadReportRequest { + #[schemars(description = "Absolute or relative path to the APerf report directory")] + pub report_path: String, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars(description = "Get available metrics from the loaded report")] +pub struct GetMetricsRequest { + #[schemars(description = "JS filename, e.g. 'cpu_utilization.js'")] + pub file_name: Option, + #[schemars(description = "Data category, e.g. 'cpu_utilization'")] + pub category: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars(description = "Get metric values from the loaded report")] +pub struct GetMetricValuesRequest { + #[schemars(description = "Name of the metric to retrieve (e.g. 'user', 'system', 'idle')")] + pub metric_name: String, + #[schemars(description = "JS filename containing the metric (e.g. 'cpu_utilization.js')")] + pub file_name: Option, + #[schemars( + description = "Category/data_name (e.g. 'cpu_utilization'). Either file_name or category is required." + )] + pub category: Option, + #[schemars( + description = "Output format: 'summary' (default, smart text with stats/trends/anomalies - minimal tokens), 'stats' (aggregated statistics only), 'timeseries' (full raw data), 'compact' (delta-encoded notation - ~80% fewer tokens), 'downsampled' (fixed N buckets preserving shape)" + )] + pub output_type: Option, + #[schemars( + description = "List of CPU/series IDs for per-CPU metrics (e.g. ['CPU0', 'CPU1']). If omitted, averages across all CPUs." + )] + pub cpu_ids: Option>, + #[schemars(description = "Specific run ID (default: all runs)")] + pub run_id: Option, + #[schemars( + description = "Start of time range in seconds. Negative values are relative to end (e.g. -60 = last 60s). If omitted, starts from beginning." + )] + pub from_time: Option, + #[schemars( + description = "End of time range in seconds. Negative values are relative to end (e.g. -10 = stop 10s before end). If omitted, goes to end." + )] + pub to_time: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars(description = "Get analytical findings from the loaded report")] +pub struct GetAnalyticalFindingsRequest { + #[schemars(description = "Starting index for pagination (default: 0)")] + pub offset: Option, + #[schemars(description = "Maximum number of findings to return (default: 50)")] + pub limit: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars( + description = "Get statistical findings (metric stat deltas between runs) from the loaded report" +)] +pub struct GetStatisticalFindingsRequest { + #[schemars(description = "Starting index for pagination (default: 0)")] + pub offset: Option, + #[schemars(description = "Maximum number of findings to return (default: 50)")] + pub limit: Option, + #[schemars( + description = "Filter by stat type: 'avg', 'std', 'min', 'max', 'p50', 'p90', 'p99', 'p99_9'. If omitted, returns all stats." + )] + pub stat: Option, + #[schemars( + description = "Filter by data category (e.g. 'cpu_utilization', 'meminfo'). If omitted, returns all categories." + )] + pub data_type: Option, + #[schemars( + description = "Minimum absolute delta percentage to include (e.g. 5.0 = only show deltas >= 5%). Default: 0 (all)." + )] + pub min_delta_pct: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars( + description = "Query flamegraph data from the loaded report. Returns top functions by default, with optional regex filtering to search for specific functions. Supports normal, reverse, diff, and reverse-diff modes. Returns data for all runs by default, or specific runs if run_id is provided. In diff mode, run_id must contain exactly 2 run IDs (base and comparison)." +)] +pub struct GetFlamegraphRequest { + #[schemars( + description = "Type of flamegraph: 'normal' (callers→callees, default), 'reverse' (callees→callers), 'diff' (compare two runs, normal direction), or 'reverse-diff' (compare two runs, reverse direction)" + )] + pub flamegraph_type: Option, + #[schemars( + description = "Run ID(s) to query. For normal/reverse: omit for all runs, or provide specific run IDs. For diff/reverse-diff: provide exactly 2 run IDs [base, comparison], or omit to use first two runs in the report." + )] + pub run_id: Option>, + #[schemars(description = "Maximum number of functions to return per run (default: 30)")] + pub limit: Option, + #[schemars( + description = "Minimum percentage threshold to include (default: 0.1). Functions below this % are excluded. In diff mode, this is the minimum absolute delta percentage (default: 0.5)." + )] + pub min_pct: Option, + #[schemars( + description = "Regex filter for function names (case-insensitive). Only functions matching this pattern are returned. Example: 'compact|migrate' to find compaction-related functions." + )] + pub filter: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars(description = "Record performance data on the current system")] +pub struct RecordRequest { + #[schemars(description = "Name of the run (default: aperf_)")] + pub run_name: Option, + #[schemars(description = "Interval in seconds at which data is collected (default: 1)")] + pub interval: Option, + #[schemars(description = "Duration in seconds for the recording (default: 10)")] + pub period: Option, + #[schemars(description = "Enable CPU profiling using perf (default: false)")] + pub profile: Option, + #[schemars(description = "Perf profiling frequency in Hz (default: 99)")] + pub perf_frequency: Option, + #[schemars( + description = "Collect memory allocation data: buddyinfo, pagetypeinfo, slabinfo (default: false)" + )] + pub memory_allocation: Option, + #[schemars( + description = "Comma-separated list of data types to skip (e.g. 'perf_stat,interrupts')" + )] + pub dont_collect: Option, + #[schemars( + description = "Comma-separated list of data types to collect exclusively (e.g. 'cpu_utilization,meminfo')" + )] + pub collect_only: Option, + #[schemars( + description = "Profile JVMs using async-profiler. Provide comma-separated PIDs/names, or empty to profile all JVMs." + )] + pub profile_java: Option, + #[schemars(description = "Path to custom PMU config file")] + pub pmu_config: Option, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +#[schemars(description = "Generate an HTML report from recorded APerf data")] +pub struct GenerateReportRequest { + #[schemars( + description = "Paths to run directories or archives (at least one required). For multi-run comparison, provide multiple paths." + )] + pub runs: Vec, + #[schemars( + description = "Name for the report directory and archive (default: aperf_report_)" + )] + pub name: Option, +} + +// --------------------------------------------------------------------------- +// Tool implementations +// --------------------------------------------------------------------------- + +#[tool_router] +impl AperfMcpServer { + pub fn new() -> Self { + Self { + state: Arc::new(Mutex::new(ServerState::default())), + } + } + + #[tool( + description = "Load an APerf report from a directory path and get metadata and available metrics. Must be called first before any other tool. Returns JSON with status, message, report_path, metadata (runs, aliases, instance info), and metrics (organized by file)." + )] + async fn load_report( + &self, + Parameters(LoadReportRequest { report_path }): Parameters, + ) -> Result { + match report::load_report(&report_path, true) { + Ok(loaded) => { + let response = build_load_report_response(&loaded); + let response_str = serde_json::to_string_pretty(&response) + .unwrap_or_else(|e| format!("Serialization error: {}", e)); + let mut state = self.state.lock().await; + state.report = Some(loaded); + Ok(CallToolResult::success(vec![Content::text(response_str)])) + } + Err(e) => { + let err_response = json!({"status": "error", "message": format!("Error: {}", e)}); + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string(&err_response).unwrap(), + )])) + } + } + } + + #[tool( + description = "Get available metrics from the loaded report. Supports filtering by file_name or category. Returns JSON with status, message, and metrics dict." + )] + async fn get_metrics( + &self, + Parameters(GetMetricsRequest { + file_name, + category, + }): Parameters, + ) -> Result { + let mut state = self.state.lock().await; + + let loaded = match &mut state.report { + Some(r) => r, + None => { + let err = json!({"status": "error", "message": "Error: No report loaded. Call load_report() first."}); + return Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string(&err).unwrap(), + )])); + } + }; + + // If metrics are empty, extract them now + if loaded.metrics.is_empty() { + match report::extract_all_metrics(&loaded.data_dir) { + Ok(m) => loaded.metrics = m, + Err(e) => { + let err = json!({"status": "error", "message": format!("Error extracting metrics: {}", e)}); + return Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string(&err).unwrap(), + )])); + } + } + } + + let mut metrics = loaded.metrics.clone(); + + // Filter by file_name + if let Some(ref fname) = file_name { + metrics.retain(|_, v| v.filename == *fname); + if metrics.is_empty() { + let available: Vec<&str> = loaded + .metrics + .values() + .map(|v| v.filename.as_str()) + .collect(); + let err = json!({ + "status": "error", + "message": format!("Error: No metrics found for file '{}'. Available files: {:?}", fname, available) + }); + return Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string(&err).unwrap(), + )])); + } + } + + // Filter by category + if let Some(ref cat) = category { + if !metrics.contains_key(cat.as_str()) { + let available: Vec<&str> = loaded.metrics.keys().map(|k| k.as_str()).collect(); + let err = json!({ + "status": "error", + "message": format!("Error: Category '{}' not found. Available categories: {:?}", cat, available) + }); + return Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string(&err).unwrap(), + )])); + } + let entry = metrics.remove(cat.as_str()).unwrap(); + metrics.clear(); + metrics.insert(cat.clone(), entry); + } + + let total_metrics: usize = metrics.values().map(|v| v.metrics.len()).sum(); + + let msg = if file_name.is_some() || category.is_some() { + let filter_desc = file_name + .as_deref() + .map(|f| format!("file '{}'", f)) + .or_else(|| category.as_deref().map(|c| format!("category '{}'", c))) + .unwrap(); + format!( + "Retrieved metrics for {} ({} total metrics)", + filter_desc, total_metrics + ) + } else { + format!( + "Retrieved all available metrics from {} files ({} total metrics)", + metrics.len(), + total_metrics + ) + }; + + let response = json!({ + "status": "success", + "message": msg, + "report_path": loaded.report_path, + "metrics": metrics, + }); + + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&response).unwrap(), + )])) + } + + #[tool( + description = "Get metric values from the loaded report. Returns stats (avg/std/min/max) by default, or full time-series data. Supports multi-run reports and time-range filtering. Must specify either file_name or category." + )] + async fn get_metric_values( + &self, + Parameters(req): Parameters, + ) -> Result { + let state = self.state.lock().await; + let loaded = match &state.report { + Some(r) => r, + None => { + return ok_json( + json!({"status": "error", "message": "Error: No report loaded. Call load_report() first."}), + ) + } + }; + + let output_type = req.output_type.as_deref().unwrap_or("summary"); + if !["stats", "timeseries", "summary", "compact", "downsampled"].contains(&output_type) { + return ok_json( + json!({"status": "error", "message": format!("Error: Invalid output_type '{}'. Must be 'stats', 'timeseries', 'summary', 'compact', or 'downsampled'.", output_type)}), + ); + } + + // Find the target file and deserialize into ProcessedData + let target_file = find_target_file( + &loaded.data_dir, + req.file_name.as_deref(), + req.category.as_deref(), + ); + let target_file = match target_file { + Ok(f) => f, + Err(msg) => return ok_json(json!({"status": "error", "message": msg})), + }; + + let processed_data = match load_processed_data(&target_file) { + Ok(pd) => pd, + Err(msg) => return ok_json(json!({"status": "error", "message": msg})), + }; + + let filename = target_file + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Validate run selection + let selected_run_ids: Vec<&String> = if let Some(ref rid) = req.run_id { + if !processed_data.runs.contains_key(rid) { + let available: Vec<&String> = processed_data.runs.keys().collect(); + return ok_json( + json!({"status": "error", "message": format!("Error: Run '{}' not found. Available runs: {:?}", rid, available)}), + ); + } + vec![rid] + } else { + processed_data.runs.keys().collect() + }; + + // Build ProcessedDataAccessor with time-range support + let mut accessor = if req.from_time.is_some() || req.to_time.is_some() { + let from_map: HashMap = if let Some(from) = req.from_time { + selected_run_ids + .iter() + .map(|&rid| (rid.clone(), from)) + .collect() + } else { + HashMap::new() + }; + let to_map: HashMap = if let Some(to) = req.to_time { + selected_run_ids + .iter() + .map(|&rid| (rid.clone(), to)) + .collect() + } else { + HashMap::new() + }; + ProcessedDataAccessor::from_time_ranges( + from_map, + to_map, + HashMap::new(), + HashMap::new(), + ) + } else { + ProcessedDataAccessor::new() + }; + + match processed_data.data_format { + DataFormat::TimeSeries => { + let mut run_results = Vec::new(); + for rid in &selected_run_ids { + let ts_data = match &processed_data.runs[rid.as_str()] { + AperfData::TimeSeries(ts) => ts, + _ => { + return ok_json( + json!({"status": "error", "message": "Error: Run data is not time-series format"}), + ) + } + }; + + let metric = match ts_data.metrics.get(&req.metric_name) { + Some(m) => m, + None => { + let available: Vec<&String> = ts_data.metrics.keys().collect(); + return ok_json( + json!({"status": "error", "message": format!("Error: Metric '{}' not found. Available: {:?}", req.metric_name, available)}), + ); + } + }; + + // Get stats via accessor (respects time range) + let stats = accessor + .time_series_metric_stats(&processed_data, rid, &req.metric_name) + .unwrap_or_default(); + let stats_json = serde_json::to_value(&stats).unwrap_or(json!({})); + + let result_data = if output_type == "stats" { + json!({"stats": stats_json}) + } else { + if metric.series.is_empty() { + return ok_json( + json!({"status": "error", "message": format!("Error: No time series data for metric '{}'", req.metric_name)}), + ); + } + + // Resolve series based on cpu_ids filtering or averaging + let resolved = resolve_typed_series( + metric, + &req.cpu_ids, + &req.metric_name, + &accessor, + rid, + ); + let resolved = match resolved { + Ok(r) => r, + Err(msg) => return ok_json(json!({"status": "error", "message": msg})), + }; + + match output_type { + "timeseries" => { + json!({"series": resolved.series_json, "stats": stats_json}) + } + "summary" => { + let summary = build_timeseries_summary( + &resolved.values, + &resolved.time_diff, + &stats_json, + ); + json!({"summary": summary, "stats": stats_json}) + } + "compact" => { + let compact = build_compact_notation( + &resolved.values, + &resolved.time_diff, + &req.metric_name, + ); + json!({"compact": compact, "stats": stats_json}) + } + "downsampled" => { + let ds = + build_downsampled(&resolved.values, &resolved.time_diff, 50); + json!({"downsampled": ds, "stats": stats_json, "original_points": resolved.values.len()}) + } + _ => json!({"stats": stats_json}), + } + }; + + run_results.push(json!({"run_id": rid, "data": result_data})); + } + + let type_label = match output_type { + "stats" => "Statistics", + "timeseries" => "Time series", + "summary" => "Summary", + "compact" => "Compact notation", + "downsampled" => "Downsampled", + _ => output_type, + }; + let response = if selected_run_ids.len() == 1 { + json!({ + "status": "success", + "message": format!("{} for metric '{}' from {}", type_label, req.metric_name, filename), + "metric_name": req.metric_name, + "run_id": selected_run_ids[0], + "file_name": filename, + "output_type": output_type, + "data": run_results[0]["data"] + }) + } else { + json!({ + "status": "success", + "message": format!("{} for metric '{}' from {} ({} runs)", type_label, req.metric_name, filename, selected_run_ids.len()), + "metric_name": req.metric_name, + "run_count": selected_run_ids.len(), + "file_name": filename, + "output_type": output_type, + "data": {"runs": run_results} + }) + }; + ok_json(response) + } + DataFormat::KeyValue => { + let mut run_results = Vec::new(); + for rid in &selected_run_ids { + let value = + accessor.key_value_value_by_key(&processed_data, rid, &req.metric_name); + match value { + Some(val) => { + run_results.push(json!({"run_id": rid, "data": {"value": val}})); + } + None => { + return ok_json( + json!({"status": "error", "message": format!("Error: Metric '{}' not found in key-value data", req.metric_name)}), + ); + } + } + } + + let response = if selected_run_ids.len() == 1 { + json!({ + "status": "success", + "message": format!("Key-value metric '{}' from {}", req.metric_name, filename), + "metric_name": req.metric_name, + "run_id": selected_run_ids[0], + "file_name": filename, + "output_type": "key_value", + "data": run_results[0]["data"] + }) + } else { + json!({ + "status": "success", + "message": format!("Key-value metric '{}' from {} ({} runs)", req.metric_name, filename, selected_run_ids.len()), + "metric_name": req.metric_name, + "run_count": selected_run_ids.len(), + "file_name": filename, + "output_type": "key_value", + "data": {"runs": run_results} + }) + }; + ok_json(response) + } + _ => ok_json( + json!({"status": "error", "message": format!("Error: Unsupported data format '{:?}'", processed_data.data_format)}), + ), + } + } + + #[tool( + description = "Get analytical findings from the loaded report with pagination. Findings represent significant differences between runs (regressions, improvements, mismatches)." + )] + async fn get_analytical_findings( + &self, + Parameters(req): Parameters, + ) -> Result { + let state = self.state.lock().await; + let loaded = match &state.report { + Some(r) => r, + None => { + return ok_json( + json!({"status": "error", "message": "Error: No report loaded. Call load_report() first."}), + ) + } + }; + + let offset = req.offset.unwrap_or(0).max(0) as usize; + let limit = req.limit.unwrap_or(50).max(1) as usize; + + // Scan all JS files for *_findings variables + let mut all_findings: Vec = Vec::new(); + + let mut js_files: Vec<_> = std::fs::read_dir(&loaded.data_dir) + .unwrap_or_else(|_| std::fs::read_dir(".").unwrap()) + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension() == Some(std::ffi::OsStr::new("js"))) + .collect(); + js_files.sort_by_key(|e| e.file_name()); + + for entry in &js_files { + let content = match std::fs::read_to_string(entry.path()) { + Ok(c) => c, + Err(_) => continue, + }; + + let findings_vars = super::js_parser::extract_findings_variables(&content); + for (category, json_str) in findings_vars { + let findings_data: Value = match serde_json::from_str(&json_str) { + Ok(v) => v, + Err(_) => continue, + }; + + let per_run = match findings_data + .get("per_run_findings") + .and_then(|v| v.as_object()) + { + Some(p) => p, + None => continue, + }; + + for (run_id, run_data) in per_run { + let findings_dict = match run_data.get("findings").and_then(|v| v.as_object()) { + Some(f) => f, + None => continue, + }; + + for (metric_name, finding_list) in findings_dict { + if let Some(arr) = finding_list.as_array() { + for finding in arr { + all_findings.push(json!({ + "category": category, + "run_id": run_id, + "metric_name": metric_name, + "rule_name": finding.get("rule_name").and_then(|v| v.as_str()).unwrap_or(""), + "score": finding.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0), + "description": finding.get("description").and_then(|v| v.as_str()).unwrap_or(""), + "message": finding.get("message").and_then(|v| v.as_str()).unwrap_or(""), + "reference": finding.get("reference").and_then(|v| v.as_str()).unwrap_or(""), + })); + } + } + } + } + } + } + + // Sort by absolute score descending (highest severity first) + all_findings.sort_by(|a, b| { + let score_a = a.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0).abs(); + let score_b = b.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0).abs(); + score_b + .partial_cmp(&score_a) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + let total = all_findings.len(); + let end = (offset + limit).min(total); + let paginated: Vec = if offset < total { + all_findings[offset..end].to_vec() + } else { + Vec::new() + }; + let returned_count = paginated.len(); + let has_more = end < total; + + let msg = if total == 0 { + "No analytical findings found in the report.".to_string() + } else if returned_count == 0 { + format!( + "No findings at offset {}. Total available: {}", + offset, total + ) + } else { + format!( + "Analytical findings (showing {} of {}, range {}-{})", + returned_count, + total, + offset + 1, + offset + returned_count + ) + }; + + let response = json!({ + "status": "success", + "message": msg, + "report_path": loaded.report_path, + "total_findings": total, + "offset": offset, + "limit": limit, + "returned_count": returned_count, + "has_more": has_more, + "findings": paginated, + }); + + ok_json(response) + } + + #[tool( + description = "Get statistical findings (metric stat deltas between runs). Computes the percentage change of each time-series metric's statistics compared to the base run (first run). Sorted by absolute delta descending. Supports filtering by stat type, data category, and minimum delta threshold." + )] + async fn get_statistical_findings( + &self, + Parameters(req): Parameters, + ) -> Result { + let state = self.state.lock().await; + let loaded = match &state.report { + Some(r) => r, + None => { + return ok_json( + json!({"status": "error", "message": "Error: No report loaded. Call load_report() first."}), + ) + } + }; + + let offset = req.offset.unwrap_or(0).max(0) as usize; + let limit = req.limit.unwrap_or(50).max(1) as usize; + let min_delta = req.min_delta_pct.unwrap_or(0.0); + + let valid_stats = ["avg", "std", "min", "max", "p50", "p90", "p99", "p99_9"]; + if let Some(ref s) = req.stat { + if !valid_stats.contains(&s.as_str()) { + return ok_json( + json!({"status": "error", "message": format!("Error: Invalid stat '{}'. Must be one of: {:?}", s, valid_stats)}), + ); + } + } + + // Collect all time-series JS files and compute deltas + let exclude = ["runs.js", "version.js", "systeminfo.js"]; + let mut entries: Vec<_> = std::fs::read_dir(&loaded.data_dir) + .unwrap_or_else(|_| std::fs::read_dir(".").unwrap()) + .filter_map(|e| e.ok()) + .filter(|e| { + let name = e.file_name().to_string_lossy().to_string(); + name.ends_with(".js") && !exclude.contains(&name.as_str()) + }) + .collect(); + entries.sort_by_key(|e| e.file_name()); + + let mut all_deltas: Vec = Vec::new(); + + for entry in &entries { + let pd = match load_processed_data(&entry.path()) { + Ok(pd) => pd, + Err(_) => continue, + }; + + // Only process time-series data + if !matches!(pd.data_format, DataFormat::TimeSeries) { + continue; + } + + // Filter by data_type if specified + if let Some(ref dt) = req.data_type { + if pd.data_name != *dt { + continue; + } + } + + let run_ids: Vec = pd.runs.keys().cloned().collect(); + if run_ids.len() < 2 { + continue; // Need at least 2 runs for comparison + } + + // First run is the base + let base_run = &run_ids[0]; + let mut accessor = ProcessedDataAccessor::new(); + + // Get all metric names from base run + let metric_names: Vec = match &pd.runs[base_run] { + AperfData::TimeSeries(ts) => ts.metrics.keys().cloned().collect(), + _ => continue, + }; + + for run_id in &run_ids[1..] { + for metric_name in &metric_names { + let base_stats = + match accessor.time_series_metric_stats(&pd, base_run, metric_name) { + Some(s) => s, + None => continue, + }; + let run_stats = + match accessor.time_series_metric_stats(&pd, run_id, metric_name) { + Some(s) => s, + None => continue, + }; + + // Compute delta for each stat + let stat_pairs: &[(&str, f64, f64)] = &[ + ("avg", base_stats.avg, run_stats.avg), + ("std", base_stats.std, run_stats.std), + ("min", base_stats.min, run_stats.min), + ("max", base_stats.max, run_stats.max), + ("p50", base_stats.p50, run_stats.p50), + ("p90", base_stats.p90, run_stats.p90), + ("p99", base_stats.p99, run_stats.p99), + ("p99_9", base_stats.p99_9, run_stats.p99_9), + ]; + + for &(stat_name, base_val, run_val) in stat_pairs { + // Filter by stat if specified + if let Some(ref s) = req.stat { + if s != stat_name { + continue; + } + } + + // Compute percentage delta + let delta_pct = if base_val.abs() > 1e-10 { + ((run_val - base_val) / base_val) * 100.0 + } else if run_val.abs() > 1e-10 { + 100.0 // went from ~0 to something + } else { + 0.0 // both ~0 + }; + + // Filter by min_delta + if delta_pct.abs() < min_delta { + continue; + } + + // Skip zero deltas + if delta_pct == 0.0 { + continue; + } + + all_deltas.push(json!({ + "data_type": pd.data_name, + "metric_name": metric_name, + "stat": stat_name, + "run_id": run_id, + "base_run_id": base_run, + "base_value": (base_val * 100.0).round() / 100.0, + "run_value": (run_val * 100.0).round() / 100.0, + "delta_pct": (delta_pct * 100.0).round() / 100.0, + })); + } + } + } + } + + // Sort by absolute delta descending + all_deltas.sort_by(|a, b| { + let da = a + .get("delta_pct") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + .abs(); + let db = b + .get("delta_pct") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + .abs(); + db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal) + }); + + let total = all_deltas.len(); + let end = (offset + limit).min(total); + let paginated: Vec = if offset < total { + all_deltas[offset..end].to_vec() + } else { + Vec::new() + }; + let returned_count = paginated.len(); + let has_more = end < total; + + let msg = if total == 0 { + "No statistical findings (metric deltas) found. Requires a multi-run report." + .to_string() + } else if returned_count == 0 { + format!( + "No findings at offset {}. Total available: {}", + offset, total + ) + } else { + format!( + "Statistical findings (showing {} of {}, range {}-{})", + returned_count, + total, + offset + 1, + offset + returned_count + ) + }; + + let response = json!({ + "status": "success", + "message": msg, + "report_path": loaded.report_path, + "total_findings": total, + "offset": offset, + "limit": limit, + "returned_count": returned_count, + "has_more": has_more, + "findings": paginated, + }); + + ok_json(response) + } + + #[tool( + description = "Query flamegraph data from the loaded report. Returns top functions by default (sorted by percentage), with optional regex filtering to search for specific functions. Supports normal, reverse, diff, and reverse-diff modes. Returns data for all runs by default, or a specific run if run_id is provided. In diff/reverse-diff mode, compares two runs and returns functions with the biggest percentage change." + )] + async fn get_flamegraph( + &self, + Parameters(req): Parameters, + ) -> Result { + let state = self.state.lock().await; + let loaded = match &state.report { + Some(r) => r, + None => { + return ok_json( + json!({"status": "error", "message": "Error: No report loaded. Call load_report() first."}), + ) + } + }; + + let fg_type = req.flamegraph_type.as_deref().unwrap_or("normal"); + if !["normal", "reverse", "diff", "reverse-diff"].contains(&fg_type) { + return ok_json( + json!({"status": "error", "message": "Error: flamegraph_type must be 'normal', 'reverse', 'diff', or 'reverse-diff'."}), + ); + } + + let is_diff = fg_type == "diff" || fg_type == "reverse-diff"; + // The underlying SVG type for file lookup + let svg_type = if fg_type == "reverse-diff" { + "reverse" + } else if fg_type == "diff" { + "normal" + } else { + fg_type + }; + + let limit = req.limit.unwrap_or(30); + + // Compile regex filter if provided + let filter_re = if let Some(ref pattern) = req.filter { + match Regex::new(&format!("(?i){}", pattern)) { + Ok(re) => Some(re), + Err(e) => { + return ok_json( + json!({"status": "error", "message": format!("Error: Invalid regex filter '{}': {}", pattern, e)}), + ) + } + } + } else { + None + }; + + // --- DIFF MODE --- + if is_diff { + let min_delta = req.min_pct.unwrap_or(0.5); + + // Resolve the two run IDs for diff + let (rid1, rid2) = match &req.run_id { + Some(ids) if ids.len() == 2 => (Some(ids[0].as_str()), Some(ids[1].as_str())), + Some(ids) if ids.len() > 2 => { + return ok_json( + json!({"status": "error", "message": "Error: diff/reverse-diff mode requires exactly 2 run IDs in run_id array."}), + ); + } + Some(ids) if ids.len() == 1 => { + return ok_json( + json!({"status": "error", "message": "Error: diff/reverse-diff mode requires exactly 2 run IDs in run_id array. Got 1."}), + ); + } + _ => (None, None), // Use defaults (first and second run) + }; + + // Find both SVGs + let svg1 = find_flamegraph_svg(loaded, rid1, svg_type); + let svg1 = match svg1 { + Ok(p) => p, + Err(msg) => { + return ok_json( + json!({"status": "error", "message": format!("Base run: {}", msg)}), + ) + } + }; + let svg2 = find_flamegraph_svg(loaded, rid2, svg_type); + let svg2 = match svg2 { + Ok(p) => p, + Err(msg) => { + return ok_json( + json!({"status": "error", "message": format!("Comparison run: {}", msg)}), + ) + } + }; + + if svg1 == svg2 { + return ok_json( + json!({"status": "error", "message": "Error: Both run IDs resolve to the same flamegraph. Provide two different run IDs in the run_id array."}), + ); + } + + // Parse both + let frames1 = match parse_flamegraph_svg(&svg1) { + Ok(f) => f, + Err(msg) => return ok_json(json!({"status": "error", "message": msg})), + }; + let frames2 = match parse_flamegraph_svg(&svg2) { + Ok(f) => f, + Err(msg) => return ok_json(json!({"status": "error", "message": msg})), + }; + + // Build function → pct maps + let map1: HashMap<&str, f64> = + frames1.iter().map(|f| (f.name.as_str(), f.pct)).collect(); + let map2: HashMap<&str, f64> = + frames2.iter().map(|f| (f.name.as_str(), f.pct)).collect(); + + // Compute deltas + let mut all_functions: std::collections::HashSet<&str> = + std::collections::HashSet::new(); + all_functions.extend(map1.keys()); + all_functions.extend(map2.keys()); + + let mut deltas: Vec = Vec::new(); + for func in all_functions { + let pct1 = map1.get(func).copied().unwrap_or(0.0); + let pct2 = map2.get(func).copied().unwrap_or(0.0); + let delta = pct2 - pct1; + if delta.abs() >= min_delta { + // Apply regex filter if provided + if let Some(ref re) = filter_re { + if !re.is_match(func) { + continue; + } + } + deltas.push(json!({ + "function": func, + "pct_run1": (pct1 * 100.0).round() / 100.0, + "pct_run2": (pct2 * 100.0).round() / 100.0, + "delta_pct": (delta * 100.0).round() / 100.0, + })); + } + } + + // Sort by absolute delta descending + deltas.sort_by(|a, b| { + let da = a + .get("delta_pct") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + .abs(); + let db = b + .get("delta_pct") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + .abs(); + db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal) + }); + + let total = deltas.len(); + let truncated: Vec = deltas.into_iter().take(limit).collect(); + + let filter_desc = if let Some(ref pattern) = req.filter { + format!(" matching '{}'", pattern) + } else { + String::new() + }; + + let response = json!({ + "status": "success", + "message": format!("Flamegraph diff ({}): {} functions{} with delta >= {:.1}% (showing top {})", fg_type, total, filter_desc, min_delta, truncated.len()), + "flamegraph_type": fg_type, + "total_changed": total, + "functions": truncated, + }); + + return ok_json(response); + } + + // --- NORMAL / REVERSE MODE --- + let min_pct = req.min_pct.unwrap_or(0.1); + + // Determine which runs to process + let run_ids: Vec = match &req.run_id { + Some(ids) if !ids.is_empty() => ids.clone(), + _ => find_all_flamegraph_run_ids(loaded, fg_type), + }; + + if run_ids.is_empty() { + return ok_json( + json!({"status": "error", "message": format!("Error: No {} flamegraph SVGs found in report.", fg_type)}), + ); + } + + let mut all_run_results: Vec = Vec::new(); + + for rid in &run_ids { + let svg_path = find_flamegraph_svg(loaded, Some(rid.as_str()), fg_type); + let svg_path = match svg_path { + Ok(p) => p, + Err(msg) => { + log::warn!("Skipping run '{}': {}", rid, msg); + continue; + } + }; + + let frames = match parse_flamegraph_svg(&svg_path) { + Ok(f) => f, + Err(msg) => { + all_run_results.push(json!({ + "run_id": rid, + "status": "error", + "message": msg, + })); + continue; + } + }; + + // Build parent relationships and filter by min_pct + let annotated = build_annotated_frames(&frames, min_pct); + + // Apply regex filter if provided + let filtered: Vec<&AnnotatedFrame> = if let Some(ref re) = filter_re { + annotated.iter().filter(|f| re.is_match(&f.name)).collect() + } else { + annotated.iter().collect() + }; + + // Take top N by percentage + let top: Vec = filtered + .iter() + .take(limit) + .map(|f| { + json!({ + "function": f.name, + "samples": f.samples, + "pct": f.pct, + "depth": f.depth, + "parent": f.parent, + }) + }) + .collect(); + + let total_samples = frames.first().map(|f| f.samples).unwrap_or(0); + + all_run_results.push(json!({ + "run_id": rid, + "total_frames_parsed": frames.len(), + "total_samples": total_samples, + "matched_count": filtered.len(), + "returned_count": top.len(), + "functions": top, + })); + } + + // Build response + let filter_desc = if let Some(ref pattern) = req.filter { + format!(" matching filter '{}'", pattern) + } else { + String::new() + }; + + let response = if all_run_results.len() == 1 { + let run_data = &all_run_results[0]; + json!({ + "status": "success", + "message": format!("Flamegraph ({} type) for run '{}': {} functions{} (top {})", + fg_type, + run_data.get("run_id").and_then(|v| v.as_str()).unwrap_or("?"), + run_data.get("matched_count").and_then(|v| v.as_u64()).unwrap_or(0), + filter_desc, + run_data.get("returned_count").and_then(|v| v.as_u64()).unwrap_or(0), + ), + "flamegraph_type": fg_type, + "run_id": run_data.get("run_id"), + "total_frames_parsed": run_data.get("total_frames_parsed"), + "total_samples": run_data.get("total_samples"), + "matched_count": run_data.get("matched_count"), + "functions": run_data.get("functions"), + }) + } else { + json!({ + "status": "success", + "message": format!("Flamegraph ({} type) for {} runs{} (top {} per run)", + fg_type, all_run_results.len(), filter_desc, limit), + "flamegraph_type": fg_type, + "run_count": all_run_results.len(), + "runs": all_run_results, + }) + }; + + ok_json(response) + } + + #[tool( + description = "Record performance data on the current system using aperf. Runs 'aperf record' as a subprocess. Returns when recording completes. Requires Linux and appropriate kernel permissions." + )] + async fn record( + &self, + Parameters(req): Parameters, + ) -> Result { + let aperf_bin = match std::env::current_exe() { + Ok(p) => p, + Err(e) => { + return ok_json( + json!({"status": "error", "message": format!("Cannot find aperf binary: {}", e)}), + ) + } + }; + + let args = build_record_args(&req); + let cmd_str = format!("{} {}", aperf_bin.display(), args.join(" ")); + + match tokio::process::Command::new(&aperf_bin) + .args(&args) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + { + Ok(child) => match child.wait_with_output().await { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let success = output.status.success(); + + let run_name = req + .run_name + .clone() + .unwrap_or_else(|| "aperf_".to_string()); + + ok_json(json!({ + "status": if success { "success" } else { "error" }, + "message": if success { + format!("Recording completed successfully. Run name: {}", run_name) + } else { + format!("Recording failed with exit code: {}", output.status) + }, + "command": cmd_str, + "exit_code": output.status.code(), + "stdout": stdout.trim(), + "stderr": stderr.trim(), + })) + } + Err(e) => ok_json( + json!({"status": "error", "message": format!("Failed to wait for process: {}", e), "command": cmd_str}), + ), + }, + Err(e) => ok_json( + json!({"status": "error", "message": format!("Failed to spawn aperf record: {}", e), "command": cmd_str}), + ), + } + } + + #[tool( + description = "Generate an HTML report from one or more recorded APerf runs. Runs 'aperf report' as a subprocess. For multi-run comparison, provide multiple run paths — the first run is used as the base for analytical findings." + )] + async fn generate_report( + &self, + Parameters(req): Parameters, + ) -> Result { + if req.runs.is_empty() { + return ok_json( + json!({"status": "error", "message": "Error: At least one run path is required."}), + ); + } + + let aperf_bin = match std::env::current_exe() { + Ok(p) => p, + Err(e) => { + return ok_json( + json!({"status": "error", "message": format!("Cannot find aperf binary: {}", e)}), + ) + } + }; + + let args = build_report_args(&req); + let cmd_str = format!("{} {}", aperf_bin.display(), args.join(" ")); + + match tokio::process::Command::new(&aperf_bin) + .args(&args) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + { + Ok(child) => match child.wait_with_output().await { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let success = output.status.success(); + + let report_name = req + .name + .clone() + .unwrap_or_else(|| "aperf_report_".to_string()); + + ok_json(json!({ + "status": if success { "success" } else { "error" }, + "message": if success { + format!("Report generated successfully. Report name: {}. Open {}/index.html in a browser to view.", report_name, report_name) + } else { + format!("Report generation failed with exit code: {}", output.status) + }, + "command": cmd_str, + "run_count": req.runs.len(), + "runs": req.runs, + "exit_code": output.status.code(), + "stdout": stdout.trim(), + "stderr": stderr.trim(), + })) + } + Err(e) => ok_json( + json!({"status": "error", "message": format!("Failed to wait for process: {}", e), "command": cmd_str}), + ), + }, + Err(e) => ok_json( + json!({"status": "error", "message": format!("Failed to spawn aperf report: {}", e), "command": cmd_str}), + ), + } + } +} + +#[tool_handler] +impl ServerHandler for AperfMcpServer { + fn get_info(&self) -> ServerInfo { + let mut info = ServerInfo::default(); + info.instructions = Some( + "APerf MCP Server — analyze APerf performance reports. \ + Call load_report first, then query metrics and findings." + .to_string(), + ); + info.capabilities = ServerCapabilities::builder().enable_tools().build(); + info + } +} + +// --------------------------------------------------------------------------- +// Series resolution (cpu_ids filtering / averaging) using typed data +// --------------------------------------------------------------------------- + +use crate::data::common::data_formats::{Series, TimeSeriesMetric}; + +struct ResolvedTypedSeries { + series_json: Value, // JSON array of series for "timeseries" output + values: Vec, // single flattened/averaged values for summary/compact/downsampled + time_diff: Vec, // corresponding time points +} + +/// Resolve series from a typed TimeSeriesMetric, applying cpu_ids filtering or averaging. +fn resolve_typed_series( + metric: &TimeSeriesMetric, + cpu_ids: &Option>, + metric_name: &str, + _accessor: &ProcessedDataAccessor, + _run_name: &str, +) -> Result { + let has_multiple_series = metric.series.len() > 1; + + if has_multiple_series { + if let Some(ref ids) = cpu_ids { + // Filter to requested series + let filtered: Vec<&Series> = metric + .series + .iter() + .filter(|s| ids.iter().any(|c| c == &s.series_name)) + .collect(); + if filtered.is_empty() { + let available: Vec<&str> = metric + .series + .iter() + .map(|s| s.series_name.as_str()) + .collect(); + return Err(format!( + "Error: None of the requested CPUs {:?} found. Available: {:?}", + ids, available + )); + } + let values = filtered[0].values.clone(); + let time_diff: Vec = filtered[0].time_diff.iter().map(|&t| t as f64).collect(); + let series_json = serde_json::to_value(&filtered).unwrap_or(json!([])); + Ok(ResolvedTypedSeries { + series_json, + values, + time_diff, + }) + } else { + // Average across all series + let (averaged, td) = average_typed_series(&metric.series, metric_name)?; + let avg_series = + json!([{"series_name": "averaged", "time_diff": td, "values": averaged}]); + Ok(ResolvedTypedSeries { + series_json: avg_series, + values: averaged, + time_diff: td.iter().map(|&x| x as f64).collect(), + }) + } + } else { + // Single series + let s = &metric.series[0]; + let values = s.values.clone(); + let time_diff: Vec = s.time_diff.iter().map(|&t| t as f64).collect(); + let series_json = serde_json::to_value(&metric.series).unwrap_or(json!([])); + Ok(ResolvedTypedSeries { + series_json, + values, + time_diff, + }) + } +} + +fn average_typed_series( + series: &[Series], + metric_name: &str, +) -> Result<(Vec, Vec), String> { + if series.is_empty() || series[0].values.is_empty() { + return Err(format!( + "Error: No values in time series for '{}'", + metric_name + )); + } + let num_points = series[0].values.len(); + let mut averaged = Vec::with_capacity(num_points); + for i in 0..num_points { + let mut sum = 0.0; + let mut count = 0usize; + for s in series { + if i < s.values.len() { + sum += s.values[i]; + count += 1; + } + } + averaged.push(if count > 0 { sum / count as f64 } else { 0.0 }); + } + let td = series[0].time_diff.clone(); + Ok((averaged, td)) +} + +// --------------------------------------------------------------------------- +// Helper: load a JS file into ProcessedData +// --------------------------------------------------------------------------- + +/// Read a JS file, strip the variable prefix, and deserialize into ProcessedData. +fn load_processed_data(path: &std::path::Path) -> Result { + let content = + std::fs::read_to_string(path).map_err(|e| format!("Error reading file: {}", e))?; + let json_str = super::js_parser::extract_json_from_js(&content) + .ok_or_else(|| format!("Error: No data found in {}", path.display()))?; + serde_json::from_str(&json_str).map_err(|e| format!("Error deserializing ProcessedData: {}", e)) +} + +// --------------------------------------------------------------------------- +// Flamegraph SVG parsing +// --------------------------------------------------------------------------- + +use regex::Regex; +use std::path::PathBuf; + +/// A single frame parsed from a flamegraph SVG. +#[derive(Debug, Clone)] +struct FlamegraphFrame { + name: String, + samples: u64, + pct: f64, + depth: u32, + x: u64, // fg:x — horizontal position in sample space + w: u64, // fg:w — width in sample space +} + +/// A frame annotated with its parent function name. +struct AnnotatedFrame { + name: String, + samples: u64, + pct: f64, + depth: u32, + parent: String, +} + +/// Find the flamegraph SVG file for a given run and type. +fn find_flamegraph_svg( + loaded: &LoadedReport, + run_id: Option<&str>, + fg_type: &str, +) -> Result { + // Flamegraph SVGs live in the same data/js/ directory as metric files + let search_dir = &loaded.data_dir; + let not_reverse = "reverse-flamegraph.svg"; + + // If run_id specified, look for files containing run_id and matching the type + if let Some(rid) = run_id { + if let Ok(entries) = std::fs::read_dir(search_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + let matches = if fg_type == "reverse" { + name.contains(rid) && name.ends_with(not_reverse) + } else { + name.contains(rid) + && name.ends_with("flamegraph.svg") + && !name.ends_with(not_reverse) + }; + if matches { + return Ok(entry.path()); + } + } + } + return Err(format!( + "Error: No {} flamegraph found for run '{}' in {}", + fg_type, + rid, + search_dir.display() + )); + } + + // No run_id — find any matching SVG (prefer first alphabetically) + if let Ok(entries) = std::fs::read_dir(search_dir) { + let mut svgs: Vec = entries + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + let name = p + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + if fg_type == "reverse" { + name.ends_with(not_reverse) + } else { + name.ends_with("flamegraph.svg") && !name.ends_with(not_reverse) + } + }) + .collect(); + svgs.sort(); + if let Some(first) = svgs.first() { + return Ok(first.clone()); + } + } + + Err(format!( + "Error: No {} flamegraph SVG found in {}", + fg_type, + search_dir.display() + )) +} + +/// Find all run IDs that have flamegraph SVGs available. +/// Extracts run IDs from SVG filenames by matching against known run IDs from metadata. +fn find_all_flamegraph_run_ids(loaded: &LoadedReport, fg_type: &str) -> Vec { + let search_dir = &loaded.data_dir; + let not_reverse = "reverse-flamegraph.svg"; + + // Get known run IDs from metadata + let known_run_ids: Vec = loaded.metadata.run_ids.clone().unwrap_or_default(); + + // Find all matching SVG files + let svgs: Vec = match std::fs::read_dir(search_dir) { + Ok(entries) => entries + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .filter(|name| { + if fg_type == "reverse" { + name.ends_with(not_reverse) + } else { + name.ends_with("flamegraph.svg") && !name.ends_with(not_reverse) + } + }) + .collect(), + Err(_) => return Vec::new(), + }; + + // Match SVG filenames to known run IDs + let mut matched_ids: Vec = Vec::new(); + for rid in &known_run_ids { + if svgs.iter().any(|svg| svg.contains(rid.as_str())) { + matched_ids.push(rid.clone()); + } + } + + // If no known run IDs matched (e.g., single-run report), fall back to returning + // a synthetic ID based on the SVG filename + if matched_ids.is_empty() && !svgs.is_empty() { + // Use the SVG filenames themselves as identifiers — extract the part before "-flamegraph.svg" + let mut sorted_svgs = svgs; + sorted_svgs.sort(); + for svg_name in &sorted_svgs { + let stem = if fg_type == "reverse" { + svg_name.trim_end_matches("-reverse-flamegraph.svg") + } else { + svg_name.trim_end_matches("-flamegraph.svg") + }; + if !stem.is_empty() && stem != svg_name { + matched_ids.push(stem.to_string()); + } + } + } + + matched_ids +} + +/// Parse a flamegraph SVG and extract all frames. +fn parse_flamegraph_svg(path: &std::path::Path) -> Result, String> { + let content = std::fs::read_to_string(path).map_err(|e| format!("Error reading SVG: {}", e))?; + + // Extract total_samples from: + let total_samples_re = Regex::new(r#"total_samples="(\d+)""#).unwrap(); + let total_samples: u64 = total_samples_re + .captures(&content) + .and_then(|c| c.get(1)) + .and_then(|m| m.as_str().parse().ok()) + .unwrap_or(0); + + if total_samples == 0 { + return Err("Error: Could not find total_samples in SVG.".to_string()); + } + + // Parse frames: function_name (N samples, X.XX%) + // The title format is: "name (N samples, X.XX%)" or "name (N,NNN samples, X.XX%)" + let frame_re = Regex::new( + r#"([^<]+?) \(([\d,]+) samples?, ([\d.]+)%\)]*y="(\d+)"[^>]*fg:x="(\d+)"[^>]*fg:w="(\d+)""#, + ) + .unwrap(); + + let mut frames: Vec = Vec::new(); + + for cap in frame_re.captures_iter(&content) { + let name = cap[1].to_string(); + let samples: u64 = cap[2].replace(',', "").parse().unwrap_or(0); + let pct: f64 = cap[3].parse().unwrap_or(0.0); + let y: u32 = cap[4].parse().unwrap_or(0); + let x: u64 = cap[5].parse().unwrap_or(0); + let w: u64 = cap[6].parse().unwrap_or(0); + + // Skip the "all" root frame + if name == "all" { + continue; + } + + frames.push(FlamegraphFrame { + name, + samples, + pct, + depth: y, + x, + w, + }); + } + + if frames.is_empty() { + return Err(format!( + "Error: No frames found in SVG at {}", + path.display() + )); + } + + Ok(frames) +} + +/// Build annotated frames with parent relationships, sorted by pct descending. +fn build_annotated_frames(frames: &[FlamegraphFrame], min_pct: f64) -> Vec { + // Sort frames by depth then x for parent lookup + let mut sorted: Vec<&FlamegraphFrame> = frames.iter().collect(); + sorted.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.x.cmp(&b.x))); + + // For each frame, find its parent: the frame at depth-15 (one level up, since each level is 15px) + // whose x-range contains this frame's x-range + let mut annotated: Vec = Vec::new(); + + for frame in frames { + if frame.pct < min_pct { + continue; + } + + // Find parent: frame at depth - 15 (or + 15 for inverted) where parent.x <= frame.x and parent.x + parent.w >= frame.x + frame.w + let parent_depth = if frame.depth >= 15 { + frame.depth - 15 + } else { + frame.depth + 15 + }; + + let parent_name = sorted + .iter() + .find(|f| { + f.depth == parent_depth && f.x <= frame.x && (f.x + f.w) >= (frame.x + frame.w) + }) + .map(|f| f.name.as_str()) + .unwrap_or("-"); + + annotated.push(AnnotatedFrame { + name: frame.name.clone(), + samples: frame.samples, + pct: frame.pct, + depth: frame.depth / 15, // Convert pixel depth to stack level + parent: parent_name.to_string(), + }); + } + + // Sort by percentage descending + annotated.sort_by(|a, b| { + b.pct + .partial_cmp(&a.pct) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + annotated +} + +// --------------------------------------------------------------------------- +// Output format: summary +// --------------------------------------------------------------------------- + +fn build_timeseries_summary(values: &[f64], time_diff: &[f64], stats: &Value) -> String { + if values.is_empty() { + return "No data points".to_string(); + } + let n = values.len(); + let avg = stats.get("avg").and_then(|v| v.as_f64()).unwrap_or(0.0); + let min = stats.get("min").and_then(|v| v.as_f64()).unwrap_or(0.0); + let max = stats.get("max").and_then(|v| v.as_f64()).unwrap_or(0.0); + let std_val = stats.get("std").and_then(|v| v.as_f64()).unwrap_or(0.0); + let p50 = stats.get("p50").and_then(|v| v.as_f64()).unwrap_or(0.0); + let p90 = stats.get("p90").and_then(|v| v.as_f64()).unwrap_or(0.0); + let p99 = stats.get("p99").and_then(|v| v.as_f64()).unwrap_or(0.0); + + let duration = if time_diff.len() >= 2 { + time_diff[time_diff.len() - 1] - time_diff[0] + } else { + n as f64 + }; + + // Trend: compare first 10% avg vs last 10% + let tenth = (n / 10).max(1); + let first_avg: f64 = values[..tenth].iter().sum::() / tenth as f64; + let last_avg: f64 = values[n - tenth..].iter().sum::() / tenth as f64; + let trend_pct = if first_avg.abs() > 0.001 { + ((last_avg - first_avg) / first_avg) * 100.0 + } else { + 0.0 + }; + let trend = if trend_pct.abs() < 2.0 { + "stable".to_string() + } else if trend_pct > 0.0 { + format!("rising({:+.1}%)", trend_pct) + } else { + format!("falling({:+.1}%)", trend_pct) + }; + + // Detect spikes (>2 std above avg) and drops (>2 std below avg) + let mut anomalies: Vec = Vec::new(); + let spike_threshold = avg + 2.0 * std_val; + let drop_threshold = (avg - 2.0 * std_val).max(0.0); + + // Spike detection + let mut in_spike = false; + let mut spike_start = 0usize; + let mut spike_peak = 0.0f64; + for i in 0..n { + let v = values[i]; + if v > spike_threshold && !in_spike { + in_spike = true; + spike_start = i; + spike_peak = v; + } else if v > spike_threshold && in_spike { + spike_peak = spike_peak.max(v); + } else if in_spike { + in_spike = false; + let t_start = if spike_start < time_diff.len() { + time_diff[spike_start] + } else { + spike_start as f64 + }; + let t_end = if i < time_diff.len() { + time_diff[i] + } else { + i as f64 + }; + anomalies.push(format!( + "spike t={:.0}s-{:.0}s (peak {:.1})", + t_start, t_end, spike_peak + )); + if anomalies.len() >= 5 { + break; + } + } + } + + // Drop detection (only if std > 0 to avoid false positives on flat data) + if std_val > 0.001 && anomalies.len() < 5 { + let mut in_drop = false; + let mut drop_start = 0usize; + let mut drop_trough = f64::MAX; + for i in 0..n { + let v = values[i]; + if v < drop_threshold && !in_drop { + in_drop = true; + drop_start = i; + drop_trough = v; + } else if v < drop_threshold && in_drop { + drop_trough = drop_trough.min(v); + } else if in_drop { + in_drop = false; + let t_start = if drop_start < time_diff.len() { + time_diff[drop_start] + } else { + drop_start as f64 + }; + let t_end = if i < time_diff.len() { + time_diff[i] + } else { + i as f64 + }; + anomalies.push(format!( + "drop t={:.0}s-{:.0}s (trough {:.1})", + t_start, t_end, drop_trough + )); + if anomalies.len() >= 5 { + break; + } + } + } + } + + let mut summary = format!( + "points={}, duration={:.0}s, avg={:.2}, min={:.2}, max={:.2}, std={:.2}, p50={:.2}, p90={:.2}, p99={:.2}, trend={}", + n, duration, avg, min, max, std_val, p50, p90, p99, trend + ); + if !anomalies.is_empty() { + summary.push_str(&format!(", anomalies: [{}]", anomalies.join("; "))); + } + summary +} + +// --------------------------------------------------------------------------- +// Output format: compact (delta-encoded) +// --------------------------------------------------------------------------- + +fn build_compact_notation(values: &[f64], time_diff: &[f64], metric_name: &str) -> String { + if values.is_empty() { + return format!("ts:{}|empty", metric_name); + } + + // Time header + let t0 = if !time_diff.is_empty() { + time_diff[0] + } else { + 0.0 + }; + let dt = if time_diff.len() >= 2 { + time_diff[1] - time_diff[0] + } else { + 1.0 + }; + + let mut parts = Vec::new(); + parts.push(format!( + "ts:{}|t0={:.0},dt={:.0}|v0={:.2}", + metric_name, t0, dt, values[0] + )); + + // Delta encode values, round to 2 decimal places + let mut deltas = Vec::with_capacity(values.len() - 1); + for i in 1..values.len() { + let d = ((values[i] - values[i - 1]) * 100.0).round() / 100.0; + if d == 0.0 { + deltas.push("0".to_string()); + } else { + deltas.push(format!("{:+.2}", d)); + } + } + + // Run-length encode repeated deltas + let mut rle: Vec = Vec::new(); + let mut i = 0; + while i < deltas.len() { + let mut count = 1; + while i + count < deltas.len() && deltas[i + count] == deltas[i] { + count += 1; + } + if count >= 3 { + rle.push(format!("{}x{}", deltas[i], count)); + } else { + for _ in 0..count { + rle.push(deltas[i].clone()); + } + } + i += count; + } + + parts.push(format!("d:{}", rle.join(","))); + parts.join("|") +} + +// --------------------------------------------------------------------------- +// Output format: downsampled (fixed buckets) +// --------------------------------------------------------------------------- + +fn build_downsampled(values: &[f64], time_diff: &[f64], num_buckets: usize) -> Value { + if values.is_empty() { + return json!({"buckets": []}); + } + + let n = values.len(); + let buckets = num_buckets.min(n); + let bucket_size = n as f64 / buckets as f64; + + let mut result = Vec::with_capacity(buckets); + for b in 0..buckets { + let start = (b as f64 * bucket_size) as usize; + let end = (((b + 1) as f64 * bucket_size) as usize).min(n); + let slice = &values[start..end]; + + let avg = slice.iter().sum::() / slice.len() as f64; + let min = slice.iter().cloned().fold(f64::INFINITY, f64::min); + let max = slice.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + + let t_start = if start < time_diff.len() { + time_diff[start] + } else { + start as f64 + }; + let t_end = if end > 0 && end - 1 < time_diff.len() { + time_diff[end - 1] + } else { + (end - 1) as f64 + }; + + result.push(json!({ + "t": format!("{:.0}-{:.0}s", t_start, t_end), + "avg": (avg * 100.0).round() / 100.0, + "min": (min * 100.0).round() / 100.0, + "max": (max * 100.0).round() / 100.0, + })); + } + + json!({"buckets": result, "bucket_count": buckets}) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Arg builders (extracted for testability) +// --------------------------------------------------------------------------- + +fn build_record_args(req: &RecordRequest) -> Vec { + let mut args: Vec = vec!["record".to_string()]; + if let Some(ref name) = req.run_name { + args.push("-r".to_string()); + args.push(name.clone()); + } + if let Some(interval) = req.interval { + args.push("-i".to_string()); + args.push(interval.to_string()); + } + if let Some(period) = req.period { + args.push("-p".to_string()); + args.push(period.to_string()); + } + if req.profile.unwrap_or(false) { + args.push("--profile".to_string()); + } + if let Some(freq) = req.perf_frequency { + args.push("-F".to_string()); + args.push(freq.to_string()); + } + if req.memory_allocation.unwrap_or(false) { + args.push("--memory-allocation".to_string()); + } + if let Some(ref dc) = req.dont_collect { + args.push("--dont-collect".to_string()); + args.push(dc.clone()); + } + if let Some(ref co) = req.collect_only { + args.push("--collect-only".to_string()); + args.push(co.clone()); + } + if let Some(ref java) = req.profile_java { + args.push("--profile-java".to_string()); + if !java.is_empty() { + args.push(java.clone()); + } + } + if let Some(ref pmu) = req.pmu_config { + args.push("--pmu-config".to_string()); + args.push(pmu.clone()); + } + args +} + +fn build_report_args(req: &GenerateReportRequest) -> Vec { + let mut args: Vec = vec!["report".to_string()]; + args.push("-r".to_string()); + for run in &req.runs { + args.push(run.clone()); + } + if let Some(ref name) = req.name { + args.push("-n".to_string()); + args.push(name.clone()); + } + args +} + +fn ok_json(value: Value) -> Result { + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string_pretty(&value).unwrap(), + )])) +} + +fn find_target_file( + data_dir: &std::path::Path, + file_name: Option<&str>, + category: Option<&str>, +) -> Result { + if let Some(fname) = file_name { + let path = data_dir.join(fname); + if path.exists() { + return Ok(path); + } + return Err(format!( + "Error: File '{}' not found in data directory.", + fname + )); + } + if let Some(cat) = category { + let path = data_dir.join(format!("{}.js", cat)); + if path.exists() { + return Ok(path); + } + // Try without underscores + if let Ok(entries) = std::fs::read_dir(data_dir) { + for entry in entries.flatten() { + let stem = entry + .path() + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + if stem == cat || stem.replace('_', "") == cat.replace('_', "") { + return Ok(entry.path()); + } + } + } + return Err(format!("Error: No file found for category '{}'.", cat)); + } + Err("Error: Must specify either 'file_name' or 'category'.".to_string()) +} + +// --------------------------------------------------------------------------- +// Response builders +// --------------------------------------------------------------------------- + +fn build_load_report_response(loaded: &LoadedReport) -> Value { + let meta = &loaded.metadata; + let mut msg = format!( + "Successfully loaded APerf report from: {}\n\n", + loaded.report_path + ); + + if let Some(ref runs) = meta.runs { + let count = runs.len(); + if count > 1 { + msg.push_str(&format!( + "Report Information: Multi-run ({} runs)\n\n", + count + )); + } else { + msg.push_str("Report Information: Single-run\n\n"); + } + + for (idx, run) in runs.iter().enumerate() { + let indent = if count > 1 { + msg.push_str(&format!(" Run {}: {}\n", idx + 1, run.run_id)); + " " + } else { + " " + }; + if let Some(ref v) = run.instance_type { + msg.push_str(&format!("{}Instance Type: {}\n", indent, v)); + } + if let Some(ref v) = run.cpu_count { + msg.push_str(&format!("{}CPUs: {}\n", indent, v)); + } + if let Some(ref v) = run.os { + msg.push_str(&format!("{}OS: {}\n", indent, v)); + } + if let Some(ref v) = run.kernel { + msg.push_str(&format!("{}Kernel: {}\n", indent, v)); + } + if let Some(ref v) = run.region { + msg.push_str(&format!("{}Region: {}\n", indent, v)); + } + if count > 1 { + msg.push('\n'); + } + } + } + + if !loaded.metrics.is_empty() { + msg.push_str(&format!( + "\nAvailable Metrics: {} metric files found\n", + loaded.metrics.len() + )); + } + + msg.push_str("\nReport validated and ready for analysis."); + + json!({ + "status": "success", + "message": msg.trim(), + "report_path": loaded.report_path, + "metadata": meta, + "metrics": loaded.metrics, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::server::mcp::js_parser; + use std::path::PathBuf; + + fn test_data_base() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/mcp_test_data") + } + + #[test] + fn test_find_target_file_by_category() { + let data_dir = test_data_base().join("metric_files"); + // metric_files has cpu_utilization.js + let result = find_target_file(&data_dir, None, Some("cpu_utilization")); + assert!(result.is_ok()); + assert!(result.unwrap().ends_with("cpu_utilization.js")); + } + + #[test] + fn test_find_target_file_by_filename() { + let data_dir = test_data_base().join("metric_files"); + let result = find_target_file(&data_dir, Some("cpu_utilization.js"), None); + assert!(result.is_ok()); + } + + #[test] + fn test_find_target_file_missing() { + let data_dir = test_data_base().join("metric_files"); + let result = find_target_file(&data_dir, Some("nonexistent.js"), None); + assert!(result.is_err()); + } + + #[test] + fn test_find_target_file_no_params() { + let data_dir = test_data_base().join("metric_files"); + let result = find_target_file(&data_dir, None, None); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Must specify")); + } + + #[test] + fn test_findings_extraction_two_run_report() { + // The two_run_report has findings in both systeminfo.js and cpu_utilization.js + let data_dir = test_data_base().join("two_run_report/data/js"); + if !data_dir.exists() { + return; + } + + let mut all_findings: Vec = Vec::new(); + let mut js_files: Vec<_> = std::fs::read_dir(&data_dir) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension() == Some(std::ffi::OsStr::new("js"))) + .collect(); + js_files.sort_by_key(|e| e.file_name()); + + for entry in &js_files { + let content = std::fs::read_to_string(entry.path()).unwrap(); + let findings_vars = js_parser::extract_findings_variables(&content); + for (category, json_str) in findings_vars { + let findings_data: Value = serde_json::from_str(&json_str).unwrap(); + if let Some(per_run) = findings_data + .get("per_run_findings") + .and_then(|v| v.as_object()) + { + for (run_id, run_data) in per_run { + if let Some(findings_dict) = + run_data.get("findings").and_then(|v| v.as_object()) + { + for (metric_name, finding_list) in findings_dict { + if let Some(arr) = finding_list.as_array() { + for finding in arr { + all_findings.push(json!({ + "category": category, + "run_id": run_id, + "metric_name": metric_name, + "rule_name": finding.get("rule_name").and_then(|v| v.as_str()).unwrap_or(""), + })); + } + } + } + } + } + } + } + } + + // two_run_report should have findings: kernel version mismatch + underutilized CPU + assert!( + all_findings.len() >= 2, + "Expected at least 2 findings, got {}", + all_findings.len() + ); + + // Check we have the kernel version mismatch + let has_kernel = all_findings.iter().any(|f| { + f.get("rule_name").and_then(|v| v.as_str()) == Some("Kernel Version Mismatch") + }); + assert!(has_kernel, "Expected Kernel Version Mismatch finding"); + + // Check we have underutilized CPU + let has_cpu = all_findings + .iter() + .any(|f| f.get("rule_name").and_then(|v| v.as_str()) == Some("Underutilized CPU")); + assert!(has_cpu, "Expected Underutilized CPU finding"); + } + + #[test] + fn test_findings_extraction_single_run_empty() { + // Single run report should have no findings + let data_dir = test_data_base().join("single_run_report/data/js"); + if !data_dir.exists() { + return; + } + + let mut all_findings: Vec = Vec::new(); + let js_files: Vec<_> = std::fs::read_dir(&data_dir) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension() == Some(std::ffi::OsStr::new("js"))) + .collect(); + + for entry in &js_files { + let content = std::fs::read_to_string(entry.path()).unwrap(); + let findings_vars = js_parser::extract_findings_variables(&content); + for (_category, json_str) in findings_vars { + let findings_data: Value = serde_json::from_str(&json_str).unwrap(); + if let Some(per_run) = findings_data + .get("per_run_findings") + .and_then(|v| v.as_object()) + { + for (_run_id, run_data) in per_run { + if let Some(findings_dict) = + run_data.get("findings").and_then(|v| v.as_object()) + { + for (_metric_name, finding_list) in findings_dict { + if let Some(arr) = finding_list.as_array() { + all_findings.extend(arr.iter().cloned()); + } + } + } + } + } + } + } + + assert_eq!( + all_findings.len(), + 0, + "Single-run report should have no findings" + ); + } + + #[test] + fn test_metric_values_timeseries_from_file() { + // Test reading time series data from cpu_utilization.js metric file + let data_dir = test_data_base().join("metric_files"); + let target = find_target_file(&data_dir, None, Some("cpu_utilization")).unwrap(); + let content = std::fs::read_to_string(&target).unwrap(); + let json_str = js_parser::extract_json_from_js(&content).unwrap(); + let data: Value = serde_json::from_str(&json_str).unwrap(); + + let runs = data.get("runs").unwrap().as_object().unwrap(); + assert!(runs.contains_key("test_run")); + + let run = &runs["test_run"]; + let metrics = run.get("metrics").unwrap().as_object().unwrap(); + assert!(metrics.contains_key("user")); + + let user = &metrics["user"]; + let stats = user.get("stats").unwrap(); + assert!(stats.get("avg").unwrap().as_f64().unwrap() > 0.0); + + let series = user.get("series").unwrap().as_array().unwrap(); + assert!(!series.is_empty()); + assert_eq!( + series[0].get("series_name").unwrap().as_str().unwrap(), + "CPU0" + ); + } + + #[test] + fn test_metric_values_keyvalue_from_file() { + // Test reading key-value data from systeminfo.js + let data_dir = test_data_base().join("single_run_report/data/js"); + if !data_dir.exists() { + return; + } + let target = find_target_file(&data_dir, Some("systeminfo.js"), None).unwrap(); + let content = std::fs::read_to_string(&target).unwrap(); + let json_str = js_parser::extract_json_from_js(&content).unwrap(); + let data: Value = serde_json::from_str(&json_str).unwrap(); + + let format = data.get("data_format").unwrap().as_str().unwrap(); + assert_eq!(format, "key_value"); + + let runs = data.get("runs").unwrap().as_object().unwrap(); + let run = runs.values().next().unwrap(); + let kv_groups = run.get("key_value_groups").unwrap().as_object().unwrap(); + let default_group = kv_groups.get("").unwrap(); + let kvs = default_group + .get("key_values") + .unwrap() + .as_object() + .unwrap(); + assert_eq!( + kvs.get("Instance Type").unwrap().as_str().unwrap(), + "m8g.24xlarge" + ); + } + + #[test] + fn test_load_processed_data_timeseries() { + // Verify that load_processed_data correctly deserializes a time-series JS file + // into ProcessedData and that ProcessedDataAccessor can access it. + let data_dir = test_data_base().join("metric_files"); + let target = find_target_file(&data_dir, None, Some("cpu_utilization")).unwrap(); + let pd = load_processed_data(&target).unwrap(); + + assert_eq!(pd.data_name, "cpu_utilization"); + assert!(matches!(pd.data_format, DataFormat::TimeSeries)); + assert!(pd.runs.contains_key("test_run")); + + // Use ProcessedDataAccessor to get stats + let mut accessor = ProcessedDataAccessor::new(); + let stats = accessor + .time_series_metric_stats(&pd, "test_run", "user") + .unwrap(); + assert!(stats.avg > 0.0); + assert!(stats.max >= stats.avg); + assert!(stats.min <= stats.avg); + } + + #[test] + fn test_load_processed_data_keyvalue() { + // Verify that load_processed_data correctly deserializes a key-value JS file + let data_dir = test_data_base().join("single_run_report/data/js"); + if !data_dir.exists() { + return; + } + let target = find_target_file(&data_dir, Some("systeminfo.js"), None).unwrap(); + let pd = load_processed_data(&target).unwrap(); + + assert_eq!(pd.data_format as u8, DataFormat::KeyValue as u8); + + // Use ProcessedDataAccessor to get a key-value + let accessor = ProcessedDataAccessor::new(); + let run_name = pd.runs.keys().next().unwrap(); + let instance_type = accessor.key_value_value_by_key(&pd, run_name, "Instance Type"); + assert_eq!(instance_type, Some("m8g.24xlarge")); + } + + #[test] + fn test_load_processed_data_with_time_range() { + // Verify that ProcessedDataAccessor respects time ranges + let data_dir = test_data_base().join("metric_files"); + let target = find_target_file(&data_dir, None, Some("cpu_utilization")).unwrap(); + let pd = load_processed_data(&target).unwrap(); + + // Full range stats + let mut full_accessor = ProcessedDataAccessor::new(); + let full_stats = full_accessor + .time_series_metric_stats(&pd, "test_run", "user") + .unwrap(); + + // Restricted range — from_time=5 should give fewer or different stats + let mut ranged_accessor = ProcessedDataAccessor::from_time_ranges( + std::collections::HashMap::from([("test_run".to_string(), 5)]), + std::collections::HashMap::new(), + std::collections::HashMap::new(), + std::collections::HashMap::new(), + ); + let ranged_stats = ranged_accessor + .time_series_metric_stats(&pd, "test_run", "user") + .unwrap(); + + // The ranged stats should differ from full (unless all data is after t=5) + // At minimum, both should be valid + assert!(ranged_stats.avg >= 0.0); + assert!(full_stats.avg >= 0.0); + } + + #[test] + fn test_cpu_ids_filtering() { + // cpu_utilization.js in metric_files has series with series_name="CPU0" + let data_dir = test_data_base().join("metric_files"); + let target = find_target_file(&data_dir, None, Some("cpu_utilization")).unwrap(); + let content = std::fs::read_to_string(&target).unwrap(); + let json_str = js_parser::extract_json_from_js(&content).unwrap(); + let data: Value = serde_json::from_str(&json_str).unwrap(); + + let runs = data.get("runs").unwrap().as_object().unwrap(); + let run = runs.values().next().unwrap(); + let metrics = run.get("metrics").unwrap().as_object().unwrap(); + let user = &metrics["user"]; + let series = user.get("series").unwrap().as_array().unwrap(); + + // Filter to CPU0 — should match + let filtered: Vec<&Value> = series + .iter() + .filter(|s| s.get("series_name").and_then(|v| v.as_str()) == Some("CPU0")) + .collect(); + assert_eq!(filtered.len(), 1); + assert_eq!( + filtered[0].get("series_name").unwrap().as_str().unwrap(), + "CPU0" + ); + + // Filter to CPU99 — should be empty + let filtered_miss: Vec<&Value> = series + .iter() + .filter(|s| s.get("series_name").and_then(|v| v.as_str()) == Some("CPU99")) + .collect(); + assert!(filtered_miss.is_empty()); + } + + #[test] + fn test_cpu_averaging() { + // Build synthetic multi-CPU data and verify averaging + let series = vec![ + json!({"series_name": "CPU0", "time_diff": [0, 1, 2], "values": [10.0, 20.0, 30.0]}), + json!({"series_name": "CPU1", "time_diff": [0, 1, 2], "values": [20.0, 40.0, 60.0]}), + ]; + + let all_values: Vec> = series + .iter() + .filter_map(|s| { + s.get("values") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|x| x.as_f64()).collect()) + }) + .collect(); + + let num_points = all_values[0].len(); + let mut averaged = Vec::with_capacity(num_points); + for i in 0..num_points { + let mut sum = 0.0; + let mut count = 0usize; + for vals in &all_values { + if i < vals.len() { + sum += vals[i]; + count += 1; + } + } + averaged.push(if count > 0 { sum / count as f64 } else { 0.0 }); + } + + assert_eq!(averaged.len(), 3); + assert!((averaged[0] - 15.0).abs() < 0.001); // (10+20)/2 + assert!((averaged[1] - 30.0).abs() < 0.001); // (20+40)/2 + assert!((averaged[2] - 45.0).abs() < 0.001); // (30+60)/2 + } + + #[test] + fn test_build_timeseries_summary() { + let values = vec![10.0, 12.0, 11.0, 50.0, 48.0, 11.0, 10.0, 10.5, 11.0, 10.0]; + let time_diff: Vec = (0..10).map(|x| x as f64).collect(); + let stats = json!({"avg": 18.35, "min": 10.0, "max": 50.0, "std": 15.0, "p50": 11.0, "p90": 48.0, "p99": 50.0}); + let summary = build_timeseries_summary(&values, &time_diff, &stats); + assert!(summary.contains("points=10")); + assert!(summary.contains("avg=18.35")); + assert!(summary.contains("p50=11.00")); + assert!(summary.contains("p90=48.00")); + assert!(summary.contains("p99=50.00")); + assert!(summary.contains("trend")); + } + + #[test] + fn test_build_timeseries_summary_with_drops() { + // avg=50, std=10 → drop_threshold=30. Values 5.0 at t=3-4 should be detected. + let values = vec![50.0, 52.0, 48.0, 5.0, 5.0, 51.0, 49.0, 50.0, 53.0, 48.0]; + let time_diff: Vec = (0..10).map(|x| x as f64).collect(); + let stats = json!({"avg": 41.1, "min": 5.0, "max": 53.0, "std": 17.0, "p50": 49.0, "p90": 52.0, "p99": 53.0}); + let summary = build_timeseries_summary(&values, &time_diff, &stats); + assert!( + summary.contains("drop"), + "Expected drop detection in: {}", + summary + ); + assert!( + summary.contains("trough"), + "Expected trough value in: {}", + summary + ); + } + + #[test] + fn test_build_compact_notation() { + let values = vec![10.0, 10.5, 11.0, 11.0, 11.0, 11.0, 12.0]; + let time_diff: Vec = (0..7).map(|x| x as f64).collect(); + let compact = build_compact_notation(&values, &time_diff, "test_metric"); + assert!(compact.starts_with("ts:test_metric|")); + assert!(compact.contains("v0=10.00")); + // Should have RLE for the repeated 0 deltas + assert!(compact.contains("0x")); + } + + #[test] + fn test_build_downsampled() { + let values: Vec = (0..100).map(|x| x as f64).collect(); + let time_diff: Vec = (0..100).map(|x| x as f64).collect(); + let ds = build_downsampled(&values, &time_diff, 10); + let buckets = ds.get("buckets").unwrap().as_array().unwrap(); + assert_eq!(buckets.len(), 10); + // First bucket: values 0-9, avg=4.5 + let first = &buckets[0]; + assert!((first.get("avg").unwrap().as_f64().unwrap() - 4.5).abs() < 0.01); + assert_eq!(ds.get("bucket_count").unwrap().as_u64().unwrap(), 10); + } + + #[test] + fn test_build_compact_empty() { + let compact = build_compact_notation(&[], &[], "empty"); + assert_eq!(compact, "ts:empty|empty"); + } + + #[test] + fn test_build_downsampled_small() { + // Fewer points than buckets + let values = vec![1.0, 2.0, 3.0]; + let time_diff = vec![0.0, 1.0, 2.0]; + let ds = build_downsampled(&values, &time_diff, 50); + let buckets = ds.get("buckets").unwrap().as_array().unwrap(); + assert_eq!(buckets.len(), 3); // capped to data size + } + + // ----------------------------------------------------------------------- + // record / generate_report arg building tests + // ----------------------------------------------------------------------- + + #[test] + fn test_build_record_args_defaults() { + let req = RecordRequest { + run_name: None, + interval: None, + period: None, + profile: None, + perf_frequency: None, + memory_allocation: None, + dont_collect: None, + collect_only: None, + profile_java: None, + pmu_config: None, + }; + let args = build_record_args(&req); + assert_eq!(args, vec!["record"]); + } + + #[test] + fn test_build_record_args_full() { + let req = RecordRequest { + run_name: Some("my_run".to_string()), + interval: Some(2), + period: Some(60), + profile: Some(true), + perf_frequency: Some(199), + memory_allocation: Some(true), + dont_collect: Some("interrupts,netstat".to_string()), + collect_only: None, + profile_java: None, + pmu_config: Some("/path/to/pmu.json".to_string()), + }; + let args = build_record_args(&req); + assert!(args.contains(&"record".to_string())); + assert!(args.contains(&"-r".to_string())); + assert!(args.contains(&"my_run".to_string())); + assert!(args.contains(&"-i".to_string())); + assert!(args.contains(&"2".to_string())); + assert!(args.contains(&"-p".to_string())); + assert!(args.contains(&"60".to_string())); + assert!(args.contains(&"--profile".to_string())); + assert!(args.contains(&"-F".to_string())); + assert!(args.contains(&"199".to_string())); + assert!(args.contains(&"--memory-allocation".to_string())); + assert!(args.contains(&"--dont-collect".to_string())); + assert!(args.contains(&"interrupts,netstat".to_string())); + assert!(args.contains(&"--pmu-config".to_string())); + assert!(args.contains(&"/path/to/pmu.json".to_string())); + } + + #[test] + fn test_build_record_args_collect_only() { + let req = RecordRequest { + run_name: Some("test".to_string()), + interval: None, + period: Some(10), + profile: None, + perf_frequency: None, + memory_allocation: None, + dont_collect: None, + collect_only: Some("cpu_utilization,meminfo".to_string()), + profile_java: None, + pmu_config: None, + }; + let args = build_record_args(&req); + assert!(args.contains(&"--collect-only".to_string())); + assert!(args.contains(&"cpu_utilization,meminfo".to_string())); + assert!(!args.contains(&"--dont-collect".to_string())); + } + + #[test] + fn test_build_record_args_java_profiling() { + let req = RecordRequest { + run_name: None, + interval: None, + period: None, + profile: None, + perf_frequency: None, + memory_allocation: None, + dont_collect: None, + collect_only: None, + profile_java: Some("".to_string()), // empty = profile all JVMs + pmu_config: None, + }; + let args = build_record_args(&req); + assert!(args.contains(&"--profile-java".to_string())); + // Empty string means no PID arg, just the flag + assert_eq!(args.len(), 2); // ["record", "--profile-java"] + } + + #[test] + fn test_build_record_args_java_profiling_with_pids() { + let req = RecordRequest { + run_name: None, + interval: None, + period: None, + profile: None, + perf_frequency: None, + memory_allocation: None, + dont_collect: None, + collect_only: None, + profile_java: Some("1234,myapp".to_string()), + pmu_config: None, + }; + let args = build_record_args(&req); + assert!(args.contains(&"--profile-java".to_string())); + assert!(args.contains(&"1234,myapp".to_string())); + } + + #[test] + fn test_build_report_args_single_run() { + let req = GenerateReportRequest { + runs: vec!["/path/to/run1".to_string()], + name: Some("my_report".to_string()), + }; + let args = build_report_args(&req); + assert_eq!( + args, + vec!["report", "-r", "/path/to/run1", "-n", "my_report"] + ); + } + + #[test] + fn test_build_report_args_multi_run() { + let req = GenerateReportRequest { + runs: vec![ + "/path/to/run1".to_string(), + "/path/to/run2".to_string(), + "/path/to/run3".to_string(), + ], + name: None, + }; + let args = build_report_args(&req); + assert_eq!( + args, + vec![ + "report", + "-r", + "/path/to/run1", + "/path/to/run2", + "/path/to/run3" + ] + ); + } + + #[test] + fn test_build_report_args_no_name() { + let req = GenerateReportRequest { + runs: vec!["/path/to/run1".to_string()], + name: None, + }; + let args = build_report_args(&req); + assert_eq!(args, vec!["report", "-r", "/path/to/run1"]); + assert!(!args.contains(&"-n".to_string())); + } + + #[test] + fn test_current_exe_resolves() { + // Verify that current_exe() works in the test environment + let exe = std::env::current_exe(); + assert!(exe.is_ok()); + assert!(exe.unwrap().exists()); + } + + #[test] + fn test_parse_flamegraph_svg() { + // Test parsing the flamegraph SVG in tmp/ if it exists + let svg_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tmp/aperf_record_m8g24xlarge_be57e2e2-acae-4a0a-8a02-e743489c74c6_20260319T120630-flamegraph.svg"); + if !svg_path.exists() { + return; + } + + let frames = parse_flamegraph_svg(&svg_path).unwrap(); + assert!(!frames.is_empty()); + + // Should have parsed many frames + assert!( + frames.len() > 50, + "Expected >50 frames, got {}", + frames.len() + ); + + // Check a known function exists (from our earlier grep) + let has_postgres = frames.iter().any(|f| f.name == "PostgresMain"); + assert!(has_postgres, "Expected PostgresMain in flamegraph"); + + // Check percentages are reasonable + let max_pct = frames.iter().map(|f| f.pct).fold(0.0f64, f64::max); + assert!( + max_pct > 50.0, + "Expected top function >50%, got {}", + max_pct + ); + + // Test annotated frames with parent + let annotated = build_annotated_frames(&frames, 1.0); + assert!(!annotated.is_empty()); + + // Top function should have a parent + let top = &annotated[0]; + assert!(!top.parent.is_empty()); + assert!(top.pct > 10.0); + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 00000000..1d2ec10a --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,25 @@ +//! Server module for APerf — exposes report analysis over various protocols. +//! +//! Currently supports MCP (Model Context Protocol) over stdio. + +pub mod mcp; + +use anyhow::Result; +use clap::Args; + +/// CLI arguments for the `aperf server` subcommand. +#[derive(Args, Debug)] +pub struct Server { + /// Start an MCP (Model Context Protocol) server over stdio. + #[clap(long)] + pub mcp: bool, +} + +/// Run the server based on CLI flags. +pub fn run_server(args: &Server) -> Result<()> { + if args.mcp { + return mcp::run_mcp_server(); + } + + anyhow::bail!("No server type specified. Use --mcp to start an MCP server.") +} diff --git a/tests/mcp_test_data/metric_files/cpu_utilization.js b/tests/mcp_test_data/metric_files/cpu_utilization.js new file mode 100644 index 00000000..b112cba6 --- /dev/null +++ b/tests/mcp_test_data/metric_files/cpu_utilization.js @@ -0,0 +1 @@ +processed_cpu_utilization_data = {"data_name":"cpu_utilization","data_format":"time_series","runs":{"test_run":{"metrics":{"nice":{"metric_name":"nice","series":[{"series_name":"CPU0","time_diff":[0,1,2],"values":[0.0,0.0,0.0]}],"value_range":[0,100],"stats":{"avg":0.0,"std":0.0,"min":0.0,"max":0.0}},"softirq":{"metric_name":"softirq","series":[{"series_name":"CPU0","time_diff":[0,1,2],"values":[0.0,1.03,0.0]}],"value_range":[0,100],"stats":{"avg":0.34,"std":0.59,"min":0.0,"max":1.03}},"user":{"metric_name":"user","series":[{"series_name":"CPU0","time_diff":[0,1,2],"values":[5.1,4.9,5.2]}],"value_range":[0,100],"stats":{"avg":5.07,"std":0.15,"min":4.9,"max":5.2}}}}}} diff --git a/tests/mcp_test_data/metric_files/meminfo.js b/tests/mcp_test_data/metric_files/meminfo.js new file mode 100644 index 00000000..2a7d46b4 --- /dev/null +++ b/tests/mcp_test_data/metric_files/meminfo.js @@ -0,0 +1 @@ +processed_meminfo_data = {"data_name":"meminfo","data_format":"time_series","runs":{"test_run":{"metrics":{"mem_total":{"metric_name":"mem_total","series":[{"time_diff":[0,1,2],"values":[131702784.0,131702784.0,131702784.0]}],"value_range":[131702784,131702784],"stats":{"avg":131702784.0,"std":0.0,"min":131702784.0,"max":131702784.0}},"mem_free":{"metric_name":"mem_free","series":[{"time_diff":[0,1,2],"values":[120000000.0,119500000.0,119000000.0]}],"value_range":[119000000,120000000],"stats":{"avg":119500000.0,"std":500000.0,"min":119000000.0,"max":120000000.0}}}}}} diff --git a/tests/mcp_test_data/metric_files/perf_stat.js b/tests/mcp_test_data/metric_files/perf_stat.js new file mode 100644 index 00000000..fd0e77ba --- /dev/null +++ b/tests/mcp_test_data/metric_files/perf_stat.js @@ -0,0 +1 @@ +processed_perf_stat_data = {"data_name":"perf_stat","data_format":"time_series","runs":{"test_run":{"metrics":{"instructions":{"metric_name":"instructions","series":[{"series_name":"CPU0","time_diff":[0,1,2],"values":[1000000.0,1050000.0,1100000.0]}],"value_range":[1000000,1100000],"stats":{"avg":1050000.0,"std":50000.0,"min":1000000.0,"max":1100000.0}},"cycles":{"metric_name":"cycles","series":[{"series_name":"CPU0","time_diff":[0,1,2],"values":[2000000.0,2100000.0,2200000.0]}],"value_range":[2000000,2200000],"stats":{"avg":2100000.0,"std":100000.0,"min":2000000.0,"max":2200000.0}},"cache_misses":{"metric_name":"cache_misses","series":[{"series_name":"CPU0","time_diff":[0,1,2],"values":[5000.0,5200.0,5100.0]}],"value_range":[5000,5200],"stats":{"avg":5100.0,"std":100.0,"min":5000.0,"max":5200.0}}}}}} diff --git a/tests/mcp_test_data/single_run_report/data/js/cpu_utilization.js b/tests/mcp_test_data/single_run_report/data/js/cpu_utilization.js new file mode 100644 index 00000000..5dd87875 --- /dev/null +++ b/tests/mcp_test_data/single_run_report/data/js/cpu_utilization.js @@ -0,0 +1 @@ +cpu_utilization_findings = {"per_run_findings":{}} diff --git a/tests/mcp_test_data/single_run_report/data/js/runs.js b/tests/mcp_test_data/single_run_report/data/js/runs.js new file mode 100644 index 00000000..c6ad71b5 --- /dev/null +++ b/tests/mcp_test_data/single_run_report/data/js/runs.js @@ -0,0 +1 @@ +runs_raw = ["m8g24xlarge"] diff --git a/tests/mcp_test_data/single_run_report/data/js/systeminfo.js b/tests/mcp_test_data/single_run_report/data/js/systeminfo.js new file mode 100644 index 00000000..7b667208 --- /dev/null +++ b/tests/mcp_test_data/single_run_report/data/js/systeminfo.js @@ -0,0 +1,3 @@ +processed_systeminfo_data = {"data_name":"systeminfo","data_format":"key_value","runs":{"m8g24xlarge":{"key_value_groups":{"":{"key_values":{"OS Version":"2023","Kernel Version":"6.1.158-180.294.amzn2023.aarch64","Instance Type":"m8g.24xlarge","AMI ID":"ami-0720c0a2e1e125edd","Local Hostname":"ip-172-31-1-1.ec2.internal","CPUs":"96","Instance ID":"i-0123456789abcdef0","Hostname":"ip-172-31-1-1.ec2.internal","System Name":"Amazon Linux","Region":"us-east-1"}}}}}} + +systeminfo_findings = {"per_run_findings":{}} diff --git a/tests/mcp_test_data/two_run_report/data/js/cpu_utilization.js b/tests/mcp_test_data/two_run_report/data/js/cpu_utilization.js new file mode 100644 index 00000000..e3848d23 --- /dev/null +++ b/tests/mcp_test_data/two_run_report/data/js/cpu_utilization.js @@ -0,0 +1 @@ +cpu_utilization_findings = {"per_run_findings":{"c6a_soap":{"findings":{"idle":[{"rule_name":"Underutilized CPU","score":-381.78665685494724,"description":"The average in c6a_soap is 74.56.","message":"CPU utilization might not be maximized.","reference":""}]}},"c7g_soap":{"findings":{"idle":[{"rule_name":"Underutilized CPU","score":-387.71635007453995,"description":"The average in c7g_soap is 75.72.","message":"CPU utilization might not be maximized.","reference":""}]}}}} diff --git a/tests/mcp_test_data/two_run_report/data/js/runs.js b/tests/mcp_test_data/two_run_report/data/js/runs.js new file mode 100644 index 00000000..47aefabf --- /dev/null +++ b/tests/mcp_test_data/two_run_report/data/js/runs.js @@ -0,0 +1 @@ +runs_raw = ["c6a_soap","c7g_soap"] diff --git a/tests/mcp_test_data/two_run_report/data/js/systeminfo.js b/tests/mcp_test_data/two_run_report/data/js/systeminfo.js new file mode 100644 index 00000000..a77ced6b --- /dev/null +++ b/tests/mcp_test_data/two_run_report/data/js/systeminfo.js @@ -0,0 +1,3 @@ +processed_systeminfo_data = {"data_name":"systeminfo","data_format":"key_value","runs":{"c6a_soap":{"key_value_groups":{"":{"key_values":{"OS Version":"2023","Kernel Version":"6.1.158-180.294.amzn2023.x86_64","Instance Type":"c6a.12xlarge","AMI ID":"ami-068c0051b15cdb816","Local Hostname":"ip-172-31-95-106.ec2.internal","CPUs":"48","Instance ID":"i-09157d38182adf3eb","Hostname":"ip-172-31-95-106.ec2.internal","System Name":"Amazon Linux","Region":"us-east-1"}}}},"c7g_soap":{"key_value_groups":{"":{"key_values":{"Region":"us-east-1","System Name":"Amazon Linux","Hostname":"ip-172-31-35-2.ec2.internal","AMI ID":"ami-0720c0a2e1e125edd","OS Version":"2023","Kernel Version":"6.1.158-180.294.amzn2023.aarch64","Instance Type":"c7g.12xlarge","Local Hostname":"ip-172-31-35-2.ec2.internal","CPUs":"48","Instance ID":"i-060c97a3c29b9c957"}}}}}} + +systeminfo_findings = {"per_run_findings":{"c7g_soap":{"findings":{"Kernel Version":[{"rule_name":"Kernel Version Mismatch","score":-256.0,"description":"The value of Kernel Version in c7g_soap (\"6.1.158-180.294.amzn2023.aarch64\") is different from c6a_soap (\"6.1.158-180.294.amzn2023.x86_64\").","message":"Kernel Versions between runs are different. Make sure the difference is intended.","reference":""}]}}}}