From 254e71eb4ea21565355c6179dc1ef83a89e3494f Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:03:17 +0100 Subject: [PATCH 01/13] fix: move to iter_batched --- libs/@local/hashql/core/src/heap/allocator.rs | 6 + libs/@local/hashql/core/src/heap/mod.rs | 6 + libs/@local/hashql/mir/benches/transform.rs | 145 ++++++++++-------- 3 files changed, 94 insertions(+), 63 deletions(-) diff --git a/libs/@local/hashql/core/src/heap/allocator.rs b/libs/@local/hashql/core/src/heap/allocator.rs index a293611ee49..bf3058dd066 100644 --- a/libs/@local/hashql/core/src/heap/allocator.rs +++ b/libs/@local/hashql/core/src/heap/allocator.rs @@ -24,6 +24,12 @@ impl Allocator { Self(Bump::with_capacity(capacity)) } + /// Sets the allocation limit for the allocator. + #[inline] + pub(crate) fn set_allocation_limit(&self, capacity: Option) { + self.0.set_allocation_limit(capacity); + } + /// Allocates a value using a closure to avoid moving before allocation. #[inline] pub(crate) fn alloc_with(&self, func: impl FnOnce() -> T) -> &mut T { diff --git a/libs/@local/hashql/core/src/heap/mod.rs b/libs/@local/hashql/core/src/heap/mod.rs index fb2763751fc..7c4a474f53f 100644 --- a/libs/@local/hashql/core/src/heap/mod.rs +++ b/libs/@local/hashql/core/src/heap/mod.rs @@ -241,6 +241,12 @@ impl Heap { } } + /// Sets the allocation limit for the heap. + #[inline] + pub fn set_allocation_limit(&self, capacity: Option) { + self.inner.set_allocation_limit(capacity); + } + /// Allocates a value in the arena, returning a mutable reference. /// /// Only accepts types that do **not** require [`Drop`]. Types requiring destructors diff --git a/libs/@local/hashql/mir/benches/transform.rs b/libs/@local/hashql/mir/benches/transform.rs index 5918b2be04b..b091de2c4f0 100644 --- a/libs/@local/hashql/mir/benches/transform.rs +++ b/libs/@local/hashql/mir/benches/transform.rs @@ -2,10 +2,7 @@ #![test_runner(criterion::runner)] #![expect(clippy::min_ident_chars, clippy::many_single_char_names)] -use core::{hint::black_box, time::Duration}; -use std::time::Instant; - -use criterion::Criterion; +use criterion::{BatchSize, Bencher, Criterion}; use criterion_macro::criterion; use hashql_core::{ heap::{BumpAllocator as _, Heap, Scratch}, @@ -242,48 +239,74 @@ fn create_complex_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap builder.finish(1, TypeBuilder::synthetic(env).integer()) } -fn run_fn( - iters: u64, +#[expect(unsafe_code)] +#[inline] +fn run_bencher( + bencher: &mut Bencher, body: for<'heap> fn(&Environment<'heap>, &Interner<'heap>) -> Body<'heap>, mut func: impl for<'env, 'heap> FnMut(&mut MirContext<'env, 'heap>, &mut Body<'heap>), -) -> Duration { +) { + // NOTE: `heap` must not be moved or reassigned; `heap_ptr` assumes its address is stable + // for the entire duration of this function. let mut heap = Heap::new(); - let mut total = Duration::ZERO; - - for _ in 0..iters { - heap.reset(); - let env = Environment::new(&heap); - let interner = Interner::new(&heap); - let mut body = black_box(body(&env, &interner)); - - let mut context = MirContext { - heap: &heap, - env: &env, - interner: &interner, - diagnostics: DiagnosticIssues::new(), - }; - - let start = Instant::now(); - func(&mut context, &mut body); - total += start.elapsed(); - - drop(black_box(body)); - } - - total + let heap_ptr = &raw mut heap; + + // Using `iter_custom` here would be better, but codspeed doesn't support it yet. + // + // IMPORTANT: `BatchSize::PerIteration` is critical for soundness. Do NOT change this to + // `SmallInput`, `LargeInput`, or any other batch size. Doing so will cause undefined + // behavior (use-after-free of arena allocations). + bencher.iter_batched( + || { + // SAFETY: We create a `&mut Heap` from the raw pointer to call `reset()` and build + // the environment/interner/body. This is sound because: + // - `heap` outlives the entire `iter_batched` call (it's a local in the outer scope). + // - `BatchSize::PerIteration` ensures only one `(env, interner, body)` tuple exists at + // a time, and it is dropped before the next `setup()` call. + // - No other references to `heap` exist during this closure's execution. + // - This code runs single-threaded. + let heap = unsafe { &mut *heap_ptr }; + heap.reset(); + + let env = Environment::new(heap); + let interner = Interner::new(heap); + let body = body(&env, &interner); + + (env, interner, body) + }, + |(env, interner, mut body)| { + // SAFETY: We create a shared `&Heap` reference. This is sound because: + // - The `&mut Heap` from setup no longer exists (setup closure has returned) + // - The `env`, `interner`, and `body` already hold shared borrows of `heap` + // - Adding another `&Heap` is just shared-shared aliasing, which is allowed + let heap = unsafe { &*heap_ptr }; + + let mut context = MirContext { + heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }; + + func(&mut context, &mut body); + (env, interner, body) + }, + BatchSize::PerIteration, + ); } +#[inline] fn run( - iters: u64, + bencher: &mut Bencher, body: for<'heap> fn(&Environment<'heap>, &Interner<'heap>) -> Body<'heap>, mut pass: impl for<'env, 'heap> TransformPass<'env, 'heap>, -) -> Duration { - run_fn( - iters, +) { + run_bencher( + bencher, body, #[inline] |context, body| pass.run(context, body), - ) + ); } #[criterion] @@ -291,13 +314,15 @@ fn cfg_simplify(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("cfg_simplify"); group.bench_function("linear", |bencher| { - bencher.iter_custom(|iters| run(iters, create_linear_cfg, CfgSimplify::new())); + run(bencher, create_linear_cfg, CfgSimplify::new()); }); + group.bench_function("diamond", |bencher| { - bencher.iter_custom(|iters| run(iters, create_diamond_cfg, CfgSimplify::new())); + run(bencher, create_diamond_cfg, CfgSimplify::new()); }); + group.bench_function("complex", |bencher| { - bencher.iter_custom(|iters| run(iters, create_complex_cfg, CfgSimplify::new())); + run(bencher, create_complex_cfg, CfgSimplify::new()); }); } @@ -306,13 +331,13 @@ fn sroa(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("sroa"); group.bench_function("linear", |bencher| { - bencher.iter_custom(|iters| run(iters, create_linear_cfg, Sroa::new())); + run(bencher, create_linear_cfg, Sroa::new()); }); group.bench_function("diamond", |bencher| { - bencher.iter_custom(|iters| run(iters, create_diamond_cfg, Sroa::new())); + run(bencher, create_diamond_cfg, Sroa::new()); }); group.bench_function("complex", |bencher| { - bencher.iter_custom(|iters| run(iters, create_complex_cfg, Sroa::new())); + run(bencher, create_complex_cfg, Sroa::new()); }); } @@ -321,16 +346,16 @@ fn dse(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("dse"); group.bench_function("dead stores", |bencher| { - bencher.iter_custom(|iters| run(iters, create_dead_store_cfg, DeadStoreElimination::new())); + run(bencher, create_dead_store_cfg, DeadStoreElimination::new()); }); group.bench_function("linear", |bencher| { - bencher.iter_custom(|iters| run(iters, create_linear_cfg, DeadStoreElimination::new())); + run(bencher, create_linear_cfg, DeadStoreElimination::new()); }); group.bench_function("diamond", |bencher| { - bencher.iter_custom(|iters| run(iters, create_diamond_cfg, DeadStoreElimination::new())); + run(bencher, create_diamond_cfg, DeadStoreElimination::new()); }); group.bench_function("complex", |bencher| { - bencher.iter_custom(|iters| run(iters, create_complex_cfg, DeadStoreElimination::new())); + run(bencher, create_complex_cfg, DeadStoreElimination::new()); }); } @@ -341,34 +366,28 @@ fn pipeline(criterion: &mut Criterion) { group.bench_function("linear", |bencher| { let mut scratch = Scratch::new(); - bencher.iter_custom(|iters| { - run_fn(iters, create_linear_cfg, |context, body| { - CfgSimplify::new_in(&mut scratch).run(context, body); - Sroa::new_in(&mut scratch).run(context, body); - DeadStoreElimination::new_in(&mut scratch).run(context, body); - }) + run_bencher(bencher, create_linear_cfg, |context, body| { + CfgSimplify::new_in(&mut scratch).run(context, body); + Sroa::new_in(&mut scratch).run(context, body); + DeadStoreElimination::new_in(&mut scratch).run(context, body); }); }); group.bench_function("diamond", |bencher| { let mut scratch = Scratch::new(); - bencher.iter_custom(|iters| { - run_fn(iters, create_diamond_cfg, |context, body| { - CfgSimplify::new_in(&mut scratch).run(context, body); - Sroa::new_in(&mut scratch).run(context, body); - DeadStoreElimination::new_in(&mut scratch).run(context, body); - }) + run_bencher(bencher, create_diamond_cfg, |context, body| { + CfgSimplify::new_in(&mut scratch).run(context, body); + Sroa::new_in(&mut scratch).run(context, body); + DeadStoreElimination::new_in(&mut scratch).run(context, body); }); }); group.bench_function("complex", |bencher| { let mut scratch = Scratch::new(); - bencher.iter_custom(|iters| { - run_fn(iters, create_complex_cfg, |context, body| { - CfgSimplify::new_in(&mut scratch).run(context, body); - Sroa::new_in(&mut scratch).run(context, body); - DeadStoreElimination::new_in(&mut scratch).run(context, body); - }) + run_bencher(bencher, create_complex_cfg, |context, body| { + CfgSimplify::new_in(&mut scratch).run(context, body); + Sroa::new_in(&mut scratch).run(context, body); + DeadStoreElimination::new_in(&mut scratch).run(context, body); }); }); } From 2332e6b3f1677cc6ac7e94e3c37217333d401955 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:12:57 +0100 Subject: [PATCH 02/13] feat: use `iter_batched_ref instead` --- Cargo.lock | 122 +++++++++++++++++++- libs/@local/hashql/mir/Cargo.toml | 11 +- libs/@local/hashql/mir/benches/transform.rs | 24 ++-- 3 files changed, 138 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0b60e9120c..a7e35a7dbc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,6 +216,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "ar_archive_writer" version = "0.2.0" @@ -1404,12 +1413,80 @@ dependencies = [ "cc", ] +[[package]] +name = "codspeed" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b847e05a34be5c38f3f2a5052178a3bd32e6b5702f3ea775efde95c483a539" +dependencies = [ + "anyhow", + "cc", + "colored", + "getrandom 0.2.16", + "glob", + "libc", + "nix 0.30.1", + "serde", + "serde_json", + "statrs", +] + +[[package]] +name = "codspeed-criterion-compat" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a0e2a53beb18dec493ec133f226e0d35e8bb2fdc638ccf7351696eabee416c" +dependencies = [ + "clap", + "codspeed", + "codspeed-criterion-compat-walltime", + "colored", + "regex", +] + +[[package]] +name = "codspeed-criterion-compat-walltime" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f652d6e6d40bba0f5c244744db94d92a26b7f083df18692df88fb0772f1c793" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "codspeed", + "criterion-plot 0.5.0", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "compact_str" version = "0.9.0" @@ -1596,7 +1673,7 @@ dependencies = [ "cast", "ciborium", "clap", - "criterion-plot", + "criterion-plot 0.8.1", "itertools 0.13.0", "num-traits", "oorandom", @@ -1621,6 +1698,16 @@ dependencies = [ "quote", ] +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "criterion-plot" version = "0.8.1" @@ -3730,8 +3817,7 @@ version = "0.0.0" dependencies = [ "anstyle-svg", "bstr", - "criterion", - "criterion-macro", + "codspeed-criterion-compat", "hashql-compiletest", "hashql-core", "hashql-diagnostics", @@ -4376,6 +4462,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -4397,6 +4494,15 @@ dependencies = [ "nom", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -8051,6 +8157,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "str_stack" version = "0.1.0" diff --git a/libs/@local/hashql/mir/Cargo.toml b/libs/@local/hashql/mir/Cargo.toml index 318a916cef5..267fc2e6443 100644 --- a/libs/@local/hashql/mir/Cargo.toml +++ b/libs/@local/hashql/mir/Cargo.toml @@ -22,10 +22,9 @@ bstr = { workspace = true } simple-mermaid = { workspace = true } [dev-dependencies] -criterion = { workspace = true } -criterion-macro = { workspace = true } -hashql-compiletest = { workspace = true } -insta = { workspace = true } +codspeed-criterion-compat = "4.1.0" +hashql-compiletest = { workspace = true } +insta = { workspace = true } [lints] @@ -35,6 +34,10 @@ workspace = true name = "compiletest" harness = false +[[bench]] +name = "transform" +harness = false + [package.metadata.sync.turborepo] ignore-dev-dependencies = [ "@rust/hashql-compiletest", diff --git a/libs/@local/hashql/mir/benches/transform.rs b/libs/@local/hashql/mir/benches/transform.rs index b091de2c4f0..7988209e714 100644 --- a/libs/@local/hashql/mir/benches/transform.rs +++ b/libs/@local/hashql/mir/benches/transform.rs @@ -2,8 +2,9 @@ #![test_runner(criterion::runner)] #![expect(clippy::min_ident_chars, clippy::many_single_char_names)] -use criterion::{BatchSize, Bencher, Criterion}; -use criterion_macro::criterion; +use core::hint::black_box; + +use codspeed_criterion_compat::{BatchSize, Bencher, Criterion, criterion_group, criterion_main}; use hashql_core::{ heap::{BumpAllocator as _, Heap, Scratch}, r#type::{TypeBuilder, environment::Environment}, @@ -256,7 +257,7 @@ fn run_bencher( // IMPORTANT: `BatchSize::PerIteration` is critical for soundness. Do NOT change this to // `SmallInput`, `LargeInput`, or any other batch size. Doing so will cause undefined // behavior (use-after-free of arena allocations). - bencher.iter_batched( + bencher.iter_batched_ref( || { // SAFETY: We create a `&mut Heap` from the raw pointer to call `reset()` and build // the environment/interner/body. This is sound because: @@ -274,7 +275,7 @@ fn run_bencher( (env, interner, body) }, - |(env, interner, mut body)| { + |(env, interner, body)| { // SAFETY: We create a shared `&Heap` reference. This is sound because: // - The `&mut Heap` from setup no longer exists (setup closure has returned) // - The `env`, `interner`, and `body` already hold shared borrows of `heap` @@ -283,13 +284,13 @@ fn run_bencher( let mut context = MirContext { heap, - env: &env, - interner: &interner, + env, + interner, diagnostics: DiagnosticIssues::new(), }; - func(&mut context, &mut body); - (env, interner, body) + func(black_box(&mut context), black_box(body)); + context.diagnostics }, BatchSize::PerIteration, ); @@ -309,7 +310,6 @@ fn run( ); } -#[criterion] fn cfg_simplify(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("cfg_simplify"); @@ -326,7 +326,6 @@ fn cfg_simplify(criterion: &mut Criterion) { }); } -#[criterion] fn sroa(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("sroa"); @@ -341,7 +340,6 @@ fn sroa(criterion: &mut Criterion) { }); } -#[criterion] fn dse(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("dse"); @@ -359,7 +357,6 @@ fn dse(criterion: &mut Criterion) { }); } -#[criterion] fn pipeline(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("pipeline"); @@ -391,3 +388,6 @@ fn pipeline(criterion: &mut Criterion) { }); }); } + +criterion_group!(benches, cfg_simplify, sroa, dse, pipeline); +criterion_main!(benches); From e22205c754afdad077026861c9bb25fff05790b5 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:17:45 +0100 Subject: [PATCH 03/13] fix: lints --- .config/mise/config.toml | 1 + libs/@local/hashql/mir/benches/transform.rs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.config/mise/config.toml b/.config/mise/config.toml index e82e46d5b4f..c321a992acb 100644 --- a/.config/mise/config.toml +++ b/.config/mise/config.toml @@ -18,6 +18,7 @@ protoc = "32.1" # CLI tools biome = "1.9.5-nightly.ff02a0b" +"cargo:cargo-codspeed" = "latest" "cargo:cargo-hack" = "0.6.37" "cargo:cargo-insta" = "1.43.1" "cargo:cargo-llvm-cov" = "0.6.18" diff --git a/libs/@local/hashql/mir/benches/transform.rs b/libs/@local/hashql/mir/benches/transform.rs index 7988209e714..d3ea26c06aa 100644 --- a/libs/@local/hashql/mir/benches/transform.rs +++ b/libs/@local/hashql/mir/benches/transform.rs @@ -1,6 +1,8 @@ -#![feature(custom_test_frameworks)] -#![test_runner(criterion::runner)] -#![expect(clippy::min_ident_chars, clippy::many_single_char_names)] +#![expect( + clippy::min_ident_chars, + clippy::many_single_char_names, + clippy::significant_drop_tightening +)] use core::hint::black_box; From 408a63a04b9727fc15dda0845854ac1b123dd4af Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:35:50 +0100 Subject: [PATCH 04/13] feat: CI --- .github/workflows/codspeed.yml | 80 +++++++++++++++++++++++++++++ libs/@local/hashql/mir/package.json | 1 + 2 files changed, 81 insertions(+) create mode 100644 .github/workflows/codspeed.yml diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 00000000000..58c458ac2b5 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,80 @@ +name: CodSpeed Benchmarks + +on: + push: + branches: + - "main" + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + setup: + runs-on: ubuntu-24.04 + permissions: + id-token: write + outputs: + packages: ${{ steps.packages.outputs.packages }} + steps: + - name: Checkout source code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 2 + + - name: Install tools + uses: ./.github/actions/install-tools + with: + token: ${{ secrets.GITHUB_TOKEN }} + vault_address: ${{ secrets.VAULT_ADDR }} + rust: false + + - name: Determine changed packages that have codspeed + id: packages + run: | + PACKAGES_QUERY='query { affectedPackages(base: "HEAD^" filter: {has: {field: "TASK_NAME", value: "build:codspeed"}}) {items {name path}}}' + PACKAGES=$(turbo query "$PACKAGES_QUERY" \ + | jq --compact-output '.data.affectedPackages.items | [(.[] | select(.name != "//"))] | { name: [.[].name], include: . }') + + echo "packages=$PACKAGES" | tee -a $GITHUB_OUTPUT + + benchmarks: + name: Run benchmarks + needs: [setup] + runs-on: ubuntu-24.04 + strategy: + matrix: ${{ fromJSON(needs.setup.outputs.packages) }} + fail-fast: false + if: needs.setup.outputs.packages != '{"name":[],"include":[]}' + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 2 + + - name: Clean up disk + uses: ./.github/actions/clean-up-disk + + - name: Install tools + uses: ./.github/actions/install-tools + with: + token: ${{ secrets.GITHUB_TOKEN }} + vault_address: ${{ secrets.VAULT_ADDR }} + + - name: Prune repository + uses: ./.github/actions/prune-repository + with: + scope: ${{ matrix.name }} + + - name: Build the benchmark target + run: turbo run build:codspeed --filter=${{ matrix.name }} + + - name: Run the benchmark + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: cargo codspeed run diff --git a/libs/@local/hashql/mir/package.json b/libs/@local/hashql/mir/package.json index 7152978e9a8..4a9ec591c4c 100644 --- a/libs/@local/hashql/mir/package.json +++ b/libs/@local/hashql/mir/package.json @@ -4,6 +4,7 @@ "private": true, "license": "AGPL-3", "scripts": { + "build:codspeed": "cargo codspeed build -p hashql-mir", "doc:dependency-diagram": "cargo run -p hash-repo-chores -- dependency-diagram --output docs/dependency-diagram.mmd --root hashql-mir --root-deps-and-dependents --link-mode non-roots --include-dev-deps --include-build-deps --logging-console-level info", "fix:clippy": "just clippy --fix", "lint:clippy": "just clippy", From 421c4621c3f68090ebd780d6a0986733f661d6ff Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:38:25 +0100 Subject: [PATCH 05/13] fix: workspace dep --- Cargo.toml | 1 + libs/@local/hashql/mir/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2361e04d9f9..91c4c0bc3a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ circular-buffer = { version = "1.1.0", default-features = fal clap = { version = "4.5.51", features = ["color", "error-context", "help", "std", "suggestions", "usage"] } clap_builder = { version = "4.5.51", default-features = false, features = ["std"] } clap_complete = { version = "4.5.60", default-features = false } +codspeed-criterion-compat = { version = "4.1.0" } console_error_panic_hook = { version = "0.1.7", default-features = false } convert_case = { version = "0.10.0", default-features = false } criterion = { version = "0.8.0" } diff --git a/libs/@local/hashql/mir/Cargo.toml b/libs/@local/hashql/mir/Cargo.toml index 267fc2e6443..2c48b2045f2 100644 --- a/libs/@local/hashql/mir/Cargo.toml +++ b/libs/@local/hashql/mir/Cargo.toml @@ -22,7 +22,7 @@ bstr = { workspace = true } simple-mermaid = { workspace = true } [dev-dependencies] -codspeed-criterion-compat = "4.1.0" +codspeed-criterion-compat = { workspace = true } hashql-compiletest = { workspace = true } insta = { workspace = true } From 944568f574afe549e09f6fa301d9725e17459ade Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:46:19 +0100 Subject: [PATCH 06/13] fix: register build:codspeed --- turbo.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/turbo.json b/turbo.json index 0b98a1e805d..2f47af07c3f 100644 --- a/turbo.json +++ b/turbo.json @@ -28,6 +28,9 @@ "build:types": { "dependsOn": ["^build:types"] }, + "build:codspeed": { + "cache": false + }, "compile": { "cache": false }, From 7242d8793ba1c51ea9402de675d11ca1daaa3b20 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:47:22 +0100 Subject: [PATCH 07/13] fix: pin codspeed --- .config/mise/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/mise/config.toml b/.config/mise/config.toml index c321a992acb..8d21a77b320 100644 --- a/.config/mise/config.toml +++ b/.config/mise/config.toml @@ -18,7 +18,7 @@ protoc = "32.1" # CLI tools biome = "1.9.5-nightly.ff02a0b" -"cargo:cargo-codspeed" = "latest" +"cargo:cargo-codspeed" = "4.1.0" "cargo:cargo-hack" = "0.6.37" "cargo:cargo-insta" = "1.43.1" "cargo:cargo-llvm-cov" = "0.6.18" From 4d1e3ede0f287b3b999eeacb9b2e97a2695dec1d Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 22:58:20 +0100 Subject: [PATCH 08/13] fix: CI --- .github/workflows/codspeed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 58c458ac2b5..f2a271a4bce 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -71,7 +71,7 @@ jobs: scope: ${{ matrix.name }} - name: Build the benchmark target - run: turbo run build:codspeed --filter=${{ matrix.name }} + run: cargo codspeed build - name: Run the benchmark uses: CodSpeedHQ/action@v4 From bbd1b2d2878b564032d5e4ddf880d884f660f70c Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 23:02:26 +0100 Subject: [PATCH 09/13] chore: explicitely set instrumentation target --- .github/workflows/codspeed.yml | 4 ++-- libs/@local/hashql/mir/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index f2a271a4bce..a1ef9c6ca88 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -71,10 +71,10 @@ jobs: scope: ${{ matrix.name }} - name: Build the benchmark target - run: cargo codspeed build + run: turbo run build:codspeed --filter=${{ matrix.name }} - name: Run the benchmark - uses: CodSpeedHQ/action@v4 + uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 with: mode: simulation run: cargo codspeed run diff --git a/libs/@local/hashql/mir/package.json b/libs/@local/hashql/mir/package.json index 4a9ec591c4c..8f5a1276d3d 100644 --- a/libs/@local/hashql/mir/package.json +++ b/libs/@local/hashql/mir/package.json @@ -4,7 +4,7 @@ "private": true, "license": "AGPL-3", "scripts": { - "build:codspeed": "cargo codspeed build -p hashql-mir", + "build:codspeed": "cargo codspeed build -p hashql-mir -m instrumentation", "doc:dependency-diagram": "cargo run -p hash-repo-chores -- dependency-diagram --output docs/dependency-diagram.mmd --root hashql-mir --root-deps-and-dependents --link-mode non-roots --include-dev-deps --include-build-deps --logging-console-level info", "fix:clippy": "just clippy --fix", "lint:clippy": "just clippy", From 381151727f67ba09cec2ef4e197adcbab2a90018 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 23:07:12 +0100 Subject: [PATCH 10/13] chore: hardcode hashql-mir (for now) --- .github/workflows/codspeed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index a1ef9c6ca88..a35e5757f28 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -77,4 +77,4 @@ jobs: uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 with: mode: simulation - run: cargo codspeed run + run: cargo codspeed run -p hashql-mir From ae3060a1ba0e2e8e3173f51d89569b2d7fbd2fd4 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 23:12:00 +0100 Subject: [PATCH 11/13] chore: put test command into separate package.json command --- .github/workflows/codspeed.yml | 2 +- libs/@local/hashql/mir/package.json | 3 ++- turbo.json | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index a35e5757f28..310d181ba92 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -77,4 +77,4 @@ jobs: uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 with: mode: simulation - run: cargo codspeed run -p hashql-mir + run: turbo run test:codspeed --filter=${{ matrix.name }} diff --git a/libs/@local/hashql/mir/package.json b/libs/@local/hashql/mir/package.json index 8f5a1276d3d..1a0eee7fc9a 100644 --- a/libs/@local/hashql/mir/package.json +++ b/libs/@local/hashql/mir/package.json @@ -4,10 +4,11 @@ "private": true, "license": "AGPL-3", "scripts": { - "build:codspeed": "cargo codspeed build -p hashql-mir -m instrumentation", + "build:codspeed": "cargo codspeed build -p hashql-mir", "doc:dependency-diagram": "cargo run -p hash-repo-chores -- dependency-diagram --output docs/dependency-diagram.mmd --root hashql-mir --root-deps-and-dependents --link-mode non-roots --include-dev-deps --include-build-deps --logging-console-level info", "fix:clippy": "just clippy --fix", "lint:clippy": "just clippy", + "test:codspeed": "cargo codspeed run -p hashql-mir", "test:unit": "mise run test:unit @rust/hashql-mir" }, "dependencies": { diff --git a/turbo.json b/turbo.json index 2f47af07c3f..c5f915ccada 100644 --- a/turbo.json +++ b/turbo.json @@ -73,6 +73,9 @@ "dependsOn": ["codegen", "^start:test:healthcheck"], "env": ["TEST_COVERAGE"] }, + "test:codspeed": { + "cache": false + }, "test:miri": {}, // Benchmarks "bench:unit": { From 4f03b077c98b7232ff1a22232ab545ecc85ecdfe Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 23:46:22 +0100 Subject: [PATCH 12/13] feat: use intermediary scratch for dominators --- .../src/graph/algorithms/dominators/mod.rs | 30 +++++++++++-------- libs/@local/hashql/core/src/heap/scratch.rs | 9 ++++++ .../hashql/mir/src/body/basic_blocks.rs | 1 + .../hashql/mir/src/body/terminator/mod.rs | 4 +++ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs b/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs index ea28b1e3a53..eed493d7330 100644 --- a/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs +++ b/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs @@ -32,6 +32,7 @@ #![expect(clippy::min_ident_chars, reason = "vendored in code")] use alloc::alloc::Global; use core::{cmp, ops}; +use std::alloc::Allocator; use smallvec::{SmallVec, smallvec}; @@ -41,6 +42,7 @@ pub use self::{ }; use crate::{ graph::{DirectedGraph, Predecessors, Successors}, + heap::Scratch, id::{Id, IdSlice, IdVec}, newtype, }; @@ -106,18 +108,21 @@ fn dominators_impl( graph: &G, start_node: G::NodeId, ) -> Inner { + let scratch = Scratch::new(); + // We allocate capacity for the full set of nodes, because most of the time // most of the nodes *are* reachable. - let mut parent: IdVec = IdVec::with_capacity(graph.node_count()); + let mut parent: IdVec = + IdVec::with_capacity_in(graph.node_count(), &scratch); let mut stack = vec![PreOrderFrame { pre_order_idx: PreorderIndex::MIN, iter: graph.successors(start_node), }]; - let mut pre_order_to_real: IdVec = - IdVec::with_capacity(graph.node_count()); - let mut real_to_pre_order: IdVec> = - IdVec::from_elem(None, graph.node_count()); + let mut pre_order_to_real: IdVec = + IdVec::with_capacity_in(graph.node_count(), &scratch); + let mut real_to_pre_order: IdVec, _> = + IdVec::from_elem_in(None, graph.node_count(), &scratch); pre_order_to_real.push(start_node); parent.push(PreorderIndex::MIN); // the parent of the root node is the root for now. @@ -150,10 +155,10 @@ fn dominators_impl( let reachable_vertices = pre_order_to_real.len(); - let mut idom = IdVec::from_elem(PreorderIndex::MIN, reachable_vertices); - let mut semi = IdVec::from_fn(reachable_vertices, core::convert::identity); + let mut idom = IdVec::from_elem_in(PreorderIndex::MIN, reachable_vertices, &scratch); + let mut semi = IdVec::from_fn_in(reachable_vertices, core::convert::identity, &scratch); let mut label = semi.clone(); - let mut bucket = IdVec::from_elem(vec![], reachable_vertices); + let mut bucket = IdVec::from_elem_in(vec![], reachable_vertices, &scratch); let mut lastlinked = None; // We loop over vertices in reverse preorder. This implements the pseudocode @@ -318,7 +323,7 @@ fn dominators_impl( immediate_dominators[start_node] = None; - let time = compute_access_time(start_node, &immediate_dominators); + let time = compute_access_time(start_node, &immediate_dominators, &scratch); Inner { immediate_dominators, @@ -446,16 +451,17 @@ struct Time { newtype!(struct EdgeIndex(u32 is 0..=u32::MAX)); -fn compute_access_time( +fn compute_access_time( start_node: N, immediate_dominators: &IdSlice>, + alloc: A, ) -> IdVec { // Transpose the dominator tree edges, so that child nodes of vertex v are stored in // node[edges[v].start..edges[v].end]. - let mut edges: IdVec> = IdVec::from_domain_in( + let mut edges: IdVec, A> = IdVec::from_domain_in( EdgeIndex::from_u32(0)..EdgeIndex::from_u32(0), immediate_dominators, - Global, + alloc, ); for &idom in immediate_dominators { diff --git a/libs/@local/hashql/core/src/heap/scratch.rs b/libs/@local/hashql/core/src/heap/scratch.rs index efdbe488e07..372198fcd53 100644 --- a/libs/@local/hashql/core/src/heap/scratch.rs +++ b/libs/@local/hashql/core/src/heap/scratch.rs @@ -30,11 +30,20 @@ pub struct Scratch { impl Scratch { /// Creates a new scratch allocator. #[must_use] + #[inline] pub fn new() -> Self { Self { inner: Allocator::new(), } } + + #[must_use] + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + Self { + inner: Allocator::with_capacity(capacity), + } + } } impl Default for Scratch { diff --git a/libs/@local/hashql/mir/src/body/basic_blocks.rs b/libs/@local/hashql/mir/src/body/basic_blocks.rs index a37733a7784..683e09c6ff2 100644 --- a/libs/@local/hashql/mir/src/body/basic_blocks.rs +++ b/libs/@local/hashql/mir/src/body/basic_blocks.rs @@ -341,6 +341,7 @@ impl graph::Successors for BasicBlocks<'_> { where Self: 'this; + #[inline] fn successors(&self, node: Self::NodeId) -> Self::SuccIter<'_> { self.blocks[node].terminator.kind.successor_blocks() } diff --git a/libs/@local/hashql/mir/src/body/terminator/mod.rs b/libs/@local/hashql/mir/src/body/terminator/mod.rs index 85b453fdec5..1f861c46609 100644 --- a/libs/@local/hashql/mir/src/body/terminator/mod.rs +++ b/libs/@local/hashql/mir/src/body/terminator/mod.rs @@ -46,10 +46,12 @@ where { type Item = L::Item; + #[inline] fn next(&mut self) -> Option { for_both!(self; value => value.next()) } + #[inline] fn size_hint(&self) -> (usize, Option) { for_both!(self; value => value.size_hint()) } @@ -60,6 +62,7 @@ where L: DoubleEndedIterator, R: DoubleEndedIterator, { + #[inline] fn next_back(&mut self) -> Option { for_both!(self; value => value.next_back()) } @@ -70,6 +73,7 @@ where L: ExactSizeIterator, R: ExactSizeIterator, { + #[inline] fn len(&self) -> usize { for_both!(self; value => value.len()) } From 3326b24887d197fd2dfdb498469c4505061f0a1e Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud Date: Fri, 19 Dec 2025 23:58:21 +0100 Subject: [PATCH 13/13] chore: remove scratch again --- .../src/graph/algorithms/dominators/mod.rs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs b/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs index eed493d7330..ea28b1e3a53 100644 --- a/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs +++ b/libs/@local/hashql/core/src/graph/algorithms/dominators/mod.rs @@ -32,7 +32,6 @@ #![expect(clippy::min_ident_chars, reason = "vendored in code")] use alloc::alloc::Global; use core::{cmp, ops}; -use std::alloc::Allocator; use smallvec::{SmallVec, smallvec}; @@ -42,7 +41,6 @@ pub use self::{ }; use crate::{ graph::{DirectedGraph, Predecessors, Successors}, - heap::Scratch, id::{Id, IdSlice, IdVec}, newtype, }; @@ -108,21 +106,18 @@ fn dominators_impl( graph: &G, start_node: G::NodeId, ) -> Inner { - let scratch = Scratch::new(); - // We allocate capacity for the full set of nodes, because most of the time // most of the nodes *are* reachable. - let mut parent: IdVec = - IdVec::with_capacity_in(graph.node_count(), &scratch); + let mut parent: IdVec = IdVec::with_capacity(graph.node_count()); let mut stack = vec![PreOrderFrame { pre_order_idx: PreorderIndex::MIN, iter: graph.successors(start_node), }]; - let mut pre_order_to_real: IdVec = - IdVec::with_capacity_in(graph.node_count(), &scratch); - let mut real_to_pre_order: IdVec, _> = - IdVec::from_elem_in(None, graph.node_count(), &scratch); + let mut pre_order_to_real: IdVec = + IdVec::with_capacity(graph.node_count()); + let mut real_to_pre_order: IdVec> = + IdVec::from_elem(None, graph.node_count()); pre_order_to_real.push(start_node); parent.push(PreorderIndex::MIN); // the parent of the root node is the root for now. @@ -155,10 +150,10 @@ fn dominators_impl( let reachable_vertices = pre_order_to_real.len(); - let mut idom = IdVec::from_elem_in(PreorderIndex::MIN, reachable_vertices, &scratch); - let mut semi = IdVec::from_fn_in(reachable_vertices, core::convert::identity, &scratch); + let mut idom = IdVec::from_elem(PreorderIndex::MIN, reachable_vertices); + let mut semi = IdVec::from_fn(reachable_vertices, core::convert::identity); let mut label = semi.clone(); - let mut bucket = IdVec::from_elem_in(vec![], reachable_vertices, &scratch); + let mut bucket = IdVec::from_elem(vec![], reachable_vertices); let mut lastlinked = None; // We loop over vertices in reverse preorder. This implements the pseudocode @@ -323,7 +318,7 @@ fn dominators_impl( immediate_dominators[start_node] = None; - let time = compute_access_time(start_node, &immediate_dominators, &scratch); + let time = compute_access_time(start_node, &immediate_dominators); Inner { immediate_dominators, @@ -451,17 +446,16 @@ struct Time { newtype!(struct EdgeIndex(u32 is 0..=u32::MAX)); -fn compute_access_time( +fn compute_access_time( start_node: N, immediate_dominators: &IdSlice>, - alloc: A, ) -> IdVec { // Transpose the dominator tree edges, so that child nodes of vertex v are stored in // node[edges[v].start..edges[v].end]. - let mut edges: IdVec, A> = IdVec::from_domain_in( + let mut edges: IdVec> = IdVec::from_domain_in( EdgeIndex::from_u32(0)..EdgeIndex::from_u32(0), immediate_dominators, - alloc, + Global, ); for &idom in immediate_dominators {