From b716b778e7189cd05af7411ac91d87aaa52991ec Mon Sep 17 00:00:00 2001 From: meh Date: Thu, 24 Sep 2026 05:59:05 +0700 Subject: [PATCH] Let a runtime choose its workers' stack size A worker's stack is where every task it polls runs, and Runtime had no way to size it: the threads took the std default, which is RUST_MIN_STACK when the process has it and two MiB when it does not. A caller whose tasks recurse deeply had to set that variable before launch, which makes a property of the runtime depend on whoever started the process. Runtime::builder() returns a Builder with workers, tuning, label and stack_size, and build() starts the threads. with_tuning now goes through it, so new, with_tuning and background() behave as before. A test runs a task with four MiB of locals on a runtime that asked for sixteen. Release as 0.1.14. --- Cargo.toml | 2 +- README.md | 6 ++ docs/user-guide.typ | 15 +++++ src/runtime.rs | 131 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 149 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d24838a..88f717d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nagoya" -version = "0.1.13" +version = "0.1.14" edition = "2021" rust-version = "1.85" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index d293954..50174a8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ Typst is the canonical documentation source. Run `sh scripts/build-guide.sh` to create both PDFs in `docs/`; Rust examples are extracted and checked by `cargo test --doc`. +Version 0.1.14 adds `Runtime::builder()`, which takes what `with_tuning` does +plus the worker threads' stack size. Without it a worker's stack is whatever +`RUST_MIN_STACK` says, or two MiB, so a caller whose tasks recurse deeply had +to set an environment variable to run them. `new`, `with_tuning` and +`background()` are unchanged. + Version 0.1.12 adds `process`, behind a feature of the same name: `Command`, `Child` and the child's pipes, spelled as `tokio::process` spells them, with the pipes as `futures-io` readers and writers. `spawn`, `output` and `status` diff --git a/docs/user-guide.typ b/docs/user-guide.typ index 83a71f7..34823a1 100644 --- a/docs/user-guide.typ +++ b/docs/user-guide.typ @@ -465,6 +465,21 @@ let rt = nagoya::runtime::Runtime::with_tuning( 8, nagoya::Tuning::almost_tokio(), "almost_tokio"); ``` +`Runtime::builder()` sets the same three and anything `with_tuning` has no +argument for. `stack_size(bytes)` sizes every worker's stack. Unset, a worker +gets the `std::thread` default: `RUST_MIN_STACK` if the process has it, two +MiB otherwise. Tasks run on the worker's stack, so a runtime whose tasks +recurse deeply should set it rather than depend on the environment. + +```rust +let rt = nagoya::runtime::Runtime::builder() + .workers(2) + .label("deep") + .stack_size(16 * 1024 * 1024) + .build(); +rt.pool().shut_down(); +``` + WorkTable's six named flavors are WorkTable registry policy, not six different Nagoya executors. Distinct flavor pools can interfere through idle spinning, so performance comparisons should isolate them in child diff --git a/src/runtime.rs b/src/runtime.rs index 1c70319..02b01fd 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -74,19 +74,46 @@ impl Runtime { /// the underlying pool's worker bitmap capacity. #[must_use] pub fn with_tuning(workers: usize, tuning: Tuning, label: &str) -> Self { - let workers = workers.max(1); + // Built directly rather than from `builder()`, whose default count asks + // the platform for a number this is about to replace. + Builder { + workers, + tuning, + label: alloc::string::String::from(label), + stack_size: None, + } + .build() + } + + /// Everything [`Runtime::with_tuning`] sets, plus what it does not, such as + /// the worker threads' stack size. + /// + /// Starts at what [`background`] is built with: one worker per fast core, + /// the default tuning, the label `nagoya` and `std`'s default stack. So a + /// caller that wants the shared pool's shape with one thing changed says + /// only that thing. + pub fn builder() -> Builder { + Builder::new() + } + + fn start(builder: &Builder) -> Self { + let workers = builder.workers.max(1); assert!( workers <= usize::BITS as usize, "worker count exceeds the pool bitmap capacity" ); let host = Arc::new(StdHost::new(workers)); let parking = host.clone(); - let pool = Pool::with_tuning(workers, 1024, host, tuning); + let pool = Pool::with_tuning(workers, 1024, host, builder.tuning); + let label = &builder.label; for id in 0..workers { let pool = pool.clone(); let runner = pool.runner(id); - std::thread::Builder::new() - .name(alloc::format!("{label}-{id}")) + let mut thread = std::thread::Builder::new().name(alloc::format!("{label}-{id}")); + if let Some(bytes) = builder.stack_size { + thread = thread.stack_size(bytes); + } + thread .spawn(move || { // Tells the scheduler that a wake happening on this thread // belongs to worker `id`, so it can skip the injector. See @@ -137,6 +164,80 @@ impl Runtime { } } +/// A [`Runtime`] described before it is started, from [`Runtime::builder`]. +/// +/// Exists for the settings that [`Runtime::with_tuning`] has no argument for. +/// Adding one there would break every caller; adding one here breaks none. +/// +/// # Stack size +/// +/// A worker's stack is where every task it polls runs, so it bounds how deep a +/// task can recurse. Unset, a worker gets what `std::thread` gives any thread: +/// `RUST_MIN_STACK` when the process has it, two MiB when it does not. That +/// makes the stack a property of whoever launched the process, which is wrong +/// for a caller whose tasks recurse by design and know how far. Such a caller +/// sets it here, and the environment stops mattering. +#[derive(Clone, Debug)] +#[must_use] +pub struct Builder { + workers: usize, + tuning: Tuning, + label: alloc::string::String, + stack_size: Option, +} + +impl Builder { + fn new() -> Self { + Self { + workers: shared_threads(), + tuning: Tuning::default(), + label: alloc::string::String::from("nagoya"), + stack_size: None, + } + } + + /// Worker threads. Zero means one, as in [`Runtime::new`]. Unset, the + /// count [`background`] uses. + pub fn workers(mut self, workers: usize) -> Self { + self.workers = workers; + self + } + + /// The idle policy. [`Tuning::default`] unless set. + pub fn tuning(mut self, tuning: Tuning) -> Self { + self.tuning = tuning; + self + } + + /// Threads are named `{label}-{id}`. `nagoya` unless set. + pub fn label(mut self, label: &str) -> Self { + self.label = alloc::string::String::from(label); + self + } + + /// Each worker thread's stack, in bytes. + /// + /// Rounded up by the platform to its page size and minimum, as + /// `std::thread::Builder::stack_size` is. See the type's documentation for + /// what happens when it is not set. + pub fn stack_size(mut self, bytes: usize) -> Self { + self.stack_size = Some(bytes); + self + } + + /// Start the threads. + /// + /// # Panics + /// + /// As [`Runtime::with_tuning`] does: if a thread cannot be started, which + /// includes a stack the platform will not allocate, or if the worker count + /// exceeds `usize::BITS`. + #[must_use] + pub fn build(&self) -> Runtime { + Runtime::start(self) + } +} + /// Threads for the shared pool. /// /// The machine's parallelism, except that on a machine with more than one @@ -318,6 +419,28 @@ mod tests { assert_eq!(worker, Some(Some(0))); } + /// A task that needs more stack than `std`'s two MiB default runs on a + /// runtime that asked for more. + /// + /// Four MiB of locals overflows a default worker, so this fails without + /// the setting unless the process was started with a large + /// `RUST_MIN_STACK`, which is the dependence the setting exists to remove. + #[test] + fn a_built_runtime_gives_its_workers_the_stack_it_asked_for() { + let runtime = Runtime::builder() + .workers(1) + .label("stack-test") + .stack_size(16 * 1024 * 1024) + .build(); + let task = runtime.spawn(async { + let buffer = core::hint::black_box([1_u8; 4 * 1024 * 1024]); + buffer.iter().map(|&byte| usize::from(byte)).sum::() + }); + let sum = block_on(task); + runtime.pool().shut_down(); + assert_eq!(sum, Some(4 * 1024 * 1024)); + } + #[test] fn the_shared_pool_is_one_pool() { assert!(core::ptr::eq(background(), background()));