diff --git a/Cargo.lock b/Cargo.lock index 4949682e..ee1faed0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +20,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -35,6 +56,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arrayref" version = "0.3.9" @@ -61,6 +88,17 @@ dependencies = [ "wait-timeout", ] +[[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.109", +] + [[package]] name = "atty" version = "0.2.14" @@ -108,7 +146,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.10.5", "log", "prettyplease", "proc-macro2", @@ -137,6 +175,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "blake3" version = "0.3.8" @@ -185,9 +232,12 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "byteorder" @@ -197,9 +247,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytesize" @@ -207,6 +257,84 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.2", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.2", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.5", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.2", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.2", + "winx", +] + [[package]] name = "cc" version = "1.2.44" @@ -214,6 +342,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -277,6 +407,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -311,6 +450,15 @@ dependencies = [ "winapi 0.2.8", ] +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if 1.0.4", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -320,6 +468,157 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck 0.5.0", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" + +[[package]] +name = "cranelift-control" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" + +[[package]] +name = "cranelift-native" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -461,6 +760,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "deranged" version = "0.5.5" @@ -552,6 +860,16 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.4", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "3.0.2" @@ -572,6 +890,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -616,6 +945,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -694,6 +1035,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if 1.0.4", + "rustix 1.1.2", + "windows-sys 0.59.0", +] + [[package]] name = "filetime" version = "0.2.26" @@ -712,6 +1064,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "float-cmp" version = "0.8.0" @@ -727,6 +1085,12 @@ 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 = "foldhash" version = "0.2.0" @@ -757,6 +1121,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.2", + "windows-sys 0.59.0", +] + [[package]] name = "futf" version = "0.1.5" @@ -767,51 +1142,67 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[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", ] [[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-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[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-sink", "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -824,6 +1215,20 @@ dependencies = [ "byteorder", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.10.0", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -877,6 +1282,18 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -904,11 +1321,32 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "foldhash 0.2.0", "serde", "serde_core", ] @@ -1171,6 +1609,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1198,6 +1642,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1205,11 +1663,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.0", "serde", "serde_core", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipnet" version = "2.11.0" @@ -1225,6 +1699,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1238,7 +1721,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a5c0b993601cad796222ea076565c5d9f337d35592f8622c753724f06d7271" dependencies = [ "anyhow", - "ittapi-sys", + "ittapi-sys 0.3.5", + "log", +] + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys 0.4.0", "log", ] @@ -1251,6 +1745,25 @@ dependencies = [ "cc", ] +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -1277,6 +1790,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1319,6 +1838,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.10" @@ -1330,6 +1855,12 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1363,6 +1894,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.35.0" @@ -1401,12 +1941,27 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.2", +] + [[package]] name = "mime" version = "0.3.17" @@ -1577,6 +2132,18 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1699,6 +2266,16 @@ dependencies = [ "libc", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.11.3" @@ -1757,12 +2334,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.32" @@ -1797,6 +2368,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1858,7 +2441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", - "itertools", + "itertools 0.10.5", "predicates-core", ] @@ -1931,6 +2514,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1969,7 +2575,7 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", "rand_pcg", @@ -1981,6 +2587,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -1994,6 +2602,16 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2008,6 +2626,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] [[package]] name = "rand_hc" @@ -2027,6 +2648,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -2070,7 +2700,21 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.0", + "log", + "rustc-hash", + "smallvec", ] [[package]] @@ -2148,6 +2792,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2163,6 +2813,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno 0.3.14", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.2" @@ -2172,10 +2835,20 @@ dependencies = [ "bitflags 2.10.0", "errno 0.3.14", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.2", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2274,6 +2947,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2318,6 +2995,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2330,6 +3016,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "servo_arc" version = "0.4.1" @@ -2375,7 +3074,7 @@ dependencies = [ "behrens-fisher", "serde", "sightglass-data", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2394,9 +3093,9 @@ dependencies = [ "serde", "serde_json", "tar", - "thiserror", + "thiserror 1.0.69", "wasmparser 0.247.0", - "wasmprinter", + "wasmprinter 0.2.80", ] [[package]] @@ -2425,8 +3124,12 @@ dependencies = [ "sightglass-upload", "structopt", "tempfile", - "thiserror", + "thiserror 1.0.69", "vega_lite_4", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wasmtime", + "wasmtime-wasi", ] [[package]] @@ -2462,7 +3165,7 @@ dependencies = [ "anyhow", "core_affinity", "hwloc", - "ittapi", + "ittapi 0.3.5", "lazy_static", "libc", "libloading 0.9.0", @@ -2473,7 +3176,7 @@ dependencies = [ "serde", "sightglass-build", "sightglass-data", - "thiserror", + "thiserror 1.0.69", "valgrind-requests", "wat", ] @@ -2498,6 +3201,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.11" @@ -2509,6 +3222,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2708,6 +3424,22 @@ dependencies = [ "libc", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags 2.10.0", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + [[package]] name = "tar" version = "0.4.44" @@ -2719,6 +3451,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.23.0" @@ -2728,7 +3466,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.2", "windows-sys 0.61.2", ] @@ -2773,7 +3511,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2787,6 +3534,17 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "time" version = "0.3.44" @@ -2866,32 +3624,83 @@ dependencies = [ ] [[package]] -name = "tower-service" -version = "0.3.3" +name = "toml" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] [[package]] -name = "tracing" -version = "0.1.41" +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "pin-project-lite", - "tracing-core", + "serde_core", ] [[package]] -name = "tracing-core" -version = "0.1.34" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "once_cell", + "winnow 1.0.3", ] [[package]] -name = "try-lock" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" @@ -2926,6 +3735,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.5.7" @@ -2950,6 +3771,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valgrind-requests" version = "1.1.0" @@ -3104,14 +3935,55 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-compose" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +dependencies = [ + "anyhow", + "heck 0.5.0", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wat", +] + [[package]] name = "wasm-encoder" -version = "0.240.0" +version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d642d8c5ecc083aafe9ceb32809276a304547a3a6eeecceb5d8152598bc71f" +checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" dependencies = [ "leb128fmt", - "wasmparser 0.240.0", + "wasmparser 0.243.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +dependencies = [ + "leb128fmt", + "wasmparser 0.251.0", ] [[package]] @@ -3127,13 +3999,28 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.240.0" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ "bitflags 2.10.0", + "hashbrown 0.16.1", "indexmap", "semver", + "serde", ] [[package]] @@ -3143,12 +4030,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" dependencies = [ "bitflags 2.10.0", - "hashbrown", + "hashbrown 0.17.0", "indexmap", "semver", "serde", ] +[[package]] +name = "wasmparser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +dependencies = [ + "bitflags 2.10.0", + "indexmap", + "semver", +] + [[package]] name = "wasmprinter" version = "0.2.80" @@ -3159,26 +4057,346 @@ dependencies = [ "wasmparser 0.121.2", ] +[[package]] +name = "wasmprinter" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmtime" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" +dependencies = [ + "addr2line", + "async-trait", + "bitflags 2.10.0", + "bumpalo", + "cc", + "cfg-if 1.0.4", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi 0.4.0", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.2", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmprinter 0.245.1", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix 1.1.2", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5ec9fff073ff13b81732d56a9515d761c245750bcda09093827f84130ebc25" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.109", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" +dependencies = [ + "cfg-if 1.0.4", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" +dependencies = [ + "cc", + "cfg-if 1.0.4", + "libc", + "rustix 1.1.2", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" +dependencies = [ + "cc", + "object", + "rustix 1.1.2", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" +dependencies = [ + "cfg-if 1.0.4", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f599b79545e3bba0b7913406055ebede5bb0dabee9ba2015ef25a9f4c9f47807" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "heck 0.5.0", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c7daf53ba2f64aa089f47d9a54bec654a45b7b1b55660efecfb09a2e6cfbcf" +dependencies = [ + "async-trait", + "bitflags 2.10.0", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.1.2", + "system-interface", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c9c6cd3daf62a4fb75ac4742c976fee1939686ffe461a366ce6446c58a58a0" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + [[package]] name = "wast" -version = "240.0.0" +version = "251.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0efe1c93db4ac562b9733e3dca19ed7fc878dba29aef22245acf84f13da4a19" +checksum = "5cc7467dda0a96142eb2c980329dfb62480b1e1d3622fdeb1a44e2bca6ceed74" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder", + "wasm-encoder 0.251.0", ] [[package]] name = "wat" -version = "1.240.0" +version = "1.251.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec9b6eab7ecd4d639d78515e9ea491c9bacf494aa5eda10823bd35992cf8c1e" +checksum = "81b1086c9e85b95bd6a229a928bc6c6d0662e42af0250c88d067b418831ea4d4" dependencies = [ - "wast", + "wast 251.0.0", ] [[package]] @@ -3203,6 +4421,46 @@ dependencies = [ "string_cache_codegen", ] +[[package]] +name = "wiggle" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cfd3db2f05619c6f36f257d84327c11546e28d61e3a1c1220aaad553bc4b0" +dependencies = [ + "bitflags 2.10.0", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd7a197903e5b4ff5e13aef9c891960d71e92073600ecf4c86c7e795ac1c803" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.109", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6410b86fcec207070d9372b215d3470bad67215e6bbac46981a16999c4abbc28" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.2.8" @@ -3246,6 +4504,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dbb0cf07b0dfe7b7a1ca8efb8f94ba98bd0fb144c411ea1665c78f0449e958" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3536,6 +4813,18 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + [[package]] name = "winreg" version = "0.50.0" @@ -3546,12 +4835,53 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.10.0", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + [[package]] name = "writeable" version = "0.6.2" @@ -3565,7 +4895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.2", ] [[package]] @@ -3664,3 +4994,31 @@ dependencies = [ "quote", "syn 2.0.109", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 1b765338..9f1333ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,10 @@ exclude = [ "benchmarks/tract-onnx-image-classification", "engines/native/libengine", ] + +# Always build Wasmtime and Cranelift in release mode to avoid slow Wasm +# compilation when running tests. +[profile.dev.package.cranelift-codegen] +opt-level = 3 +[profile.dev.package.wasmtime] +opt-level = 3 diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a5a70c67..69e95cea 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -29,6 +29,10 @@ regex = "1.5.4" vega_lite_4 = { git = "https://github.com/procyon-rs/vega_lite_4.rs" } minijinja = "2.10" tempfile = "3.2.0" +wasmparser = "0.243.0" +wasm-encoder = { version = "0.243", features = ["wasmparser"] } +wasmtime = { version = "43", features = ["anyhow"] } +wasmtime-wasi = "43" [dev-dependencies] assert_cmd = "1.0.4" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a0db4721..bb13bb58 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,6 +2,7 @@ mod benchmark; mod clean; mod effect_size; mod fingerprint; +mod pca_metrics; mod report; mod suite; mod summarize; @@ -14,6 +15,7 @@ use clean::CleanCommand; use effect_size::EffectSizeCommand; use fingerprint::FingerprintCommand; use log::trace; +use pca_metrics::PcaMetricsCommand; use report::ReportCommand; use structopt::{clap::AppSettings, StructOpt}; use summarize::SummarizeCommand; @@ -42,6 +44,7 @@ enum SightglassCommand { Clean(CleanCommand), EffectSize(EffectSizeCommand), Fingerprint(FingerprintCommand), + PcaMetrics(PcaMetricsCommand), Report(ReportCommand), Summarize(SummarizeCommand), UploadElastic(UploadCommand), @@ -56,6 +59,7 @@ impl SightglassCommand { SightglassCommand::Clean(clean) => clean.execute(), SightglassCommand::EffectSize(effect_size) => effect_size.execute(), SightglassCommand::Fingerprint(fingerprint) => fingerprint.execute(), + SightglassCommand::PcaMetrics(metrics) => metrics.execute(), SightglassCommand::Report(report) => report.execute(), SightglassCommand::Summarize(summarize) => summarize.execute(), SightglassCommand::UploadElastic(upload) => upload.execute(), diff --git a/crates/cli/src/pca_metrics.rs b/crates/cli/src/pca_metrics.rs new file mode 100644 index 00000000..42fd81fc --- /dev/null +++ b/crates/cli/src/pca_metrics.rs @@ -0,0 +1,447 @@ +//! Capture benchmark metrics for principal component analysis (PCA). + +mod category; +mod dynamic_metrics; +mod static_metrics; + +use crate::suite::BenchmarkOrSuite; +use anyhow::{Context, Result}; +use category::{Category, NUM_CATEGORIES}; +use dynamic_metrics::{dynamic_metrics, make_engine}; +use serde::Serialize; +use sightglass_build::get_engine_filename; +use static_metrics::static_metrics; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; + +/// Capture benchmark metrics for principal component analysis (PCA). +#[derive(Debug, StructOpt)] +#[structopt(name = "pca-metrics")] +pub struct PcaMetricsCommand { + /// The optional file path to write output to. Writes output to stdout if + /// omitted. + #[structopt(long, short, parse(from_os_str))] + output: Option, + + /// Optionally bound each benchmark's execution to this many units of fuel. + /// + /// When given, benchmarks are compiled and run with fuel metering and may + /// stop early once their fuel is exhausted. When omitted, benchmarks run to + /// completion. + /// + /// This primarily exists to make testing this command easier, and shouldn't + /// be used when doing full PCA. + #[structopt(long)] + fuel: Option, + + /// The benchmark engine with which to run Callgrind measurements. + /// + /// Defaults to the Wasmtime engine library in this repository. + #[structopt(long, short, parse(from_os_str))] + engine: Option, + + /// The Wasm benchmarks whose PCA metrics should be taken. + inputs: Vec, +} + +impl PcaMetricsCommand { + pub fn execute(&self) -> Result<()> { + let benchmark_engine = self.benchmark_engine_path()?; + let stdout; + let output: Box = match &self.output { + Some(path) => { + let file = std::fs::File::create(path) + .with_context(|| format!("failed to create file: {}", path.display()))?; + Box::new(std::io::BufWriter::new(file)) as _ + } + None => { + stdout = std::io::stdout(); + Box::new(stdout.lock()) as _ + } + }; + + let mut csv = csv::WriterBuilder::new() + .has_headers(true) + .from_writer(output); + + let wasm_files: Vec<_> = self + .inputs + .iter() + .flat_map(|f| f.paths()) + .map(|p| p.display().to_string()) + .collect(); + + let engine = make_engine(self.fuel.is_some())?; + + let mut error = None; + for f in wasm_files { + eprintln!("Gathering PCA metrics for {f}"); + match pca_metrics(&f, &benchmark_engine, &engine, self.fuel) { + Ok(metrics) => { + csv.serialize(metrics) + .context("failed to serialize PCA metrics into CSV file")?; + } + // Collecting metrics for a misbehaving benchmark might + // fail. Log it for now and move on, rather than aborting the + // whole run and throwing away the metrics we have successfully + // collected. + Err(e) => { + eprintln!("failed to compute PCA metrics for {f}, skipping: {e:?}"); + log::warn!("failed to compute PCA metrics for {f}, skipping: {e:?}"); + match error.take() { + None => error = Some(e), + Some(root) => error = Some(root.context(e)), + } + } + } + } + + csv.flush().context("failed to flush data to CSV file")?; + + match error { + None => Ok(()), + Some(e) => Err(e), + } + } + + fn benchmark_engine_path(&self) -> Result { + let engine = self.engine.clone().unwrap_or_else(default_engine_path); + anyhow::ensure!( + engine.exists(), + "invalid path to engine: {}", + engine.display() + ); + Ok(engine) + } +} + +fn default_engine_path() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("engines/wasmtime") + .join(get_engine_filename()) +} + +/// Compute the full set of PCA metrics for a single benchmark: static entity +/// counts, the static instruction mix, and the dynamic instruction mix gathered +/// by instrumenting and running the benchmark once. +fn pca_metrics<'a>( + benchmark: &'a str, + benchmark_engine: &Path, + engine: &wasmtime::Engine, + fuel: Option, +) -> Result> { + let wasm = std::fs::read(benchmark) + .with_context(|| format!("failed to read benchmark: {benchmark}"))?; + + let mut counts = Counts::default(); + static_metrics(&wasm, &mut counts).context("failed to compute static metrics")?; + + // The benchmark reads its workload from its own directory, so preopen that + // as the working directory when we run it. + let working_dir = Path::new(benchmark) + .parent() + .unwrap_or_else(|| Path::new(".")); + dynamic_metrics( + &wasm, + Path::new(benchmark), + &mut counts, + benchmark_engine, + engine, + working_dir, + fuel, + ) + .context("failed to compute dynamic metrics")?; + + Ok(PcaMetrics::from_counts(benchmark, &counts)) +} + +/// Raw, un-normalized metric accumulators for a single benchmark. +#[derive(Default)] +struct Counts { + // Static entity counts. + type_count: u64, + tag_count: u64, + global_count: u64, + memory_count: u64, + table_count: u64, + data_segment_count: u64, + elem_segment_count: u64, + function_count: u64, + module_count: u64, + core_instance_count: u64, + component_count: u64, + component_instance_count: u64, + component_type_count: u64, + component_canon_function_count: u64, + + // Static instruction mix: the number of instructions of each category that + // appear in the module's code, plus the total instruction count. + total_static_insts: u64, + static_insts: [u64; NUM_CATEGORIES], + + // Dynamic instruction mix: the number of instructions of each category that + // were executed when the benchmark ran, plus the total executed. + total_dynamic_insts: u64, + dynamic_insts: [u64; NUM_CATEGORIES], + + // Callgrind-based native execution counts. + instructions_retired: u64, + conditional_branch_misses: u64, + conditional_branches: u64, + indirect_branch_misses: u64, + indirect_branches: u64, + data_reads: u64, + data_writes: u64, + l1_dcache_read_misses: u64, + l1_dcache_write_misses: u64, + ll_dcache_read_misses: u64, + ll_dcache_write_misses: u64, + l1_icache_misses: u64, + ll_icache_misses: u64, +} + +/// PCA metrics. +/// +/// Entity counts are normalized as `1.0 / (count + 1.0)`, and the instruction +/// and Callgrind-derived fields are emitted as ratios over their respective +/// totals. +#[derive(Serialize)] +struct PcaMetrics<'a> { + benchmark: &'a str, + + // Static entity counts. + type_count: f64, + tag_count: f64, + global_count: f64, + memory_count: f64, + table_count: f64, + data_segment_count: f64, + elem_segment_count: f64, + function_count: f64, + total_inst_count: f64, + module_count: f64, + core_instance_count: f64, + component_count: f64, + component_instance_count: f64, + component_type_count: f64, + component_canon_function_count: f64, + + // Static instruction mix. + static_unreachable_inst_ratio: f64, + static_nop_inst_ratio: f64, + static_control_branch_inst_ratio: f64, + static_control_call_inst_ratio: f64, + static_control_exception_inst_ratio: f64, + static_control_stack_switch_inst_ratio: f64, + static_local_variable_inst_ratio: f64, + static_global_variable_inst_ratio: f64, + static_atomic_global_variable_inst_ratio: f64, + static_table_inst_ratio: f64, + static_atomic_table_inst_ratio: f64, + static_memory_size_inst_ratio: f64, + static_memory_grow_inst_ratio: f64, + static_memory_load_inst_ratio: f64, + static_memory_store_inst_ratio: f64, + static_memory_other_inst_ratio: f64, + static_ref_inst_ratio: f64, + static_i31_inst_ratio: f64, + static_aggregate_new_inst_ratio: f64, + static_aggregate_get_inst_ratio: f64, + static_aggregate_set_inst_ratio: f64, + static_atomic_aggregate_inst_ratio: f64, + static_numeric_integer_inst_ratio: f64, + static_numeric_float_inst_ratio: f64, + static_vector_inst_ratio: f64, + static_select_inst_ratio: f64, + + // Dynamic instruction mix. + dynamic_total_inst_count: f64, + dynamic_unreachable_inst_ratio: f64, + dynamic_nop_inst_ratio: f64, + dynamic_control_branch_inst_ratio: f64, + dynamic_control_call_inst_ratio: f64, + dynamic_control_exception_inst_ratio: f64, + dynamic_control_stack_switch_inst_ratio: f64, + dynamic_local_variable_inst_ratio: f64, + dynamic_global_variable_inst_ratio: f64, + dynamic_atomic_global_variable_inst_ratio: f64, + dynamic_table_inst_ratio: f64, + dynamic_atomic_table_inst_ratio: f64, + dynamic_memory_size_inst_ratio: f64, + dynamic_memory_grow_inst_ratio: f64, + dynamic_memory_load_inst_ratio: f64, + dynamic_memory_store_inst_ratio: f64, + dynamic_memory_other_inst_ratio: f64, + dynamic_ref_inst_ratio: f64, + dynamic_i31_inst_ratio: f64, + dynamic_aggregate_new_inst_ratio: f64, + dynamic_aggregate_get_inst_ratio: f64, + dynamic_aggregate_set_inst_ratio: f64, + dynamic_atomic_aggregate_inst_ratio: f64, + dynamic_numeric_integer_inst_ratio: f64, + dynamic_numeric_float_inst_ratio: f64, + dynamic_vector_inst_ratio: f64, + dynamic_select_inst_ratio: f64, + + // Callgrind-based dynamic ratios. + wasm_insts_per_native_inst: f64, + conditional_branch_misses: f64, + conditional_branches: f64, + indirect_branch_misses: f64, + indirect_branches: f64, + l1_dcache_read_misses: f64, + l1_dcache_write_misses: f64, + ll_dcache_read_misses: f64, + ll_dcache_write_misses: f64, + l1_icache_misses: f64, + ll_icache_misses: f64, +} + +impl<'a> PcaMetrics<'a> { + fn from_counts(benchmark: &'a str, c: &Counts) -> Self { + // Normalize an entity count. + let n = |count: u64| 1.0 / (count as f64 + 1.0); + // Normalize a static instruction-mix ratio. + let s = |cat: Category| ratio(c.static_insts[cat as usize], c.total_static_insts); + // Normalize a dynamic instruction-mix ratio. + let d = |cat: Category| ratio(c.dynamic_insts[cat as usize], c.total_dynamic_insts); + + PcaMetrics { + benchmark, + + type_count: n(c.type_count), + tag_count: n(c.tag_count), + global_count: n(c.global_count), + memory_count: n(c.memory_count), + table_count: n(c.table_count), + data_segment_count: n(c.data_segment_count), + elem_segment_count: n(c.elem_segment_count), + function_count: n(c.function_count), + total_inst_count: n(c.total_static_insts), + module_count: n(c.module_count), + core_instance_count: n(c.core_instance_count), + component_count: n(c.component_count), + component_instance_count: n(c.component_instance_count), + component_type_count: n(c.component_type_count), + component_canon_function_count: n(c.component_canon_function_count), + + static_unreachable_inst_ratio: s(Category::Unreachable), + static_nop_inst_ratio: s(Category::Nop), + static_control_branch_inst_ratio: s(Category::ControlBranch), + static_control_call_inst_ratio: s(Category::ControlCall), + static_control_exception_inst_ratio: s(Category::ControlException), + static_control_stack_switch_inst_ratio: s(Category::ControlStackSwitch), + static_local_variable_inst_ratio: s(Category::LocalVariable), + static_global_variable_inst_ratio: s(Category::GlobalVariable), + static_atomic_global_variable_inst_ratio: s(Category::AtomicGlobalVariable), + static_table_inst_ratio: s(Category::Table), + static_atomic_table_inst_ratio: s(Category::AtomicTable), + static_memory_size_inst_ratio: s(Category::MemorySize), + static_memory_grow_inst_ratio: s(Category::MemoryGrow), + static_memory_load_inst_ratio: s(Category::MemoryLoad), + static_memory_store_inst_ratio: s(Category::MemoryStore), + static_memory_other_inst_ratio: s(Category::MemoryOther), + static_ref_inst_ratio: s(Category::Ref), + static_i31_inst_ratio: s(Category::I31), + static_aggregate_new_inst_ratio: s(Category::AggregateNew), + static_aggregate_get_inst_ratio: s(Category::AggregateGet), + static_aggregate_set_inst_ratio: s(Category::AggregateSet), + static_atomic_aggregate_inst_ratio: s(Category::AtomicAggregate), + static_numeric_integer_inst_ratio: s(Category::NumericInteger), + static_numeric_float_inst_ratio: s(Category::NumericFloat), + static_vector_inst_ratio: s(Category::Vector), + static_select_inst_ratio: s(Category::Select), + + dynamic_total_inst_count: c.total_dynamic_insts as f64, + dynamic_unreachable_inst_ratio: d(Category::Unreachable), + dynamic_nop_inst_ratio: d(Category::Nop), + dynamic_control_branch_inst_ratio: d(Category::ControlBranch), + dynamic_control_call_inst_ratio: d(Category::ControlCall), + dynamic_control_exception_inst_ratio: d(Category::ControlException), + dynamic_control_stack_switch_inst_ratio: d(Category::ControlStackSwitch), + dynamic_local_variable_inst_ratio: d(Category::LocalVariable), + dynamic_global_variable_inst_ratio: d(Category::GlobalVariable), + dynamic_atomic_global_variable_inst_ratio: d(Category::AtomicGlobalVariable), + dynamic_table_inst_ratio: d(Category::Table), + dynamic_atomic_table_inst_ratio: d(Category::AtomicTable), + dynamic_memory_size_inst_ratio: d(Category::MemorySize), + dynamic_memory_grow_inst_ratio: d(Category::MemoryGrow), + dynamic_memory_load_inst_ratio: d(Category::MemoryLoad), + dynamic_memory_store_inst_ratio: d(Category::MemoryStore), + dynamic_memory_other_inst_ratio: d(Category::MemoryOther), + dynamic_ref_inst_ratio: d(Category::Ref), + dynamic_i31_inst_ratio: d(Category::I31), + dynamic_aggregate_new_inst_ratio: d(Category::AggregateNew), + dynamic_aggregate_get_inst_ratio: d(Category::AggregateGet), + dynamic_aggregate_set_inst_ratio: d(Category::AggregateSet), + dynamic_atomic_aggregate_inst_ratio: d(Category::AtomicAggregate), + dynamic_numeric_integer_inst_ratio: d(Category::NumericInteger), + dynamic_numeric_float_inst_ratio: d(Category::NumericFloat), + dynamic_vector_inst_ratio: d(Category::Vector), + dynamic_select_inst_ratio: d(Category::Select), + + wasm_insts_per_native_inst: ratio(c.total_dynamic_insts, c.instructions_retired), + conditional_branch_misses: ratio(c.conditional_branch_misses, c.conditional_branches), + conditional_branches: ratio(c.conditional_branches, c.instructions_retired), + indirect_branch_misses: ratio(c.indirect_branch_misses, c.indirect_branches), + indirect_branches: ratio(c.indirect_branches, c.instructions_retired), + l1_dcache_read_misses: ratio(c.l1_dcache_read_misses, c.data_reads), + l1_dcache_write_misses: ratio(c.l1_dcache_write_misses, c.data_writes), + ll_dcache_read_misses: ratio(c.ll_dcache_read_misses, c.data_reads), + ll_dcache_write_misses: ratio(c.ll_dcache_write_misses, c.data_writes), + l1_icache_misses: ratio(c.l1_icache_misses, c.instructions_retired), + ll_icache_misses: ratio(c.ll_icache_misses, c.instructions_retired), + } + } +} + +/// Normalize a category's instruction count against the total. +fn ratio(count: u64, total: u64) -> f64 { + if total == 0 { + 0.0 + } else { + count as f64 / total as f64 + } +} + +#[cfg(test)] +mod tests { + use super::{Counts, PcaMetrics}; + + #[test] + fn callgrind_ratios_are_normalized_from_counts() { + let metrics = PcaMetrics::from_counts( + "benchmark.wasm", + &Counts { + total_dynamic_insts: 250, + instructions_retired: 500, + conditional_branch_misses: 5, + conditional_branches: 20, + indirect_branch_misses: 3, + indirect_branches: 10, + data_reads: 40, + data_writes: 50, + l1_dcache_read_misses: 4, + l1_dcache_write_misses: 5, + ll_dcache_read_misses: 2, + ll_dcache_write_misses: 1, + l1_icache_misses: 25, + ll_icache_misses: 10, + ..Counts::default() + }, + ); + + assert_eq!(metrics.wasm_insts_per_native_inst, 0.5); + assert_eq!(metrics.conditional_branch_misses, 0.25); + assert_eq!(metrics.conditional_branches, 0.04); + assert_eq!(metrics.indirect_branch_misses, 0.3); + assert_eq!(metrics.indirect_branches, 0.02); + assert_eq!(metrics.l1_dcache_read_misses, 0.1); + assert_eq!(metrics.l1_dcache_write_misses, 0.1); + assert_eq!(metrics.ll_dcache_read_misses, 0.05); + assert_eq!(metrics.ll_dcache_write_misses, 0.02); + assert_eq!(metrics.l1_icache_misses, 0.05); + assert_eq!(metrics.ll_icache_misses, 0.02); + } +} diff --git a/crates/cli/src/pca_metrics/category.rs b/crates/cli/src/pca_metrics/category.rs new file mode 100644 index 00000000..5b4f4810 --- /dev/null +++ b/crates/cli/src/pca_metrics/category.rs @@ -0,0 +1,673 @@ +//! Categorization of Wasm operators into the instruction categories whose static +//! and dynamic mixes we track for PCA. + +use anyhow::{bail, Result}; +use wasmparser::Operator; + +/// The categories of Wasm instructions whose static and dynamic mixes we track. +/// +/// The discriminants double as indices into the per-category counter arrays (see +/// the `Counts` accumulator) and as the offsets of the instrumentation's counter +/// globals (see the `dynamic_metrics` module), so their order must stay in sync +/// with both. +#[derive(Clone, Copy)] +pub(crate) enum Category { + Unreachable = 0, + Nop = 1, + ControlBranch = 2, + ControlCall = 3, + ControlException = 4, + ControlStackSwitch = 5, + LocalVariable = 6, + GlobalVariable = 7, + AtomicGlobalVariable = 8, + Table = 9, + AtomicTable = 10, + MemorySize = 11, + MemoryGrow = 12, + MemoryLoad = 13, + MemoryStore = 14, + MemoryOther = 15, + Ref = 16, + I31 = 17, + AggregateNew = 18, + AggregateGet = 19, + AggregateSet = 20, + AtomicAggregate = 21, + NumericInteger = 22, + NumericFloat = 23, + Vector = 24, + Select = 25, +} + +/// The total number of [`Category`] variants. +pub(crate) const NUM_CATEGORIES: usize = 26; + +impl Category { + /// Classify a Wasm operator into its [`Category`], returning an error for + /// any operator we don't recognize. + pub(crate) fn for_op(op: &Operator) -> Result { + Ok(match op { + Operator::Unreachable => Category::Unreachable, + Operator::Drop | Operator::Nop => Category::Nop, + Operator::Block { .. } + | Operator::Br { .. } + | Operator::BrIf { .. } + | Operator::BrOnNonNull { .. } + | Operator::BrOnNull { .. } + | Operator::BrTable { .. } + | Operator::Else + | Operator::End + | Operator::If { .. } + | Operator::Loop { .. } + | Operator::Return => Category::ControlBranch, + Operator::Call { .. } + | Operator::CallIndirect { .. } + | Operator::CallRef { .. } + | Operator::ReturnCall { .. } + | Operator::ReturnCallIndirect { .. } + | Operator::ReturnCallRef { .. } => Category::ControlCall, + Operator::Catch { .. } + | Operator::CatchAll + | Operator::Delegate { .. } + | Operator::Rethrow { .. } + | Operator::Throw { .. } + | Operator::ThrowRef + | Operator::Try { .. } + | Operator::TryTable { .. } => Category::ControlException, + Operator::ContBind { .. } + | Operator::ContNew { .. } + | Operator::Resume { .. } + | Operator::ResumeThrow { .. } + | Operator::Suspend { .. } + | Operator::Switch { .. } => Category::ControlStackSwitch, + Operator::LocalGet { .. } | Operator::LocalSet { .. } | Operator::LocalTee { .. } => { + Category::LocalVariable + } + Operator::GlobalGet { .. } | Operator::GlobalSet { .. } => Category::GlobalVariable, + Operator::GlobalAtomicGet { .. } + | Operator::GlobalAtomicRmwAdd { .. } + | Operator::GlobalAtomicRmwAnd { .. } + | Operator::GlobalAtomicRmwCmpxchg { .. } + | Operator::GlobalAtomicRmwOr { .. } + | Operator::GlobalAtomicRmwSub { .. } + | Operator::GlobalAtomicRmwXchg { .. } + | Operator::GlobalAtomicRmwXor { .. } + | Operator::GlobalAtomicSet { .. } => Category::AtomicGlobalVariable, + Operator::ElemDrop { .. } + | Operator::TableCopy { .. } + | Operator::TableFill { .. } + | Operator::TableGet { .. } + | Operator::TableGrow { .. } + | Operator::TableInit { .. } + | Operator::TableSet { .. } + | Operator::TableSize { .. } => Category::Table, + Operator::TableAtomicGet { .. } + | Operator::TableAtomicRmwCmpxchg { .. } + | Operator::TableAtomicRmwXchg { .. } + | Operator::TableAtomicSet { .. } => Category::AtomicTable, + Operator::MemorySize { .. } => Category::MemorySize, + Operator::MemoryGrow { .. } => Category::MemoryGrow, + Operator::F32Load { .. } + | Operator::F64Load { .. } + | Operator::I32AtomicLoad { .. } + | Operator::I32AtomicLoad16U { .. } + | Operator::I32AtomicLoad8U { .. } + | Operator::I32Load { .. } + | Operator::I32Load16S { .. } + | Operator::I32Load16U { .. } + | Operator::I32Load8S { .. } + | Operator::I32Load8U { .. } + | Operator::I64AtomicLoad { .. } + | Operator::I64AtomicLoad16U { .. } + | Operator::I64AtomicLoad32U { .. } + | Operator::I64AtomicLoad8U { .. } + | Operator::I64Load { .. } + | Operator::I64Load16S { .. } + | Operator::I64Load16U { .. } + | Operator::I64Load32S { .. } + | Operator::I64Load32U { .. } + | Operator::I64Load8S { .. } + | Operator::I64Load8U { .. } + | Operator::V128Load { .. } + | Operator::V128Load16Lane { .. } + | Operator::V128Load16Splat { .. } + | Operator::V128Load16x4S { .. } + | Operator::V128Load16x4U { .. } + | Operator::V128Load32Lane { .. } + | Operator::V128Load32Splat { .. } + | Operator::V128Load32Zero { .. } + | Operator::V128Load32x2S { .. } + | Operator::V128Load32x2U { .. } + | Operator::V128Load64Lane { .. } + | Operator::V128Load64Splat { .. } + | Operator::V128Load64Zero { .. } + | Operator::V128Load8Lane { .. } + | Operator::V128Load8Splat { .. } + | Operator::V128Load8x8S { .. } + | Operator::V128Load8x8U { .. } => Category::MemoryLoad, + Operator::F32Store { .. } + | Operator::F64Store { .. } + | Operator::I32AtomicStore { .. } + | Operator::I32AtomicStore16 { .. } + | Operator::I32AtomicStore8 { .. } + | Operator::I32Store { .. } + | Operator::I32Store16 { .. } + | Operator::I32Store8 { .. } + | Operator::I64AtomicStore { .. } + | Operator::I64AtomicStore16 { .. } + | Operator::I64AtomicStore32 { .. } + | Operator::I64AtomicStore8 { .. } + | Operator::I64Store { .. } + | Operator::I64Store16 { .. } + | Operator::I64Store32 { .. } + | Operator::I64Store8 { .. } + | Operator::V128Store { .. } + | Operator::V128Store16Lane { .. } + | Operator::V128Store32Lane { .. } + | Operator::V128Store64Lane { .. } + | Operator::V128Store8Lane { .. } => Category::MemoryStore, + Operator::AtomicFence + | Operator::DataDrop { .. } + | Operator::I32AtomicRmw16AddU { .. } + | Operator::I32AtomicRmw16AndU { .. } + | Operator::I32AtomicRmw16CmpxchgU { .. } + | Operator::I32AtomicRmw16OrU { .. } + | Operator::I32AtomicRmw16SubU { .. } + | Operator::I32AtomicRmw16XchgU { .. } + | Operator::I32AtomicRmw16XorU { .. } + | Operator::I32AtomicRmw8AddU { .. } + | Operator::I32AtomicRmw8AndU { .. } + | Operator::I32AtomicRmw8CmpxchgU { .. } + | Operator::I32AtomicRmw8OrU { .. } + | Operator::I32AtomicRmw8SubU { .. } + | Operator::I32AtomicRmw8XchgU { .. } + | Operator::I32AtomicRmw8XorU { .. } + | Operator::I32AtomicRmwAdd { .. } + | Operator::I32AtomicRmwAnd { .. } + | Operator::I32AtomicRmwCmpxchg { .. } + | Operator::I32AtomicRmwOr { .. } + | Operator::I32AtomicRmwSub { .. } + | Operator::I32AtomicRmwXchg { .. } + | Operator::I32AtomicRmwXor { .. } + | Operator::I64AtomicRmw16AddU { .. } + | Operator::I64AtomicRmw16AndU { .. } + | Operator::I64AtomicRmw16CmpxchgU { .. } + | Operator::I64AtomicRmw16OrU { .. } + | Operator::I64AtomicRmw16SubU { .. } + | Operator::I64AtomicRmw16XchgU { .. } + | Operator::I64AtomicRmw16XorU { .. } + | Operator::I64AtomicRmw32AddU { .. } + | Operator::I64AtomicRmw32AndU { .. } + | Operator::I64AtomicRmw32CmpxchgU { .. } + | Operator::I64AtomicRmw32OrU { .. } + | Operator::I64AtomicRmw32SubU { .. } + | Operator::I64AtomicRmw32XchgU { .. } + | Operator::I64AtomicRmw32XorU { .. } + | Operator::I64AtomicRmw8AddU { .. } + | Operator::I64AtomicRmw8AndU { .. } + | Operator::I64AtomicRmw8CmpxchgU { .. } + | Operator::I64AtomicRmw8OrU { .. } + | Operator::I64AtomicRmw8SubU { .. } + | Operator::I64AtomicRmw8XchgU { .. } + | Operator::I64AtomicRmw8XorU { .. } + | Operator::I64AtomicRmwAdd { .. } + | Operator::I64AtomicRmwAnd { .. } + | Operator::I64AtomicRmwCmpxchg { .. } + | Operator::I64AtomicRmwOr { .. } + | Operator::I64AtomicRmwSub { .. } + | Operator::I64AtomicRmwXchg { .. } + | Operator::I64AtomicRmwXor { .. } + | Operator::MemoryAtomicNotify { .. } + | Operator::MemoryAtomicWait32 { .. } + | Operator::MemoryAtomicWait64 { .. } + | Operator::MemoryCopy { .. } + | Operator::MemoryDiscard { .. } + | Operator::MemoryFill { .. } + | Operator::MemoryInit { .. } => Category::MemoryOther, + Operator::ExternConvertAny + | Operator::RefAsNonNull + | Operator::RefCastDescNonNull { .. } + | Operator::RefCastDescNullable { .. } + | Operator::RefCastNonNull { .. } + | Operator::RefCastNullable { .. } + | Operator::RefEq + | Operator::RefFunc { .. } + | Operator::RefGetDesc { .. } + | Operator::RefIsNull + | Operator::RefNull { .. } + | Operator::RefTestNonNull { .. } + | Operator::RefTestNullable { .. } => Category::Ref, + Operator::I31GetS | Operator::I31GetU | Operator::RefI31 | Operator::RefI31Shared => { + Category::I31 + } + Operator::ArrayNew { .. } + | Operator::ArrayNewData { .. } + | Operator::ArrayNewDefault { .. } + | Operator::ArrayNewElem { .. } + | Operator::ArrayNewFixed { .. } + | Operator::StructNew { .. } + | Operator::StructNewDefault { .. } + | Operator::StructNewDefaultDesc { .. } + | Operator::StructNewDesc { .. } => Category::AggregateNew, + Operator::ArrayGet { .. } + | Operator::ArrayGetS { .. } + | Operator::ArrayGetU { .. } + | Operator::ArrayLen + | Operator::StructGet { .. } + | Operator::StructGetS { .. } + | Operator::StructGetU { .. } => Category::AggregateGet, + Operator::ArrayCopy { .. } + | Operator::ArrayFill { .. } + | Operator::ArrayInitData { .. } + | Operator::ArrayInitElem { .. } + | Operator::ArraySet { .. } + | Operator::StructSet { .. } => Category::AggregateSet, + Operator::ArrayAtomicGet { .. } + | Operator::ArrayAtomicGetS { .. } + | Operator::ArrayAtomicGetU { .. } + | Operator::ArrayAtomicRmwAdd { .. } + | Operator::ArrayAtomicRmwAnd { .. } + | Operator::ArrayAtomicRmwCmpxchg { .. } + | Operator::ArrayAtomicRmwOr { .. } + | Operator::ArrayAtomicRmwSub { .. } + | Operator::ArrayAtomicRmwXchg { .. } + | Operator::ArrayAtomicRmwXor { .. } + | Operator::ArrayAtomicSet { .. } + | Operator::StructAtomicGet { .. } + | Operator::StructAtomicGetS { .. } + | Operator::StructAtomicGetU { .. } + | Operator::StructAtomicRmwAdd { .. } + | Operator::StructAtomicRmwAnd { .. } + | Operator::StructAtomicRmwCmpxchg { .. } + | Operator::StructAtomicRmwOr { .. } + | Operator::StructAtomicRmwSub { .. } + | Operator::StructAtomicRmwXchg { .. } + | Operator::StructAtomicRmwXor { .. } + | Operator::StructAtomicSet { .. } => Category::AtomicAggregate, + Operator::I32Add + | Operator::I32And + | Operator::I32Clz + | Operator::I32Const { .. } + | Operator::I32Ctz + | Operator::I32DivS + | Operator::I32DivU + | Operator::I32Eq + | Operator::I32Eqz + | Operator::I32Extend16S + | Operator::I32Extend8S + | Operator::I32GeS + | Operator::I32GeU + | Operator::I32GtS + | Operator::I32GtU + | Operator::I32LeS + | Operator::I32LeU + | Operator::I32LtS + | Operator::I32LtU + | Operator::I32Mul + | Operator::I32Ne + | Operator::I32Or + | Operator::I32Popcnt + | Operator::I32ReinterpretF32 + | Operator::I32RemS + | Operator::I32RemU + | Operator::I32Rotl + | Operator::I32Rotr + | Operator::I32Shl + | Operator::I32ShrS + | Operator::I32ShrU + | Operator::I32Sub + | Operator::I32TruncF32S + | Operator::I32TruncF32U + | Operator::I32TruncF64S + | Operator::I32TruncF64U + | Operator::I32TruncSatF32S + | Operator::I32TruncSatF32U + | Operator::I32TruncSatF64S + | Operator::I32TruncSatF64U + | Operator::I32WrapI64 + | Operator::I32Xor + | Operator::I64Add + | Operator::I64Add128 + | Operator::I64And + | Operator::I64Clz + | Operator::I64Const { .. } + | Operator::I64Ctz + | Operator::I64DivS + | Operator::I64DivU + | Operator::I64Eq + | Operator::I64Eqz + | Operator::I64Extend16S + | Operator::I64Extend32S + | Operator::I64Extend8S + | Operator::I64ExtendI32S + | Operator::I64ExtendI32U + | Operator::I64GeS + | Operator::I64GeU + | Operator::I64GtS + | Operator::I64GtU + | Operator::I64LeS + | Operator::I64LeU + | Operator::I64LtS + | Operator::I64LtU + | Operator::I64Mul + | Operator::I64MulWideS + | Operator::I64MulWideU + | Operator::I64Ne + | Operator::I64Or + | Operator::I64Popcnt + | Operator::I64ReinterpretF64 + | Operator::I64RemS + | Operator::I64RemU + | Operator::I64Rotl + | Operator::I64Rotr + | Operator::I64Shl + | Operator::I64ShrS + | Operator::I64ShrU + | Operator::I64Sub + | Operator::I64Sub128 + | Operator::I64TruncF32S + | Operator::I64TruncF32U + | Operator::I64TruncF64S + | Operator::I64TruncF64U + | Operator::I64TruncSatF32S + | Operator::I64TruncSatF32U + | Operator::I64TruncSatF64S + | Operator::I64TruncSatF64U + | Operator::I64Xor => Category::NumericInteger, + Operator::F32Abs + | Operator::F32Add + | Operator::F32Ceil + | Operator::F32Const { .. } + | Operator::F32ConvertI32S + | Operator::F32ConvertI32U + | Operator::F32ConvertI64S + | Operator::F32ConvertI64U + | Operator::F32Copysign + | Operator::F32DemoteF64 + | Operator::F32Div + | Operator::F32Eq + | Operator::F32Floor + | Operator::F32Ge + | Operator::F32Gt + | Operator::F32Le + | Operator::F32Lt + | Operator::F32Max + | Operator::F32Min + | Operator::F32Mul + | Operator::F32Ne + | Operator::F32Nearest + | Operator::F32Neg + | Operator::F32ReinterpretI32 + | Operator::F32Sqrt + | Operator::F32Sub + | Operator::F32Trunc + | Operator::F64Abs + | Operator::F64Add + | Operator::F64Ceil + | Operator::F64Const { .. } + | Operator::F64ConvertI32S + | Operator::F64ConvertI32U + | Operator::F64ConvertI64S + | Operator::F64ConvertI64U + | Operator::F64Copysign + | Operator::F64Div + | Operator::F64Eq + | Operator::F64Floor + | Operator::F64Ge + | Operator::F64Gt + | Operator::F64Le + | Operator::F64Lt + | Operator::F64Max + | Operator::F64Min + | Operator::F64Mul + | Operator::F64Ne + | Operator::F64Nearest + | Operator::F64Neg + | Operator::F64PromoteF32 + | Operator::F64ReinterpretI64 + | Operator::F64Sqrt + | Operator::F64Sub + | Operator::F64Trunc => Category::NumericFloat, + Operator::F32x4Abs + | Operator::F32x4Add + | Operator::F32x4Ceil + | Operator::F32x4ConvertI32x4S + | Operator::F32x4ConvertI32x4U + | Operator::F32x4DemoteF64x2Zero + | Operator::F32x4Div + | Operator::F32x4Eq + | Operator::F32x4ExtractLane { .. } + | Operator::F32x4Floor + | Operator::F32x4Ge + | Operator::F32x4Gt + | Operator::F32x4Le + | Operator::F32x4Lt + | Operator::F32x4Max + | Operator::F32x4Min + | Operator::F32x4Mul + | Operator::F32x4Ne + | Operator::F32x4Nearest + | Operator::F32x4Neg + | Operator::F32x4PMax + | Operator::F32x4PMin + | Operator::F32x4RelaxedMadd + | Operator::F32x4RelaxedMax + | Operator::F32x4RelaxedMin + | Operator::F32x4RelaxedNmadd + | Operator::F32x4ReplaceLane { .. } + | Operator::F32x4Splat + | Operator::F32x4Sqrt + | Operator::F32x4Sub + | Operator::F32x4Trunc + | Operator::F64x2Abs + | Operator::F64x2Add + | Operator::F64x2Ceil + | Operator::F64x2ConvertLowI32x4S + | Operator::F64x2ConvertLowI32x4U + | Operator::F64x2Div + | Operator::F64x2Eq + | Operator::F64x2ExtractLane { .. } + | Operator::F64x2Floor + | Operator::F64x2Ge + | Operator::F64x2Gt + | Operator::F64x2Le + | Operator::F64x2Lt + | Operator::F64x2Max + | Operator::F64x2Min + | Operator::F64x2Mul + | Operator::F64x2Ne + | Operator::F64x2Nearest + | Operator::F64x2Neg + | Operator::F64x2PMax + | Operator::F64x2PMin + | Operator::F64x2PromoteLowF32x4 + | Operator::F64x2RelaxedMax + | Operator::F64x2RelaxedMin + | Operator::F64x2RelaxedNmadd + | Operator::F64x2ReplaceLane { .. } + | Operator::F64x2Splat + | Operator::F64x2Sqrt + | Operator::F64x2Sub + | Operator::F64x2Trunc + | Operator::I16x8Abs + | Operator::I16x8Add + | Operator::I16x8AddSatS + | Operator::I16x8AddSatU + | Operator::I16x8AllTrue + | Operator::I16x8AvgrU + | Operator::I16x8Bitmask + | Operator::I16x8Eq + | Operator::I16x8ExtAddPairwiseI8x16S + | Operator::I16x8ExtAddPairwiseI8x16U + | Operator::I16x8ExtMulHighI8x16S + | Operator::I16x8ExtMulHighI8x16U + | Operator::I16x8ExtMulLowI8x16S + | Operator::I16x8ExtMulLowI8x16U + | Operator::I16x8ExtendHighI8x16S + | Operator::I16x8ExtendHighI8x16U + | Operator::I16x8ExtendLowI8x16S + | Operator::I16x8ExtendLowI8x16U + | Operator::I16x8ExtractLaneS { .. } + | Operator::I16x8ExtractLaneU { .. } + | Operator::I16x8GeS + | Operator::I16x8GeU + | Operator::I16x8GtS + | Operator::I16x8GtU + | Operator::I16x8LeS + | Operator::I16x8LeU + | Operator::I16x8LtS + | Operator::I16x8LtU + | Operator::I16x8MaxS + | Operator::I16x8MaxU + | Operator::I16x8MinS + | Operator::I16x8MinU + | Operator::I16x8Mul + | Operator::I16x8NarrowI32x4S + | Operator::I16x8NarrowI32x4U + | Operator::I16x8Ne + | Operator::I16x8Neg + | Operator::I16x8Q15MulrSatS + | Operator::I16x8RelaxedDotI8x16I7x16S + | Operator::I16x8RelaxedLaneselect + | Operator::I16x8RelaxedQ15mulrS + | Operator::I16x8ReplaceLane { .. } + | Operator::I16x8Shl + | Operator::I16x8ShrS + | Operator::I16x8ShrU + | Operator::I16x8Splat + | Operator::I16x8Sub + | Operator::I16x8SubSatS + | Operator::I16x8SubSatU + | Operator::I32x4Abs + | Operator::I32x4Add + | Operator::I32x4AllTrue + | Operator::I32x4Bitmask + | Operator::I32x4DotI16x8S + | Operator::I32x4Eq + | Operator::I32x4ExtAddPairwiseI16x8S + | Operator::I32x4ExtAddPairwiseI16x8U + | Operator::I32x4ExtMulHighI16x8S + | Operator::I32x4ExtMulHighI16x8U + | Operator::I32x4ExtMulLowI16x8S + | Operator::I32x4ExtMulLowI16x8U + | Operator::I32x4ExtendHighI16x8S + | Operator::I32x4ExtendHighI16x8U + | Operator::I32x4ExtendLowI16x8S + | Operator::I32x4ExtendLowI16x8U + | Operator::I32x4ExtractLane { .. } + | Operator::I32x4GeS + | Operator::I32x4GeU + | Operator::I32x4GtS + | Operator::I32x4GtU + | Operator::I32x4LeS + | Operator::I32x4LeU + | Operator::I32x4LtS + | Operator::I32x4LtU + | Operator::I32x4MaxS + | Operator::I32x4MaxU + | Operator::I32x4MinS + | Operator::I32x4MinU + | Operator::I32x4Mul + | Operator::I32x4Ne + | Operator::I32x4Neg + | Operator::I32x4RelaxedDotI8x16I7x16AddS + | Operator::I32x4RelaxedLaneselect + | Operator::I32x4RelaxedTruncF32x4S + | Operator::I32x4RelaxedTruncF32x4U + | Operator::I32x4RelaxedTruncF64x2SZero + | Operator::I32x4RelaxedTruncF64x2UZero + | Operator::I32x4ReplaceLane { .. } + | Operator::I32x4Shl + | Operator::I32x4ShrS + | Operator::I32x4ShrU + | Operator::I32x4Splat + | Operator::I32x4Sub + | Operator::I32x4TruncSatF32x4S + | Operator::I32x4TruncSatF32x4U + | Operator::I32x4TruncSatF64x2SZero + | Operator::I32x4TruncSatF64x2UZero + | Operator::I64x2Abs + | Operator::I64x2Add + | Operator::I64x2AllTrue + | Operator::I64x2Bitmask + | Operator::I64x2Eq + | Operator::I64x2ExtMulHighI32x4S + | Operator::I64x2ExtMulHighI32x4U + | Operator::I64x2ExtMulLowI32x4S + | Operator::I64x2ExtMulLowI32x4U + | Operator::I64x2ExtendHighI32x4S + | Operator::I64x2ExtendHighI32x4U + | Operator::I64x2ExtendLowI32x4S + | Operator::I64x2ExtendLowI32x4U + | Operator::I64x2ExtractLane { .. } + | Operator::I64x2GeS + | Operator::I64x2GtS + | Operator::I64x2LeS + | Operator::I64x2LtS + | Operator::I64x2Mul + | Operator::I64x2Ne + | Operator::I64x2Neg + | Operator::I64x2RelaxedLaneselect + | Operator::I64x2ReplaceLane { .. } + | Operator::I64x2Shl + | Operator::I64x2ShrS + | Operator::I64x2ShrU + | Operator::I64x2Splat + | Operator::I64x2Sub + | Operator::I8x16Abs + | Operator::I8x16Add + | Operator::I8x16AddSatS + | Operator::I8x16AddSatU + | Operator::I8x16AllTrue + | Operator::I8x16AvgrU + | Operator::I8x16Bitmask + | Operator::I8x16Eq + | Operator::I8x16ExtractLaneS { .. } + | Operator::I8x16ExtractLaneU { .. } + | Operator::I8x16GeS + | Operator::I8x16GeU + | Operator::I8x16GtS + | Operator::I8x16GtU + | Operator::I8x16LeS + | Operator::I8x16LeU + | Operator::I8x16LtS + | Operator::I8x16LtU + | Operator::I8x16MaxS + | Operator::I8x16MaxU + | Operator::I8x16MinS + | Operator::I8x16MinU + | Operator::I8x16NarrowI16x8S + | Operator::I8x16NarrowI16x8U + | Operator::I8x16Ne + | Operator::I8x16Neg + | Operator::I8x16Popcnt + | Operator::I8x16RelaxedLaneselect + | Operator::I8x16RelaxedSwizzle + | Operator::I8x16ReplaceLane { .. } + | Operator::I8x16Shl + | Operator::I8x16ShrS + | Operator::I8x16ShrU + | Operator::I8x16Shuffle { .. } + | Operator::I8x16Splat + | Operator::I8x16Sub + | Operator::I8x16SubSatS + | Operator::I8x16SubSatU + | Operator::I8x16Swizzle + | Operator::V128And + | Operator::V128AndNot + | Operator::V128AnyTrue + | Operator::V128Bitselect + | Operator::V128Const { .. } + | Operator::V128Not + | Operator::V128Or + | Operator::V128Xor => Category::Vector, + Operator::Select | Operator::TypedSelect { .. } | Operator::TypedSelectMulti { .. } => { + Category::Select + } + // `Operator` is `#[non_exhaustive]`, so we need a catch-all; reaching + // it means we encountered an instruction we don't categorize yet. + _ => bail!("unknown instruction: {op:?}"), + }) + } +} diff --git a/crates/cli/src/pca_metrics/dynamic_metrics.rs b/crates/cli/src/pca_metrics/dynamic_metrics.rs new file mode 100644 index 00000000..15476e1b --- /dev/null +++ b/crates/cli/src/pca_metrics/dynamic_metrics.rs @@ -0,0 +1,886 @@ +//! Dynamic PCA metrics: the dynamically executed instruction mix, cache +//! behavior, etc... Gathered by instrumenting the module to count how many +//! instructions of each category it executes and then running it once. + +mod component; + +use super::category::{Category, NUM_CATEGORIES}; +use super::Counts; +use anyhow::{bail, Context, Result}; +use sightglass_data::{Measurement, Phase}; +use std::path::Path; +#[cfg(all(target_os = "linux", feature = "callgrind"))] +use std::process::{Command, Stdio}; +use wasmparser::Payload; + +/// The name prefix used for the exported core functions that return how many +/// times each category of instruction was executed by an instrumented core +/// module (used when not flushing to the host; see `instrument`). +const COUNTER_PREFIX: &str = "__pca_count_"; + +/// The imported instance and function a component core module flushes its +/// counts through (and that `run_component` supplies from the host). +const PCA_INSTANCE: &str = "sightglass-pca"; +const INCREMENT_FN: &str = "increment-instruction-count"; + +/// Functions exported under these conventional names are the canonical ABI +/// `realloc`, which runs with the component's "may leave" flag cleared and so +/// cannot call the host `increment-instruction-count`. When flushing, we still +/// count their instructions into the per-category globals but never flush from +/// inside them; a later, ordinary function flushes the accumulated counts. +const NO_FLUSH_EXPORTS: &[&str] = &["realloc", "cabi_realloc"]; + +/// Compute the dynamic instruction mix for `wasm`, accumulating it into +/// `counts`, by instrumenting and running the benchmark once. +pub(crate) fn dynamic_metrics( + wasm: &[u8], + benchmark: &Path, + counts: &mut Counts, + benchmark_engine: &Path, + engine: &wasmtime::Engine, + working_dir: &Path, + fuel: Option, +) -> Result<()> { + eprintln!("> dynamic metrics"); + + if is_component(wasm) { + let instrumented = + component::instrument_component(wasm).context("failed to instrument component")?; + run_instrumented_component(&instrumented, counts, engine, working_dir, fuel) + .context("failed to run instrumented component")?; + } else { + let instrumented = instrument_core_module(wasm, false) + .context("failed to instrument Wasm")? + .finish(); + run_instrumented_core_module(&instrumented, counts, engine, working_dir, fuel) + .context("failed to run instrumented Wasm")?; + } + + // Fuel-capped runs are intentionally partial and only used for quick tests; + // skip Callgrind because its too slow for this use case. + if fuel.is_none() { + callgrind_metrics(benchmark, counts, benchmark_engine, working_dir) + .context("failed to collect Callgrind metrics")?; + } + + Ok(()) +} + +#[cfg(all(target_os = "linux", feature = "callgrind"))] +fn callgrind_metrics( + benchmark: &Path, + counts: &mut Counts, + benchmark_engine: &Path, + working_dir: &Path, +) -> Result<()> { + eprintln!("> callgrind metrics"); + let this_exe = std::env::current_exe().context("failed to get current executable")?; + let output = Command::new(&this_exe) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg("benchmark") + .arg("--measure") + .arg("callgrind") + .arg("--engine") + .arg(benchmark_engine) + .arg("--raw") + .arg("--output-format") + .arg("json") + .arg("--processes") + .arg("1") + .arg("--iterations-per-process") + .arg("1") + .arg("--benchmark-phase") + .arg("execution") + .arg("--working-dir") + .arg(working_dir) + .arg("--engine-flags") + .arg("-Wgc,function-references") + .arg("--") + .arg(benchmark) + .output() + .with_context(|| { + format!( + "failed to spawn benchmark subprocess for {}", + benchmark.display() + ) + })?; + + anyhow::ensure!( + output.status.success(), + "callgrind benchmark subprocess failed for {}: {}", + benchmark.display(), + String::from_utf8_lossy(&output.stderr).trim() + ); + + let measurements = serde_json::from_slice::>>(&output.stdout) + .with_context(|| { + format!( + "failed to parse callgrind benchmark output for {}", + benchmark.display() + ) + })?; + accumulate_callgrind_counts(&measurements, counts); + Ok(()) +} + +#[cfg(not(all(target_os = "linux", feature = "callgrind")))] +fn callgrind_metrics( + _benchmark: &Path, + _counts: &mut Counts, + _benchmark_engine: &Path, + _working_dir: &Path, +) -> Result<()> { + Ok(()) +} + +fn accumulate_callgrind_counts(measurements: &[Measurement<'_>], counts: &mut Counts) { + for measurement in measurements { + if measurement.phase != Phase::Execution { + continue; + } + + match measurement.event.as_ref() { + "instructions-retired" => counts.instructions_retired += measurement.count, + "conditional-branch-misses" => counts.conditional_branch_misses += measurement.count, + "conditional-branches" => counts.conditional_branches += measurement.count, + "indirect-branch-misses" => counts.indirect_branch_misses += measurement.count, + "indirect-branches" => counts.indirect_branches += measurement.count, + "data-reads" => counts.data_reads += measurement.count, + "data-writes" => counts.data_writes += measurement.count, + "l1-dcache-read-misses" => counts.l1_dcache_read_misses += measurement.count, + "l1-dcache-write-misses" => counts.l1_dcache_write_misses += measurement.count, + "ll-dcache-read-misses" => counts.ll_dcache_read_misses += measurement.count, + "ll-dcache-write-misses" => counts.ll_dcache_write_misses += measurement.count, + "l1-icache-misses" => counts.l1_icache_misses += measurement.count, + "ll-icache-misses" => counts.ll_icache_misses += measurement.count, + otherwise => { + eprintln!("ignoring unknown event: `{otherwise}`"); + log::warn!("ignoring unknown event: `{otherwise}`"); + } + } + } +} + +/// Whether `wasm` is a component, as opposed to a core module. +fn is_component(wasm: &[u8]) -> bool { + matches!( + wasmparser::Parser::new(0).parse_all(wasm).next(), + Some(Ok(Payload::Version { + encoding: wasmparser::Encoding::Component, + .. + })) + ) +} + +/// Run an instrumented component once and accumulate its category counters into +/// `counts`. +/// +/// The component is run the way `wasm_bench_create` runs components in +/// `wasmtime`'s `bench-api`: with WASI preview 2 and the benchmarking +/// hooks. The instrumentation pushes counts to the host via a `sightglass-pca` +/// `increment-instruction-count` import as it runs, and the host tallies them +/// while benchmarking is active (between `bench.start`/`bench.end`). Because +/// the counts live in host memory rather than inside the component, we recover +/// them even when the benchmark runs out of fuel mid-run. +fn run_instrumented_component( + component_bytes: &[u8], + counts: &mut Counts, + engine: &wasmtime::Engine, + working_dir: &Path, + fuel: Option, +) -> Result<()> { + use wasmtime::component::{Component, Linker, ResourceTable}; + use wasmtime::{Store, StoreContextMut, Trap}; + use wasmtime_wasi::p2::bindings::sync::Command; + use wasmtime_wasi::{DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + + struct Host { + wasi: WasiCtx, + table: ResourceTable, + /// Whether we are between `bench.start` and `bench.end`. + active: bool, + /// Executed-instruction counts per category, pushed by the component. + dynamic: [u64; NUM_CATEGORIES], + } + impl WasiView for Host { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } + } + + let component = Component::new(engine, component_bytes)?; + + // Provide WASIp2, the benchmarking hooks (which toggle the active flag), and + // the `sightglass-pca` instance the instrumentation pushes counts to. + let mut linker: Linker = Linker::new(engine); + wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?; + let mut bench = linker.instance("bench")?; + bench.func_wrap("start", |mut cx: StoreContextMut<'_, Host>, (): ()| { + cx.data_mut().active = true; + Ok(()) + })?; + bench.func_wrap("end", |mut cx: StoreContextMut<'_, Host>, (): ()| { + cx.data_mut().active = false; + Ok(()) + })?; + let mut pca = linker.instance(PCA_INSTANCE)?; + pca.func_wrap( + INCREMENT_FN, + |mut cx: StoreContextMut<'_, Host>, (category, count): (u32, u32)| { + let host = cx.data_mut(); + if host.active { + if let Some(slot) = host.dynamic.get_mut(category as usize) { + *slot += u64::from(count); + } + } + Ok(()) + }, + )?; + + let mut builder = WasiCtxBuilder::new(); + builder.preopened_dir(working_dir, ".", DirPerms::READ, FilePerms::READ)?; + let host = Host { + wasi: builder.build(), + table: ResourceTable::new(), + active: false, + dynamic: [0; NUM_CATEGORIES], + }; + + let mut store = Store::new(engine, host); + if let Some(fuel) = fuel { + store.set_fuel(fuel)?; + } + + let instance = linker.instantiate(&mut store, &component)?; + + // Run `wasi:cli/run`'s `run`. Completing (with any exit status) and running + // out of fuel are both acceptable stopping points: in either case the host + // already holds the counts pushed before we stopped. + let command = Command::new(&mut store, &instance)?; + if let Err(e) = command.wasi_cli_run().call_run(&mut store) { + if !(fuel.is_some() && matches!(e.downcast_ref::(), Some(Trap::OutOfFuel))) { + return Err(e.into()); + } + } + + let host = store.data(); + for i in 0..NUM_CATEGORIES { + counts.dynamic_insts[i] += host.dynamic[i]; + counts.total_dynamic_insts += host.dynamic[i]; + } + + Ok(()) +} + +/// Create the Wasmtime engine used to run instrumented benchmarks. +pub(crate) fn make_engine(consume_fuel: bool) -> Result { + let mut config = wasmtime::Config::new(); + config.consume_fuel(consume_fuel); + config.compiler_inlining(true); + + // Do not verify the CLIF (enabled in debug builds; slow). + config.cranelift_debug_verifier(false); + + // Required for running some of our benchmarks. + config.wasm_gc(true); + config.wasm_function_references(true); + + // Enable Wasmtime's compilation cache. + if let Ok(cache) = wasmtime::Cache::new(wasmtime::CacheConfig::default()) { + config.cache(Some(cache)); + } + + let engine = wasmtime::Engine::new(&config)?; + Ok(engine) +} + +/// Whether `op` ends a basic block, i.e. the *next* operator begins a new +/// straight-line run (a "leader"). +/// +/// Structured control (`block`/`loop`/`if`/`else`/`end`), branches, returns, +/// `unreachable`, and exception control are boundaries; plain calls are not, +/// since they return and fall through. +/// +/// Note that being conservative is safe: splitting a block we didn't need to +/// still counts correctly, whereas *missing* a real boundary would let us count +/// instructions past a branch that doesn't execute. +fn is_block_boundary(op: &wasmparser::Operator<'_>) -> bool { + use wasmparser::Operator::*; + matches!( + op, + Block { .. } + | Loop { .. } + | If { .. } + | Else + | End + | Br { .. } + | BrIf { .. } + | BrTable { .. } + | Return + | ReturnCall { .. } + | ReturnCallIndirect { .. } + | ReturnCallRef { .. } + | Unreachable + | BrOnNull { .. } + | BrOnNonNull { .. } + | BrOnCast { .. } + | BrOnCastFail { .. } + | Throw { .. } + | ThrowRef + | Rethrow { .. } + | TryTable { .. } + | Try { .. } + | Catch { .. } + | CatchAll + | Delegate { .. } + ) +} + +/// One basic block of a function body: a maximal straight-line run of operators. +struct BasicBlock { + /// The operator index, within the function body, where this block starts. + start: u32, + /// Whether it begins at a function entry or the top of a loop, where the + /// component instrumentation flushes its counters to the host. + is_flush_point: bool, + /// How many operators of each [`Category`] the block contains. + counts: [u32; NUM_CATEGORIES], +} + +/// Partition a function body into [`BasicBlock`]s, tallying operators by category. +/// +/// This is shared by the core-module and component instrumentation so they both +/// increment counters once per basic block (in batch) rather than once per +/// operator. +fn basic_blocks(body: &wasmparser::FunctionBody<'_>) -> Result> { + let mut blocks: Vec = Vec::new(); + let mut reader = body.get_operators_reader()?; + let mut idx = 0u32; + let mut at_leader = true; + let mut prev_was_loop = false; + while !reader.eof() { + let op = reader.read()?; + if at_leader { + blocks.push(BasicBlock { + start: idx, + is_flush_point: idx == 0 || prev_was_loop, + counts: [0; NUM_CATEGORIES], + }); + } + blocks.last_mut().unwrap().counts[Category::for_op(&op)? as usize] += 1; + at_leader = is_block_boundary(&op); + prev_was_loop = matches!(op, wasmparser::Operator::Loop { .. }); + idx += 1; + } + Ok(blocks) +} + +/// Rewrite a core module so it counts how many instructions of each [`Category`] +/// it executes, tallying them once per basic block (in batch) rather than once +/// per operator. +/// +/// The counters are appended module globals; how they are gated and read back +/// depends on `flush`: +/// +/// * `flush == false` (standalone core-module benchmarks): the counters are +/// `i64`, and each batch is multiplied by an appended `is-benchmarking-active` +/// global (set to `1` after `bench.start`, `0` before `bench.end`) so only the +/// measured region is counted. One exported getter per category lets the host +/// read the totals back after the run. +/// +/// * `flush == true` (components' core modules): the counters are `i32`, and +/// the module imports `sightglass-pca.increment-instruction-count` as +/// function index 0. At each function entry and loop top it pushes every +/// (used) counter to that import and resets it (so counts reach the host even +/// when we hit an out-of-fuel trap and don't finish execution) except in +/// canonical ABI `realloc` functions, which only accumulate. The host gates +/// on the active region itself. +/// +/// Appended items take the end of their index spaces, so the only renumbering is +/// the `+1` function shift that flushing's prepended import requires. +fn instrument_core_module(wasm: &[u8], flush: bool) -> Result { + use wasm_encoder::reencode::{Error as ReencodeError, Reencode}; + use wasm_encoder::{ + CodeSection, ConstExpr, DataCountSection, DataSection, ElementSection, EntityType, + ExportKind, ExportSection, Function, FunctionSection, GlobalSection, GlobalType, + ImportSection, Instruction, MemorySection, Module, StartSection, TableSection, TagSection, + TypeSection, ValType, + }; + + // Shift function references by one only when flushing prepends its import. + struct Shift(u32); + impl Reencode for Shift { + type Error = std::convert::Infallible; + fn function_index(&mut self, i: u32) -> Result> { + Ok(i + self.0) + } + } + let reencoder = &mut Shift(if flush { 1 } else { 0 }); + + // When flushing, pre-scan which categories occur so we only flush counters + // that can be non-zero. + let mut used = [false; NUM_CATEGORIES]; + if flush { + for payload in wasmparser::Parser::new(0).parse_all(wasm) { + if let Payload::CodeSectionEntry(body) = payload.context("failed to parse Wasm")? { + let mut reader = body.get_operators_reader()?; + while !reader.eof() { + used[Category::for_op(&reader.read()?)? as usize] = true; + } + } + } + } + + let mut types: Option = None; + let mut imports: Option = None; + let mut functions: Option = None; + let mut tables: Option = None; + let mut memories: Option = None; + let mut tags: Option = None; + let mut globals: Option = None; + let mut exports: Option = None; + let mut start: Option = None; + let mut elements: Option = None; + let mut data_count: Option = None; + let mut code: Option = None; + let mut data: Option = None; + + // Index-space sizes (final by the code section, which follows them all). For + // `flush == false` we also record the benchmarking-hook function indices; for + // `flush == true`, the original indices of `realloc`-style functions that + // must not flush. + let mut num_types = 0u32; + let mut imported_funcs = 0u32; + let mut imported_globals = 0u32; + let mut defined_funcs = 0u32; + let mut defined_globals = 0u32; + let mut bench_start_func = None; + let mut bench_end_func = None; + let mut code_entry = 0u32; + let mut no_flush_funcs: Vec = Vec::new(); + + for payload in wasmparser::Parser::new(0).parse_all(wasm) { + match payload.context("failed to parse Wasm")? { + Payload::TypeSection(s) => { + num_types += s.count(); + let sec = types.get_or_insert_with(TypeSection::new); + reencoder.parse_type_section(sec, s)?; + } + Payload::ImportSection(s) => { + let sec = imports.get_or_insert_with(ImportSection::new); + if flush { + // Prepend the `increment` import so it is function index 0. + sec.import(PCA_INSTANCE, INCREMENT_FN, EntityType::Function(num_types)); + } + for import in s { + let import = import?; + match import.ty { + wasmparser::TypeRef::Func(_) => { + if !flush && import.module == "bench" { + if import.name == "start" { + bench_start_func = Some(imported_funcs); + } else if import.name == "end" { + bench_end_func = Some(imported_funcs); + } + } + imported_funcs += 1; + } + wasmparser::TypeRef::Global(_) => imported_globals += 1, + _ => {} + } + reencoder.parse_import(sec, import)?; + } + } + Payload::FunctionSection(s) => { + defined_funcs += s.count(); + let sec = functions.get_or_insert_with(FunctionSection::new); + reencoder.parse_function_section(sec, s)?; + } + Payload::TableSection(s) => { + let sec = tables.get_or_insert_with(TableSection::new); + reencoder.parse_table_section(sec, s)?; + } + Payload::MemorySection(s) => { + let sec = memories.get_or_insert_with(MemorySection::new); + reencoder.parse_memory_section(sec, s)?; + } + Payload::TagSection(s) => { + let sec = tags.get_or_insert_with(TagSection::new); + reencoder.parse_tag_section(sec, s)?; + } + Payload::GlobalSection(s) => { + defined_globals += s.count(); + let sec = globals.get_or_insert_with(GlobalSection::new); + reencoder.parse_global_section(sec, s)?; + } + Payload::ExportSection(s) => { + let sec = exports.get_or_insert_with(ExportSection::new); + for export in s { + let export = export?; + if flush + && matches!(export.kind, wasmparser::ExternalKind::Func) + && NO_FLUSH_EXPORTS.contains(&export.name) + { + no_flush_funcs.push(export.index); + } + reencoder.parse_export(sec, export)?; + } + } + Payload::StartSection { func, .. } => { + start = Some(reencoder.function_index(func)?); + } + Payload::ElementSection(s) => { + let sec = elements.get_or_insert_with(ElementSection::new); + reencoder.parse_element_section(sec, s)?; + } + Payload::DataCountSection { count, .. } => { + data_count = Some(count); + } + Payload::CodeSectionEntry(body) => { + let base_global = imported_globals + defined_globals; + let active_global = base_global + NUM_CATEGORIES as u32; + // `realloc`-style functions accumulate counts but never flush. + let no_flush = flush && no_flush_funcs.contains(&(imported_funcs + code_entry)); + code_entry += 1; + + // Tally instructions once per basic block (in batch): at each + // block's top, add `count * active` to each category's counter, + // so only instructions executed between `bench.start` and + // `bench.end` are counted. + let blocks = basic_blocks(&body)?; + let mut next_block = 0; + let mut idx = 0u32; + + let mut func = reencoder.new_function_with_parsed_locals(&body)?; + let mut reader = body.get_operators_reader()?; + while !reader.eof() { + let op = reader.read()?; + + if next_block < blocks.len() && blocks[next_block].start == idx { + let block = &blocks[next_block]; + next_block += 1; + + // When flushing, push every counter to the host and reset + // it at function entries and loop tops (but not in the + // functions that can't leave the component). + if flush && block.is_flush_point && !no_flush { + for (cat, is_used) in used.iter().copied().enumerate() { + if is_used { + let counter = base_global + cat as u32; + func.instruction(&Instruction::I32Const(cat as i32)); + func.instruction(&Instruction::GlobalGet(counter)); + func.instruction(&Instruction::Call(0)); + func.instruction(&Instruction::I32Const(0)); + func.instruction(&Instruction::GlobalSet(counter)); + } + } + } + + // Tally this block's operators into the counters in batch. + // When flushing, count unconditionally (the host gates on + // the active region); otherwise multiply by `active`. + for cat in 0..NUM_CATEGORIES { + let count = block.counts[cat]; + if count > 0 { + let counter = base_global + cat as u32; + func.instruction(&Instruction::GlobalGet(counter)); + if flush { + func.instruction(&Instruction::I32Const(count as i32)); + func.instruction(&Instruction::I32Add); + } else { + func.instruction(&Instruction::GlobalGet(active_global)); + func.instruction(&Instruction::I64Const(i64::from(count))); + func.instruction(&Instruction::I64Mul); + func.instruction(&Instruction::I64Add); + } + func.instruction(&Instruction::GlobalSet(counter)); + } + } + } + + // Standalone mode gates locally: toggle the active flag around + // the benchmarking hooks. + if flush { + func.instruction(&reencoder.instruction(op)?); + } else { + let call_target = match &op { + wasmparser::Operator::Call { function_index } => Some(*function_index), + _ => None, + }; + if bench_end_func.is_some() && call_target == bench_end_func { + func.instruction(&Instruction::I64Const(0)); + func.instruction(&Instruction::GlobalSet(active_global)); + } + func.instruction(&reencoder.instruction(op)?); + if bench_start_func.is_some() && call_target == bench_start_func { + func.instruction(&Instruction::I64Const(1)); + func.instruction(&Instruction::GlobalSet(active_global)); + } + } + idx += 1; + } + code.get_or_insert_with(CodeSection::new).function(&func); + } + Payload::DataSection(s) => { + let sec = data.get_or_insert_with(DataSection::new); + reencoder.parse_data_section(sec, s)?; + } + + // The module header and sections that need no rewriting. Code + // section entries are handled above. + Payload::Version { .. } + | Payload::CustomSection(_) + | Payload::CodeSectionStart { .. } + | Payload::End(_) => {} + + // Component-model sections (which we can't instrument) and any + // unknown/future payloads. + payload => bail!("unsupported Wasm payload: {payload:?}"), + } + } + + let base_global = imported_globals + defined_globals; + + // Append the one type we need: `(i32, i32) -> ()` for the `increment` import + // (flush), or `() -> i64` for the counter getters (standalone). + { + let ty = types.get_or_insert_with(TypeSection::new).ty(); + if flush { + ty.function([ValType::I32, ValType::I32], []); + } else { + ty.function([], [ValType::I64]); + } + } + + // Make sure the `increment` import exists even if the module had no imports. + if flush && imports.is_none() { + let mut sec = ImportSection::new(); + sec.import(PCA_INSTANCE, INCREMENT_FN, EntityType::Function(num_types)); + imports = Some(sec); + } + + // Append the counter globals: `i32` when flushing, or `i64` plus the active + // flag and one exported getter per category when not. + if flush { + let i32_global = GlobalType { + val_type: ValType::I32, + mutable: true, + shared: false, + }; + let globals = globals.get_or_insert_with(GlobalSection::new); + for _ in 0..NUM_CATEGORIES { + globals.global(i32_global, &ConstExpr::i32_const(0)); + } + } else { + let i64_global = GlobalType { + val_type: ValType::I64, + mutable: true, + shared: false, + }; + let getter_type = num_types; + let getter_func_base = imported_funcs + defined_funcs; + let globals = globals.get_or_insert_with(GlobalSection::new); + let functions = functions.get_or_insert_with(FunctionSection::new); + let exports = exports.get_or_insert_with(ExportSection::new); + let code = code.get_or_insert_with(CodeSection::new); + for i in 0..NUM_CATEGORIES as u32 { + // The counter global itself, plus an exported getter the host calls + // to read it back after the run. + globals.global(i64_global, &ConstExpr::i64_const(0)); + functions.function(getter_type); + let mut getter = Function::new([]); + getter.instruction(&Instruction::GlobalGet(base_global + i)); + getter.instruction(&Instruction::End); + code.function(&getter); + exports.export( + &format!("{COUNTER_PREFIX}{i}"), + ExportKind::Func, + getter_func_base + i, + ); + } + // The `is-benchmarking-active` global (index base_global + NUM_CATEGORIES). + globals.global(i64_global, &ConstExpr::i64_const(0)); + } + + // Reassemble the module, emitting sections in the canonical order. + let mut module = Module::new(); + if let Some(s) = &types { + module.section(s); + } + if let Some(s) = &imports { + module.section(s); + } + if let Some(s) = &functions { + module.section(s); + } + if let Some(s) = &tables { + module.section(s); + } + if let Some(s) = &memories { + module.section(s); + } + if let Some(s) = &tags { + module.section(s); + } + if let Some(s) = &globals { + module.section(s); + } + if let Some(s) = &exports { + module.section(s); + } + if let Some(function_index) = start { + module.section(&StartSection { function_index }); + } + if let Some(s) = &elements { + module.section(s); + } + if let Some(count) = data_count { + module.section(&DataCountSection { count }); + } + if let Some(s) = &code { + module.section(s); + } + if let Some(s) = &data { + module.section(s); + } + + Ok(module) +} + +/// Run the instrumented module once and accumulate its category counters into +/// `counts`. +fn run_instrumented_core_module( + instrumented: &[u8], + counts: &mut Counts, + engine: &wasmtime::Engine, + working_dir: &Path, + fuel: Option, +) -> Result<()> { + use wasmtime::{Caller, Linker, Module, Store, Trap}; + use wasmtime_wasi::p1::{self, WasiP1Ctx}; + use wasmtime_wasi::{DirPerms, FilePerms, I32Exit, WasiCtxBuilder}; + + let module = Module::new(engine, instrumented)?; + + // Provide real WASI and the benchmarking hooks; together these cover every + // import a benchmark needs. + let mut linker: Linker = Linker::new(engine); + p1::add_to_linker_sync(&mut linker, |cx| cx)?; + linker.func_wrap("bench", "start", |_: Caller<'_, WasiP1Ctx>| {})?; + linker.func_wrap("bench", "end", |_: Caller<'_, WasiP1Ctx>| {})?; + + // Configure the WASI context the way `wasm_bench_create` does in + // `wasmtime`'s `bench-api`: preopen the benchmark's directory so it can read + // its workload, and forward the small-workload environment variable. + let mut builder = WasiCtxBuilder::new(); + builder.preopened_dir(working_dir, ".", DirPerms::READ, FilePerms::READ)?; + let wasi = builder.build_p1(); + + let mut store = Store::new(engine, wasi); + if let Some(fuel) = fuel { + store.set_fuel(fuel)?; + } + let instance = linker.instantiate(&mut store, &module)?; + + // Run the benchmark's entry point. Two non-error outcomes besides a normal + // return: a clean `proc_exit(0)` surfaces as an `I32Exit`, and when a fuel + // budget is set the benchmark may legitimately run out of fuel. + let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?; + if let Err(e) = start.call(&mut store, ()) { + if let Some(exit) = e.downcast_ref::() { + if exit.0 != 0 { + bail!("benchmark exited with non-zero code {}", exit.0); + } + } else if fuel.is_some() && matches!(e.downcast_ref::(), Some(Trap::OutOfFuel)) { + // Ran out of fuel; the counts gathered so far are what we want. + } else { + return Err(e.into()); + } + } + + // Reading a counter calls an exported Wasm function, which itself consumes + // fuel, so top the store back up first if we were running with a budget (the + // benchmark may have used all of it). + if fuel.is_some() { + store.set_fuel(NUM_CATEGORIES as u64 * 1024)?; + } + + // Read each category's execution count back via its exported getter. + let mut total = 0u64; + for i in 0..NUM_CATEGORIES { + let getter = + instance.get_typed_func::<(), i64>(&mut store, &format!("{COUNTER_PREFIX}{i}"))?; + let count = getter.call(&mut store, ())? as u64; + counts.dynamic_insts[i] += count; + total += count; + } + counts.total_dynamic_insts += total; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::accumulate_callgrind_counts; + use crate::pca_metrics::Counts; + use sightglass_data::{Engine, Measurement, Phase}; + use std::borrow::Cow; + + #[test] + fn accumulates_callgrind_measurements_by_event_name() { + let measurements = vec![ + measurement(Phase::Compilation, "instructions-retired", 999), + measurement(Phase::Execution, "instructions-retired", 200), + measurement(Phase::Execution, "conditional-branch-misses", 3), + measurement(Phase::Execution, "conditional-branches", 12), + measurement(Phase::Execution, "indirect-branch-misses", 1), + measurement(Phase::Execution, "indirect-branches", 5), + measurement(Phase::Execution, "data-reads", 20), + measurement(Phase::Execution, "data-writes", 10), + measurement(Phase::Execution, "l1-dcache-read-misses", 4), + measurement(Phase::Execution, "l1-dcache-write-misses", 2), + measurement(Phase::Execution, "ll-dcache-read-misses", 1), + measurement(Phase::Execution, "ll-dcache-write-misses", 1), + measurement(Phase::Execution, "l1-icache-misses", 8), + measurement(Phase::Execution, "ll-icache-misses", 6), + ]; + + let mut counts = Counts::default(); + accumulate_callgrind_counts(&measurements, &mut counts); + + assert_eq!(counts.instructions_retired, 200); + assert_eq!(counts.conditional_branch_misses, 3); + assert_eq!(counts.conditional_branches, 12); + assert_eq!(counts.indirect_branch_misses, 1); + assert_eq!(counts.indirect_branches, 5); + assert_eq!(counts.data_reads, 20); + assert_eq!(counts.data_writes, 10); + assert_eq!(counts.l1_dcache_read_misses, 4); + assert_eq!(counts.l1_dcache_write_misses, 2); + assert_eq!(counts.ll_dcache_read_misses, 1); + assert_eq!(counts.ll_dcache_write_misses, 1); + assert_eq!(counts.l1_icache_misses, 8); + assert_eq!(counts.ll_icache_misses, 6); + } + + fn measurement(phase: Phase, event: &'static str, count: u64) -> Measurement<'static> { + Measurement { + arch: Cow::Borrowed("x86_64"), + engine: Engine { + name: Cow::Borrowed("engine"), + flags: None, + }, + wasm: Cow::Borrowed("benchmark.wasm"), + process: 0, + iteration: 0, + phase, + event: Cow::Borrowed(event), + count, + } + } +} diff --git a/crates/cli/src/pca_metrics/dynamic_metrics/component.rs b/crates/cli/src/pca_metrics/dynamic_metrics/component.rs new file mode 100644 index 00000000..f5daf000 --- /dev/null +++ b/crates/cli/src/pca_metrics/dynamic_metrics/component.rs @@ -0,0 +1,412 @@ +//! Component-model support for dynamic PCA metrics. +//! +//! Counting executed instructions inside a component is harder than inside a +//! core module: globals can't be exported across a component boundary, +//! out-of-fuel traps leave the "may enter" flag unset so attempts to call +//! functions (e.g. to read fuel counts) after trap will always re-trap, and the +//! benchmarking hooks (`bench.start`/`bench.end`) are lowered imports that the +//! leaf core modules might not import or otherwise be able to observe directly. +//! +//! Instead, an instrumented component *pushes* its counts to the host as it +//! runs: +//! +//! * The root component (and every sub-component) imports a `sightglass-pca` +//! instance exporting `increment-instruction-count: func(category, count)`. +//! The host (see [`super::run_component`]) tallies those pushes, but only +//! while benchmarking is active: it flips an "active" flag in its own +//! `bench.start`/`bench.end` implementations. +//! +//! * Each core module keeps one defined `i32` counter global per category, +//! incremented in batch at the top of every basic block. +//! +//! * At the start of each function and the top of each loop, a core module +//! flushes those globals to the host via `increment-instruction-count` and +//! resets them to zero. (Functions used as the canonical ABI `realloc` run +//! with the component's "may leave" flag cleared, so they only accumulate +//! into the globals and never flush; the next function flushes their counts +//! later.) +//! +//! Because the counts live in host memory, we recover them even when the +//! benchmark runs out of fuel and traps mid-run. + +use super::{INCREMENT_FN, PCA_INSTANCE}; +use anyhow::Result; + +/// An empty set of canonical lowering options (our scalar `increment` import +/// needs no memory or realloc). +fn no_opts() -> impl ExactSizeIterator { + core::iter::empty() +} + +/// Emit the `sightglass-pca` instance type into `types` (as the next component +/// type) and import it into `imports` (as the next component instance). +/// +/// Both the root and every sub-component import this instance; the root only +/// forwards it, while sub-components additionally alias + lower its function. +fn import_pca_instance( + types: &mut wasm_encoder::ComponentTypeSection, + imports: &mut wasm_encoder::ComponentImportSection, +) { + use wasm_encoder::{ComponentTypeRef, ComponentValType, InstanceType, PrimitiveValType}; + + let u32_ty = ComponentValType::Primitive(PrimitiveValType::U32); + let mut pca_ty = InstanceType::new(); + pca_ty + .ty() + .function() + .params([("category", u32_ty), ("count", u32_ty)]) + .result(None); + pca_ty.export(INCREMENT_FN, ComponentTypeRef::Func(0)); + types.instance(&pca_ty); + imports.import(PCA_INSTANCE, ComponentTypeRef::Instance(0)); +} + +/// Instrument a component for the +/// dynamic instruction mix. +/// +/// Every component (the root or, recursively, any sub-component) is transformed +/// the same way: it imports a `sightglass-pca` instance and threads +/// `increment-instruction-count` down into everything that needs it. +/// +/// We prepend, before the component's original content, five items that take +/// the lowest index in each space (so every original reference shifts up by +/// one, applied only at depth 0; nested type scopes keep their own index +/// spaces): +/// +/// * Component type 0: the `sightglass-pca` instance type +/// * Component instance 0: an import of the `sightglass-pca` instance +/// * Component func 0: an alias of `sightglass-pca.increment-instruction-count` +/// * Core func 0: a `canon lower` of that function +/// * Core instance 0: a core instance of just that lowered function +/// +/// A component with no core modules of its own (e.g. a root component that just +/// instantiates and links subcomponents) simply never uses the lowered function +/// or core instance. +pub(super) fn instrument_component(wasm: &[u8]) -> Result> { + use wasm_encoder::reencode::{ + component_utils, Error as ReencodeError, Reencode, ReencodeComponent, + }; + use wasm_encoder::{ + Alias, CanonicalFunctionSection, Component, ComponentAliasSection, ComponentExportKind, + ComponentImportSection, ComponentInstanceSection, ComponentSectionId, ComponentTypeSection, + ExportKind, InstanceSection, ModuleArg, ModuleSection, RawSection, + }; + + struct Instrumenter { + depth: u32, + prepended: bool, + } + + impl Instrumenter { + /// Shift an index up by one, but only in this component's own (depth-0) + /// index spaces, not inside nested type declarations. + fn shift(&self, i: u32) -> u32 { + if self.depth == 0 { + i + 1 + } else { + i + } + } + + fn emit_prelude(component: &mut Component) { + let mut types = ComponentTypeSection::new(); + let mut imports = ComponentImportSection::new(); + import_pca_instance(&mut types, &mut imports); + component.section(&types); + component.section(&imports); + + // Alias `increment-instruction-count` out of the instance + // (component func 0), lower it (core func 0), and wrap it in a core + // instance (core instance 0) for the core modules to import. + let mut aliases = ComponentAliasSection::new(); + aliases.alias(Alias::InstanceExport { + instance: 0, + kind: ComponentExportKind::Func, + name: INCREMENT_FN, + }); + component.section(&aliases); + + let mut canon = CanonicalFunctionSection::new(); + canon.lower(0, no_opts()); + component.section(&canon); + + let mut instances = InstanceSection::new(); + instances.export_items([(INCREMENT_FN, ExportKind::Func, 0u32)]); + component.section(&instances); + } + } + + impl Reencode for Instrumenter { + type Error = anyhow::Error; + fn function_index(&mut self, i: u32) -> Result> { + Ok(self.shift(i)) + } + } + + impl ReencodeComponent for Instrumenter { + fn component_func_index(&mut self, i: u32) -> u32 { + self.shift(i) + } + fn component_type_index(&mut self, i: u32) -> u32 { + self.shift(i) + } + fn component_instance_index(&mut self, i: u32) -> u32 { + self.shift(i) + } + fn instance_index(&mut self, i: u32) -> u32 { + self.shift(i) + } + fn outer_component_type_index(&mut self, count: u32, i: u32) -> u32 { + // An `alias outer (type ...)` reaching up to this component's own + // (depth-0) component type space must account for the prepended + // type. + if count == self.depth { + i + 1 + } else { + i + } + } + fn push_depth(&mut self) { + self.depth += 1; + } + fn pop_depth(&mut self) { + self.depth -= 1; + } + + fn parse_component_payload( + &mut self, + component: &mut Component, + payload: wasmparser::Payload<'_>, + whole: &[u8], + ) -> Result<(), ReencodeError> { + // Prepend the prelude after any leading sub-components / core modules, + // so it lands in the import region (and is component instance 0). + if !self.prepended + && !matches!( + payload, + wasmparser::Payload::Version { .. } + | wasmparser::Payload::ComponentSection { .. } + | wasmparser::Payload::ModuleSection { .. } + ) + { + self.prepended = true; + Self::emit_prelude(component); + } + component_utils::parse_component_payload(self, component, payload, whole) + } + + fn parse_component_subcomponent( + &mut self, + component: &mut Component, + _parser: wasmparser::Parser, + subcomponent: &[u8], + _whole: &[u8], + ) -> Result<(), ReencodeError> { + // A nested sub-component is instrumented exactly like the root. + let instrumented = + instrument_component(subcomponent).map_err(ReencodeError::UserError)?; + component.section(&RawSection { + id: ComponentSectionId::Component.into(), + data: &instrumented, + }); + Ok(()) + } + + fn parse_component_instance( + &mut self, + instances: &mut ComponentInstanceSection, + instance: wasmparser::ComponentInstance<'_>, + ) -> Result<(), ReencodeError> { + match instance { + wasmparser::ComponentInstance::Instantiate { + component_index, + args, + } => { + let mut new_args: Vec<(&str, ComponentExportKind, u32)> = args + .iter() + .map(|arg| { + ( + arg.name, + arg.kind.into(), + self.component_external_index(arg.kind, arg.index), + ) + }) + .collect(); + // Pass the `sightglass-pca` *instance* (component instance 0) + // to each instantiated sub-component. + new_args.push((PCA_INSTANCE, ComponentExportKind::Instance, 0)); + instances.instantiate(self.component_index(component_index), new_args); + } + wasmparser::ComponentInstance::FromExports(exports) => { + let items: Vec<(&str, ComponentExportKind, u32)> = exports + .iter() + .map(|e| { + ( + e.name.0, + e.kind.into(), + self.component_external_index(e.kind, e.index), + ) + }) + .collect(); + instances.export_items(items); + } + } + Ok(()) + } + + fn parse_component_submodule( + &mut self, + component: &mut Component, + _parser: wasmparser::Parser, + module: &[u8], + ) -> Result<(), ReencodeError> { + // Reencode the core module to count and flush its instructions. + let instrumented = + super::instrument_core_module(module, true).map_err(ReencodeError::UserError)?; + component.section(&ModuleSection(&instrumented)); + Ok(()) + } + + fn parse_instance( + &mut self, + instances: &mut InstanceSection, + instance: wasmparser::Instance<'_>, + ) -> Result<(), ReencodeError> { + match instance { + wasmparser::Instance::Instantiate { module_index, args } => { + let mut new_args: Vec<(&str, ModuleArg)> = args + .iter() + .map(|arg| match arg.kind { + wasmparser::InstantiationArgKind::Instance => ( + arg.name, + ModuleArg::Instance(self.instance_index(arg.index)), + ), + }) + .collect(); + // Supply the instrumented module's `sightglass-pca` import + // from the prepended core instance (index 0). + new_args.push((PCA_INSTANCE, ModuleArg::Instance(0))); + instances.instantiate(self.module_index(module_index), new_args); + } + wasmparser::Instance::FromExports(exports) => { + let exports = exports + .iter() + .map(|export| { + Ok(( + export.name, + self.export_kind(export.kind)?, + self.external_index(export.kind, export.index)?, + )) + }) + .collect::, ReencodeError>>()?; + instances.export_items(exports); + } + } + Ok(()) + } + } + + let mut component = Component::new(); + let mut instrumenter = Instrumenter { + depth: 0, + prepended: false, + }; + instrumenter + .parse_component(&mut component, wasmparser::Parser::new(0), wasm) + .map_err(|e| anyhow::anyhow!("failed to reencode component: {e}"))?; + Ok(component.finish()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Instrumenting a core module keeps it valid and behaving the same + /// (proving the function-index shift is applied everywhere), accumulates + /// each block's operators into the counter globals, and flushes them to the + /// imported `increment-instruction-count` at the next function entry. + #[test] + fn instrumented_core_module_flushes_counts() -> anyhow::Result<()> { + use wasm_encoder::{ + CodeSection, ExportKind, ExportSection, Function, FunctionSection, Instruction, Module, + TypeSection, ValType, + }; + + // Two functions: `work() -> i32` = `1 + 2` (four operators), and an + // empty `flush()`. Running `work` accumulates its counts into the + // globals; calling `flush` afterward pushes them to the host at + // `flush`'s entry. + let mut types = TypeSection::new(); + types.ty().function([], [ValType::I32]); // 0: work + types.ty().function([], []); // 1: flush + let mut funcs = FunctionSection::new(); + funcs.function(0); + funcs.function(1); + let mut exports = ExportSection::new(); + exports.export("work", ExportKind::Func, 0); + exports.export("flush", ExportKind::Func, 1); + let mut code = CodeSection::new(); + let mut work = Function::new([]); + work.instruction(&Instruction::I32Const(1)); + work.instruction(&Instruction::I32Const(2)); + work.instruction(&Instruction::I32Add); + work.instruction(&Instruction::End); + code.function(&work); + let mut flush = Function::new([]); + flush.instruction(&Instruction::End); + code.function(&flush); + let mut m = Module::new(); + m.section(&types); + m.section(&funcs); + m.section(&exports); + m.section(&code); + let input = m.finish(); + + let instrumented = super::super::instrument_core_module(&input, true)?.finish(); + + let engine = wasmtime::Engine::default(); + let module = wasmtime::Module::new(&engine, &instrumented)?; + let mut store = wasmtime::Store::new(&engine, 0u64); + let mut linker = wasmtime::Linker::new(&engine); + linker.func_wrap( + super::super::PCA_INSTANCE, + super::super::INCREMENT_FN, + |mut caller: wasmtime::Caller<'_, u64>, _category: u32, count: u32| { + *caller.data_mut() += u64::from(count); + }, + )?; + let instance = linker.instantiate(&mut store, &module)?; + let work = instance.get_typed_func::<(), i32>(&mut store, "work")?; + let flush = instance.get_typed_func::<(), ()>(&mut store, "flush")?; + + // `work` still computes 1 + 2, and its four operators are flushed to + // the host when `flush`'s entry pushes the accumulated counters. + assert_eq!(work.call(&mut store, ())?, 3); + flush.call(&mut store, ())?; + assert_eq!( + *store.data(), + 4, + "work's four operators flushed to the host" + ); + + Ok(()) + } + + /// Instrumenting the real `cm-online-stats` component must produce a + /// component that still validates. This exercises the whole transform + /// without running the (multi-million iteration) benchmark. + #[test] + fn instrumented_component_validates() -> anyhow::Result<()> { + let path = "../../benchmarks/cm-online-stats/cm-online-stats.wasm"; + let wasm = match std::fs::read(path) { + Ok(wasm) => wasm, + // The benchmark isn't present in this checkout; nothing to test. + Err(_) => return Ok(()), + }; + + let instrumented = instrument_component(&wasm)?; + let engine = super::super::make_engine(false)?; + wasmtime::component::Component::new(&engine, &instrumented)?; + Ok(()) + } +} diff --git a/crates/cli/src/pca_metrics/static_metrics.rs b/crates/cli/src/pca_metrics/static_metrics.rs new file mode 100644 index 00000000..148ba8c0 --- /dev/null +++ b/crates/cli/src/pca_metrics/static_metrics.rs @@ -0,0 +1,70 @@ +//! Static PCA metrics: entity counts and the static instruction mix, computed +//! by parsing the Wasm module. + +use super::category::Category; +use super::Counts; +use anyhow::{bail, Context, Result}; +use wasmparser::Payload; + +/// Compute the static metrics for `wasm`, accumulating them into `counts`. +pub(crate) fn static_metrics(wasm: &[u8], counts: &mut Counts) -> Result<()> { + eprintln!("> static metrics"); + let mut parser = wasmparser::Parser::new(0); + parser.set_features(wasmparser::WasmFeatures::all()); + + for payload in parser.parse_all(wasm) { + match payload.context("failed to parse Wasm")? { + // Entity counts. + Payload::TypeSection(s) => counts.type_count += u64::from(s.count()), + Payload::CoreTypeSection(s) => counts.type_count += u64::from(s.count()), + Payload::FunctionSection(s) => counts.function_count += u64::from(s.count()), + Payload::TableSection(s) => counts.table_count += u64::from(s.count()), + Payload::MemorySection(s) => counts.memory_count += u64::from(s.count()), + Payload::TagSection(s) => counts.tag_count += u64::from(s.count()), + Payload::GlobalSection(s) => counts.global_count += u64::from(s.count()), + Payload::ElementSection(s) => counts.elem_segment_count += u64::from(s.count()), + Payload::DataSection(s) => counts.data_segment_count += u64::from(s.count()), + Payload::ModuleSection { .. } => counts.module_count += 1, + Payload::InstanceSection(s) => counts.core_instance_count += u64::from(s.count()), + Payload::ComponentSection { .. } => counts.component_count += 1, + Payload::ComponentInstanceSection(s) => { + counts.component_instance_count += u64::from(s.count()) + } + Payload::ComponentTypeSection(s) => counts.component_type_count += u64::from(s.count()), + Payload::ComponentCanonicalSection(s) => { + counts.component_canon_function_count += u64::from(s.count()) + } + + // The static instruction mix. + Payload::CodeSectionEntry(body) => { + let mut reader = body.get_operators_reader()?; + while !reader.eof() { + let op = reader.read()?; + counts.static_insts[Category::for_op(&op)? as usize] += 1; + counts.total_static_insts += 1; + } + } + + // Sections that don't contribute to the metrics we track. + Payload::Version { .. } + | Payload::ImportSection(_) + | Payload::ExportSection(_) + | Payload::StartSection { .. } + | Payload::DataCountSection { .. } + | Payload::CodeSectionStart { .. } + | Payload::CustomSection(_) + | Payload::ComponentImportSection(_) + | Payload::ComponentExportSection(_) + | Payload::ComponentAliasSection(_) + | Payload::ComponentStartSection { .. } + | Payload::UnknownSection { .. } + | Payload::End(_) => {} + + // `Payload` is `#[non_exhaustive]`, so we must handle unknown + // payloads; reaching this means a section kind we don't know about. + payload => bail!("unknown Wasm payload: {payload:?}"), + } + } + + Ok(()) +} diff --git a/crates/cli/tests/all/main.rs b/crates/cli/tests/all/main.rs index 0f8eaea5..0fd72683 100644 --- a/crates/cli/tests/all/main.rs +++ b/crates/cli/tests/all/main.rs @@ -1,6 +1,7 @@ mod benchmark; mod fingerprint; mod help; +mod pca_metrics; mod report; mod upload; mod util; diff --git a/crates/cli/tests/all/pca_metrics.rs b/crates/cli/tests/all/pca_metrics.rs new file mode 100644 index 00000000..d7cc152b --- /dev/null +++ b/crates/cli/tests/all/pca_metrics.rs @@ -0,0 +1,153 @@ +//! Test `sightglass-cli pca-metrics`. + +use super::util::{benchmark, sightglass_cli, test_engine}; +use assert_cmd::prelude::*; +use std::path::{Path, PathBuf}; + +/// Recursively collect every `*.wasm` file under `dir`. +fn collect_wasms(dir: &Path, out: &mut Vec) { + for entry in std::fs::read_dir(dir).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + collect_wasms(&path, out); + } else if path.extension().and_then(|e| e.to_str()) == Some("wasm") { + out.push(path); + } + } +} + +/// Every `benchmarks/**/*.wasm` file, relative to this crate's directory (the +/// working directory when tests run). +fn all_benchmark_wasms() -> Vec { + let mut wasms = Vec::new(); + collect_wasms(Path::new("../../benchmarks"), &mut wasms); + // The `image-classification` benchmark imports `wasi_ephemeral_nn`, which we + // don't provide a host implementation for, so it can't be run here. + wasms.retain(|p| { + !p.components() + .any(|c| c.as_os_str() == "image-classification") + }); + wasms.sort(); + assert!( + !wasms.is_empty(), + "expected to find benchmark `.wasm` files under ../../benchmarks" + ); + wasms +} + +/// `sightglass-cli pca-metrics` should succeed on every benchmark `.wasm` we +/// ship and emit parseable CSV. +/// +/// All benchmarks are passed in a single invocation so the (relatively +/// expensive) Wasmtime engine used for dynamic metrics is created once and +/// reused; the command processes the files one at a time. We cap each benchmark +/// to a small fuel budget so the test runs quickly rather than executing every +/// benchmark's full workload. +#[test] +fn pca_metrics_succeeds_on_all_benchmarks() { + let wasms = all_benchmark_wasms(); + + let assert = sightglass_cli() + .arg("pca-metrics") + .arg("--engine") + .arg(test_engine()) + .arg("--fuel") + .arg("10000") + .args(&wasms) + .assert() + .success(); + + // The output should parse as CSV: a `benchmark` column followed by rows + // that all deserialize. + let stdout = &assert.get_output().stdout; + let mut reader = csv::Reader::from_reader(stdout.as_slice()); + + let headers: Vec = reader + .headers() + .expect("output should have a CSV header") + .iter() + .map(str::to_string) + .collect(); + assert_eq!( + headers.first().map(String::as_str), + Some("benchmark"), + "first CSV column should be `benchmark`" + ); + + let mut rows = 0; + for record in reader.records() { + let record = record.expect("every CSV row should parse"); + rows += 1; + + let mut static_sum = 0.0; + let mut dynamic_sum = 0.0; + for (name, field) in headers.iter().zip(record.iter()) { + if name == "benchmark" { + continue; + } + + let value: f64 = field + .parse() + .unwrap_or_else(|_| panic!("column `{name}` should be a number, got {field:?}")); + + if name.starts_with("static_") { + static_sum += value; + } else if name.starts_with("dynamic_") && name != "dynamic_total_inst_count" { + dynamic_sum += value; + } + } + + // The static instruction mix is a distribution over a module's + // instructions, so its ratios sum to ~1.0. + assert!( + (static_sum - 1.0).abs() < 1e-6, + "static instruction ratios should sum to ~1.0, got {static_sum}" + ); + // The dynamic instruction mix sums to ~1.0 when the benchmark executed + // instructions while benchmarking was active, or to ~0.0 when it ran out + // of fuel before reaching `bench.start`. + assert!( + dynamic_sum.abs() < 1e-6 || (dynamic_sum - 1.0).abs() < 1e-6, + "dynamic instruction ratios should sum to ~1.0 or ~0.0, got {dynamic_sum}" + ); + } + assert_eq!(rows, wasms.len(), "every wasm benchmark should have a row"); +} + +#[test] +fn pca_metrics_outputs_callgrind_ratio_columns() { + let assert = sightglass_cli() + .arg("pca-metrics") + .arg("--engine") + .arg(test_engine()) + .arg(benchmark("noop")) + .assert() + .success(); + + let stdout = &assert.get_output().stdout; + let headers: Vec = csv::Reader::from_reader(stdout.as_slice()) + .headers() + .expect("output should have a CSV header") + .iter() + .map(str::to_string) + .collect(); + + for name in [ + "wasm_insts_per_native_inst", + "conditional_branch_misses", + "conditional_branches", + "indirect_branch_misses", + "indirect_branches", + "l1_dcache_read_misses", + "l1_dcache_write_misses", + "ll_dcache_read_misses", + "ll_dcache_write_misses", + "l1_icache_misses", + "ll_icache_misses", + ] { + assert!( + headers.iter().any(|header| header == name), + "missing `{name}` column" + ); + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..70f60c64 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = '2024'