From 42ba6876c218a8839491f2d366f0ad2687401b0b Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Sun, 18 Oct 2026 22:15:22 -0500 Subject: [PATCH] feat(bashkit): add feature-gated warm_up() entry point --- crates/bashkit/Cargo.toml | 2 +- crates/bashkit/benches/python.rs | 12 ++++++------ crates/bashkit/docs/cpython.md | 4 ++-- crates/bashkit/examples/cpython_load.rs | 4 ++-- crates/bashkit/examples/cpython_scripts.rs | 4 ++-- crates/bashkit/src/builtins/cpython/mod.rs | 5 +++-- crates/bashkit/src/builtins/cpython/tests.rs | 5 +++++ crates/bashkit/src/lib.rs | 20 +++++++++++++++++++- knowledge/runtimes/cpython-wasm.md | 2 +- 9 files changed, 41 insertions(+), 17 deletions(-) diff --git a/crates/bashkit/Cargo.toml b/crates/bashkit/Cargo.toml index 64ca91c35..340d91a85 100644 --- a/crates/bashkit/Cargo.toml +++ b/crates/bashkit/Cargo.toml @@ -267,7 +267,7 @@ python = ["dep:monty", "dep:monty-types", "dep:get-size2"] cpython = ["dep:bashkit-cpython-wasm", "dep:wasmtime"] # Opt-in: CPython compiled to native machine code at build time instead of # Pulley bytecode. ~2x faster calls; needs executable memory, and the first -# load in a process copies the module (call CPython::warm_up at startup). +# load in a process copies the module (call bashkit::warm_up() at startup). # Usage: cargo build --features cpython-native cpython-native = ["cpython", "bashkit-cpython-wasm/native"] # Enable ts/node/deno/bun builtins via embedded ZapCode TypeScript interpreter diff --git a/crates/bashkit/benches/python.rs b/crates/bashkit/benches/python.rs index 36a8dc97b..1a35929b1 100644 --- a/crates/bashkit/benches/python.rs +++ b/crates/bashkit/benches/python.rs @@ -20,7 +20,7 @@ //! Run: `cargo bench --bench python --features python,cpython` or //! `just bench-python` (saves results). -use bashkit::{Bash, CPython, SessionLimits}; +use bashkit::{Bash, SessionLimits}; use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use tokio::runtime::Runtime; @@ -81,7 +81,7 @@ fn verify(rt: &Runtime) { fn bench_call(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - CPython::warm_up().unwrap(); + bashkit::warm_up().unwrap(); verify(&rt); let mut group = c.benchmark_group("python_call"); for runtime in RUNTIMES { @@ -107,7 +107,7 @@ const IMPORTS: &[(&str, &str)] = &[ fn bench_import(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - CPython::warm_up().unwrap(); + bashkit::warm_up().unwrap(); let mut group = c.benchmark_group("python_import"); group.sample_size(10); let bash = tokio::sync::Mutex::new(make_bash("cpython")); @@ -124,7 +124,7 @@ fn bench_import(c: &mut Criterion) { fn bench_session(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - CPython::warm_up().unwrap(); + bashkit::warm_up().unwrap(); let mut group = c.benchmark_group("python_session"); for runtime in RUNTIMES { group.bench_function(BenchmarkId::new(*runtime, "new_bash_print"), |b| { @@ -141,7 +141,7 @@ fn bench_session(c: &mut Criterion) { fn bench_compute(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - CPython::warm_up().unwrap(); + bashkit::warm_up().unwrap(); let mut group = c.benchmark_group("python_compute"); group.sample_size(10); for runtime in RUNTIMES { @@ -160,7 +160,7 @@ fn bench_parallel(c: &mut Criterion) { .enable_all() .build() .unwrap(); - CPython::warm_up().unwrap(); + bashkit::warm_up().unwrap(); let mut group = c.benchmark_group("python_parallel"); group.sample_size(10); for runtime in RUNTIMES { diff --git a/crates/bashkit/docs/cpython.md b/crates/bashkit/docs/cpython.md index a9ce9b7eb..9750b8538 100644 --- a/crates/bashkit/docs/cpython.md +++ b/crates/bashkit/docs/cpython.md @@ -44,7 +44,7 @@ the guest never runs native code in your process. The interpreter loads on the first call of a process: the first `python3` takes about 19 ms on the reference machine, later calls about 4.4 ms. To move that one-time cost out of the first request, call -`bashkit::CPython::warm_up()` at startup. On hosts where the linker or loader +`bashkit::warm_up()` at startup. On hosts where the linker or loader cannot preserve 64 KiB module alignment, such as macOS, an aligned immutable copy is cached once for the process; later calls reuse it. @@ -247,7 +247,7 @@ bashkit = { version = "0.19.0", features = ["cpython-native"] } Calls get 4-10x faster (`print(1)` ~1 ms instead of ~4 ms; CPU-bound code ~10x). In exchange the host must allow executable memory, and the first load -in a process takes ~47 ms, so call `bashkit::CPython::warm_up()` at startup. +in a process takes ~47 ms, so call `bashkit::warm_up()` at startup. Nothing is compiled at run time with either option. ## Binary size diff --git a/crates/bashkit/examples/cpython_load.rs b/crates/bashkit/examples/cpython_load.rs index ad696dc89..91a76afd5 100644 --- a/crates/bashkit/examples/cpython_load.rs +++ b/crates/bashkit/examples/cpython_load.rs @@ -9,7 +9,7 @@ //! //! Run: cargo run --release --example cpython_load --features cpython -- 1 16 64 256 1024 -use bashkit::{Bash, CPython}; +use bashkit::Bash; use std::time::{Duration, Instant}; /// Calls per tenant; override with `CPYTHON_LOAD_CALLS`. @@ -72,7 +72,7 @@ async fn main() { } else { levels }; - CPython::warm_up().expect("load CPython"); + bashkit::warm_up().expect("load CPython"); println!( "| Tenants | Calls | Wall (s) | Calls/s | p50 (ms) | p99 (ms) | max (ms) | Failures | Peak RSS (MB) |" ); diff --git a/crates/bashkit/examples/cpython_scripts.rs b/crates/bashkit/examples/cpython_scripts.rs index 3357bf754..b0fbe2959 100644 --- a/crates/bashkit/examples/cpython_scripts.rs +++ b/crates/bashkit/examples/cpython_scripts.rs @@ -6,13 +6,13 @@ //! //! Run with: cargo run --features cpython --example cpython_scripts -use bashkit::{Bash, CPython, CPythonLimits}; +use bashkit::{Bash, CPythonLimits}; use std::time::Duration; #[tokio::main] async fn main() -> anyhow::Result<()> { // Optional: load the interpreter at startup instead of on the first call. - CPython::warm_up().map_err(anyhow::Error::msg)?; + bashkit::warm_up().map_err(anyhow::Error::msg)?; let mut bash = Bash::builder() .cpython_with_limits(CPythonLimits::default().max_duration(Duration::from_secs(10))) diff --git a/crates/bashkit/src/builtins/cpython/mod.rs b/crates/bashkit/src/builtins/cpython/mod.rs index 35e7f869f..b601a1489 100644 --- a/crates/bashkit/src/builtins/cpython/mod.rs +++ b/crates/bashkit/src/builtins/cpython/mod.rs @@ -10,7 +10,7 @@ //! the guest never runs native code in the host, so calling //! `BashBuilder::cpython()` is the opt-in. //! - One process-wide engine and pre-linked module, created on first use (or -//! by [`CPython::warm_up`]). Every call gets a fresh store and instance, +//! by [`crate::warm_up`]). Every call gets a fresh store and instance, //! mapped copy-on-write from the pre-initialized snapshot; nothing survives //! between calls or tenants. //! - Guest work is metered with fuel and yields to the async runtime at a fixed @@ -220,7 +220,8 @@ impl CPython { /// Load the embedded interpreter now instead of on the first `python3` /// call. Idempotent and process-wide; returns the error text if the - /// embedded module cannot be loaded. + /// embedded module cannot be loaded. Prefer [`crate::warm_up`], which + /// warms every enabled runtime. pub fn warm_up() -> std::result::Result<(), String> { runtime().map(|_| ()).map_err(str::to_string) } diff --git a/crates/bashkit/src/builtins/cpython/tests.rs b/crates/bashkit/src/builtins/cpython/tests.rs index 05ea88f65..752542956 100644 --- a/crates/bashkit/src/builtins/cpython/tests.rs +++ b/crates/bashkit/src/builtins/cpython/tests.rs @@ -42,6 +42,11 @@ fn runtime_loads() { CPython::warm_up().expect("embedded CPython loads"); } +#[test] +fn crate_warm_up_delegates_to_cpython() { + crate::warm_up().expect("crate warm_up loads the embedded interpreter"); +} + #[test] fn on_demand_fallback_links() { // The path taken when the pooled engine cannot reserve address space. diff --git a/crates/bashkit/src/lib.rs b/crates/bashkit/src/lib.rs index 78e855e29..efbcc41e6 100644 --- a/crates/bashkit/src/lib.rs +++ b/crates/bashkit/src/lib.rs @@ -804,6 +804,24 @@ impl Invocation { } } +/// Load embedded runtimes now instead of on the first call. +/// +/// Feature-gated warm-up: with `cpython` this loads the embedded CPython +/// interpreter (delegates to [`CPython::warm_up`]); without it, a no-op +/// returning `Ok`. Idempotent and process-wide. +#[cfg(feature = "cpython")] +pub fn warm_up() -> std::result::Result<(), String> { + CPython::warm_up() +} + +/// Load embedded runtimes now instead of on the first call. +/// +/// No-op: no enabled feature needs eager loading. +#[cfg(not(feature = "cpython"))] +pub fn warm_up() -> std::result::Result<(), String> { + Ok(()) +} + /// Main entry point for Bashkit. /// /// Provides a virtual bash interpreter with an in-memory virtual filesystem. @@ -2587,7 +2605,7 @@ impl BashBuilder { /// both features are enabled and this method is called after /// [`BashBuilder::python`]. /// - /// The interpreter loads on the first call; call [`CPython::warm_up`] at + /// The interpreter loads on the first call; call [`warm_up`] at /// process start to move that cost out of the first request. /// /// # Example diff --git a/knowledge/runtimes/cpython-wasm.md b/knowledge/runtimes/cpython-wasm.md index 7ad040b95..b97eb127f 100644 --- a/knowledge/runtimes/cpython-wasm.md +++ b/knowledge/runtimes/cpython-wasm.md @@ -125,7 +125,7 @@ Still nothing is compiled at run time and it is still one crate; the trade: 3.7 ms, `import http.client` 34 ms vs 461 ms, `fib(20)` 6.4 ms vs 69 ms. - **Executable memory** is required at run time; Pulley needs none. - **First load copies** the module into executable memory (native code - cannot run from the binary's read-only static): `CPython::warm_up()` takes + cannot run from the binary's read-only static): `bashkit::warm_up()` takes ~47 ms instead of ~15 ms. Call it at startup. - **Baseline ISA**: compiled for the target triple with no host CPU feature detection, so a binary built on one machine runs on any CPU of that