From 7626099c9b55eaf2571a8222adc091b49d0a1446 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 29 May 2026 13:10:22 +0200 Subject: [PATCH] perf(build): release profile tune + mimalloc on node binary (#134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(build): tune release profile + adopt mimalloc on the node binary Cargo's default release profile uses `codegen-units = 16` and `lto = false`. For a binary whose wall-clock is dominated by Plonky2 prove calls (3-15 min at MAX_IN_COINS=8) those defaults leave intra-crate inlining on the table at the `node` / `script-plonky2` boundary that calls into `plonky2_field`'s heavily-inlined hot path through a trait surface. - `lto = "thin"` enables cross-crate inlining without the full link- time cost of `lto = "fat"` (upstream Plonky2 tried fat and reverted in their Cargo.toml — see the inline comment). - `codegen-units = 1` forces the whole binary into a single LLVM compilation unit so the inliner sees everything at once. - `incremental = false` is the cargo default for release, restated here so a future `CARGO_INCREMENTAL=1` on a build host cannot silently fragment the codegen unit. - `panic = "abort"` is intentionally not set: `main.rs` installs a global panic hook (PR #36, MINTING_ADDRESS bootstrap recovery) and state / account layers use RwLock poison recovery to fail one request without taking the process down. Abort defeats both. mimalloc replaces the system allocator in the binary only — library crates, integration tests, and program/prover crates keep the system allocator. The Plonky2 prove path allocates many polynomial and witness buffers across the rayon worker pool; glibc-malloc on the debian-bookworm-slim runtime image is not tuned for that pattern. Scoping the swap to `main.rs` keeps unit-test behaviour identical to CI and avoids pulling a C build dependency into every test target. Local release build: 1m 57s on M3 Ultra (vs ~1m 26s pre-change), within the ~5 min CI budget documented in README.md / CONTRIBUTING.md. Pre-push hook (fmt + 3x clippy + workspace check) clean. Expected proof-time impact is modest (single-digit-to-low-double-digit percent on aggregate). The authoritative measurement is the pending R2 wall-clock probe on the M3 Ultra (ROADMAP § Step 9) — this PR makes the build profile what that measurement should be taken from, not a separate one-off tune. * docs(node): correct mimalloc default-features comment The previous comment claimed `default-features = false` strips "secure-mode overhead" and "debug rings". Upstream `purpleprotocol/mimalloc_rust` ships `default = []`, so there are no default features to disable — the original justification was fictitious. Reword the comment to state the real reason: it is a defensive opt-in posture so a future upstream change that turns on `secure`, `debug`, `extended`, or `v2` via default features cannot silently land in the node binary without an explicit code change. No behavioural change. --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 40 ++++++++++++++++++++++++++++++++++++++++ node/Cargo.toml | 15 +++++++++++++++ node/src/main.rs | 10 ++++++++++ 4 files changed, 84 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9aa6da9c..9166f018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1792,6 +1792,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + [[package]] name = "libredox" version = "0.1.16" @@ -1884,6 +1893,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -1971,6 +1989,7 @@ dependencies = [ "hex", "http-body-util", "lazy_static", + "mimalloc", "rand 0.8.6", "reqwest 0.12.28", "serde", diff --git a/Cargo.toml b/Cargo.toml index 73b96c64..e8a81183 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,46 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } [profile.dev] opt-level = 3 +# Release profile is what the Dockerfile builds (`cargo build --release -p +# node`). Defaults from Cargo are intentionally lax (codegen-units = 16, +# lto = false, incremental = false), tuned for build-time and developer +# laptops — not for a prover hot-path that runs for minutes per call. +# +# Three knobs are turned on here. Each is documented in +# https://doc.rust-lang.org/cargo/reference/profiles.html. +# +# `lto = "thin"` enables cross-crate Link-Time Optimization on a +# per-codegen-unit basis. Plonky2's hot path (`plonky2_field::goldilocks` +# `Mul`, `reduce128`, the FFT inner loops) is heavily `#[inline]`d +# intra-crate, but the `node` / `script-plonky2` boundary calls *into* +# those generics through a trait surface. `thin` lets LLVM inline +# across that boundary. `fat` was tried upstream and reverted (see +# `0xPolygonZero/plonky2/Cargo.toml` — `#lto = "fat"` commented out); +# `thin` keeps almost the full speedup with a far smaller link-time +# regression. +# +# `codegen-units = 1` forces the whole crate into a single LLVM +# compilation unit. Trades ~30-60% link-time on the node binary for +# the most aggressive intra-crate optimization. Justified because the +# binary's wall-clock is dominated by Plonky2 prove calls (3-15 min at +# `MAX_IN_COINS=8`), not by `cargo build` time. +# +# `incremental = false` is the default for release; restating it +# explicitly so a future `CARGO_INCREMENTAL=1` env var on a build host +# (or a global `~/.cargo/config.toml` override) cannot silently +# fragment the codegen unit and undo the line above. Plonky2 upstream +# left `incremental = true` in their release profile — that is +# explicitly counter to what we want here. +# +# `panic = "abort"` is *not* set: `node/src/main.rs` installs a global +# panic hook (PR #36, MINTING_ADDRESS bootstrap fix) and the account +# / state layers use `RwLock` poison recovery to fail a single request +# without taking the process down. `panic = "abort"` would defeat both. +[profile.release] +lto = "thin" +codegen-units = 1 +incremental = false + [workspace.package] version = "1.1.0" edition = "2021" diff --git a/node/Cargo.toml b/node/Cargo.toml index 30cb885f..cf09cf5d 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -6,6 +6,21 @@ edition.workspace = true [dependencies] bitcoin = { workspace = true } bitcoin_hashes = { version = "0.16.0", features = ["std"] } +# Drop-in allocator. Plonky2's prove path allocates many polynomial +# and witness buffers across the rayon worker pool; glibc-malloc on +# the Debian-bookworm-slim runtime image is not tuned for that +# pattern. mimalloc consistently outperforms glibc malloc on +# multi-thread alloc-heavy ARM64 workloads. Wired in `main.rs` via +# `#[global_allocator]`; tests, integration tests, and the +# `program-plonky2` / `script-plonky2` library crates keep using the +# system allocator (no behavioural change at the prover circuit +# level). `default-features = false` is defensive — upstream +# (`purpleprotocol/mimalloc_rust`) currently ships `default = []`, so +# this disables nothing today. It pins the opt-in posture for the +# future: if upstream ever turns on `secure`, `debug`, `extended`, or +# a v2 flip via default features, the node binary stays on the small +# portable build until someone opts in deliberately. +mimalloc = { version = "0.1", default-features = false } sha2 = { workspace = true } serde = { workspace = true } bincode = { workspace = true } diff --git a/node/src/main.rs b/node/src/main.rs index ead1a6fe..9c4feb5a 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -8,6 +8,16 @@ //! duplicating definitions or making the binary itself reachable //! from a `cargo test --test ...` target. +// mimalloc replaces the default allocator process-wide. Scoped to the +// binary (`main.rs`) so the `node` library crate, integration tests, +// and the `program-plonky2` / `script-plonky2` crates continue to use +// the system allocator — keeps unit-test behaviour identical to CI +// and avoids pulling a C build dependency into every test target. +// See the comment on the `mimalloc` line in `node/Cargo.toml` for the +// rationale. +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + use node::account_node; use node::db; use node::publisher::EsploraConfig;