Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bashkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions crates/bashkit/benches/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -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"));
Expand All @@ -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| {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit/docs/cpython.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit/examples/cpython_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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) |"
);
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit/examples/cpython_scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
5 changes: 3 additions & 2 deletions crates/bashkit/src/builtins/cpython/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 5 additions & 0 deletions crates/bashkit/src/builtins/cpython/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 19 additions & 1 deletion crates/bashkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion knowledge/runtimes/cpython-wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading