From 2f8f6bfeeaf1e7b05662840e9e7da5862c7a9c87 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Mon, 8 Jun 2026 12:14:45 -0700 Subject: [PATCH] Add a `pca-metrics` command This records various static and dynamic metrics for use in principal component analysis. Static metrics include: * Various entity counts: number of functions, number of instructions, number of data segments, etc... * Static: instruction mix: ratio of integer vs float vs memory load vs etc... instructions There are two categories of dynamic metrics: 1. Dynamic Wasm instruction mix: This is recorded via instrumenting the benchmark with counters for each instruction category and incrementing an instruction's associated counter as the program runs (with an optimization to batch these increments up once per basic block). 2. Callgrind-based metrics: The benchmark is run through callgrind with a fixed cache and branch predictor simulation configuration (so the metrics are independent of the host machine's microarchitecture) and then we report the cache read/write miss ratio, branch prediction miss ratio, number of Wasm instructions executed per native instruction, etc... --- Cargo.lock | 1492 ++++++++++++++++- Cargo.toml | 7 + crates/cli/Cargo.toml | 4 + crates/cli/src/main.rs | 4 + crates/cli/src/pca_metrics.rs | 447 +++++ crates/cli/src/pca_metrics/category.rs | 673 ++++++++ crates/cli/src/pca_metrics/dynamic_metrics.rs | 886 ++++++++++ .../pca_metrics/dynamic_metrics/component.rs | 412 +++++ crates/cli/src/pca_metrics/static_metrics.rs | 70 + crates/cli/tests/all/main.rs | 1 + crates/cli/tests/all/pca_metrics.rs | 153 ++ rustfmt.toml | 1 + 12 files changed, 4083 insertions(+), 67 deletions(-) create mode 100644 crates/cli/src/pca_metrics.rs create mode 100644 crates/cli/src/pca_metrics/category.rs create mode 100644 crates/cli/src/pca_metrics/dynamic_metrics.rs create mode 100644 crates/cli/src/pca_metrics/dynamic_metrics/component.rs create mode 100644 crates/cli/src/pca_metrics/static_metrics.rs create mode 100644 crates/cli/tests/all/pca_metrics.rs create mode 100644 rustfmt.toml 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'