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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ jobs:
- name: regression corpus under green
run: make verify-green-corpus-only

# the runner is linux, so every other step above already runs green by
# default. this is the only step that exercises the PITH_GREEN=0 opt-out
# across the corpus.
- name: regression corpus under os threads
run: make verify-osthread-corpus-only

- name: build self-hosted compiler
run: make self-host

Expand Down
111 changes: 80 additions & 31 deletions Makefile

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ directly:

```
pith build bench/chan_fanout.pith && ./bench/chan_fanout 1000000
PITH_GREEN=1 ./bench/chan_fanout 1000000
PITH_GREEN=0 ./bench/chan_fanout 1000000
go build -o bench/chan_fanout_go bench/chan_fanout.go
rustc -O -o bench/chan_fanout_rust bench/chan_fanout.rs
zig build-exe -O ReleaseFast -femit-bin=bench/chan_fanout_zig bench/chan_fanout.zig
Expand All @@ -312,8 +312,8 @@ green wake-path work described below):

| lang | ms | messages/sec | peak rss |
|---|---:|---:|---:|
| pith (os threads) | 438 | 2.3 m | 3.0 mb |
| pith (`PITH_GREEN=1`) | 171 | 5.8 m | 3.0 mb |
| pith (`PITH_GREEN=0`, os threads) | 438 | 2.3 m | 3.0 mb |
| pith (green, the linux default) | 171 | 5.8 m | 3.0 mb |
| go | 75 | 13.3 m | 2.0 mb |
| rust | 135 | 7.4 m | 2.4 mb |
| zig | 135 | 7.4 m | 2.7 mb |
Expand Down
9 changes: 5 additions & 4 deletions bench/chan_fanout_bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
#
# bench/chan_fanout_bench.sh [messages] [trials]
#
# defaults: 1000000 messages, 5 trials. pith is measured twice, once on
# the default os-thread backend and once with PITH_GREEN=1. skips any
# language whose toolchain is not installed.
# defaults: 1000000 messages, 5 trials. pith is measured twice, once with
# PITH_GREEN=0 (os threads) and once with PITH_GREEN=1 (green). both are set
# explicitly, so the table means the same thing whichever backend the host
# defaults to. skips any language whose toolchain is not installed.
set -euo pipefail
cd "$(dirname "$0")/.."

Expand Down Expand Up @@ -47,7 +48,7 @@ printf '\nchan fanout — %s messages, median of %s trials\n\n' "$MESSAGES" "$TR
printf '%-12s %8s %14s %12s\n' lang ms msgs_per_sec peak_rss_kb

ref_checksum=""
for entry in "pith:$PITH:0" "pith-green:$PITH:1" "go:$GO:0" "rust:$RUST:0" "zig:$ZIG:0"; do
for entry in "pith-threads:$PITH:0" "pith-green:$PITH:1" "go:$GO:0" "rust:$RUST:0" "zig:$ZIG:0"; do
name=${entry%%:*}
rest=${entry#*:}
bin=${rest%%:*}
Expand Down
4 changes: 2 additions & 2 deletions bench/green_fanout.pith
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# counts and the peak rss should barely move — that flat line is the point.
#
# pith build bench/green_fanout.pith
# ./bench/green_fanout 1000000 # tasks (default 500000)
# PITH_GREEN=1 ./bench/green_fanout 1000000
# ./bench/green_fanout 1000000 # tasks (default 500000), green on linux
# PITH_GREEN=0 ./bench/green_fanout 1000000 # the os-thread backend, for contrast
#
# batch size is fixed small so the working set is a handful of tasks; the
# leak, if it were still there, would show as rss climbing with the total.
Expand Down
18 changes: 9 additions & 9 deletions cranelift/codegen/src/ir_consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,11 @@ fn runtime_func_ref(
.or_insert_with(|| codegen.module.declare_func_in_func(fid, builder.func)))
}

/// Whether this build should emit green-preemption safe-points. Off by default
/// the default binary is run os-thread and a flat loop pays a large relative cost
/// for the per-iteration check — so it is opt-in via `PITH_GREEN_PREEMPT`. Set it
/// at build time on the same binary you intend to run under `PITH_GREEN=1` to make
/// compute-bound green tasks preemptible.
/// Whether this build should emit green-preemption safe-points. Off by default,
/// because a flat loop pays a large relative cost for the per-iteration check and
/// most programs never have a compute-only task that needs descheduling. Set
/// `PITH_GREEN_PREEMPT` at build time to make compute-bound green tasks
/// preemptible.
fn green_preempt_enabled() -> bool {
matches!(
std::env::var("PITH_GREEN_PREEMPT").as_deref(),
Expand Down Expand Up @@ -468,10 +468,10 @@ pub fn compile_from_ir(
// green preemption safe-points are opt-in at compile time. a flat arithmetic
// loop pays a large relative cost for the inline flag check (its body is a
// couple of instructions, so the extra load+test+branch per iteration is a
// big fraction), and the default binary is almost always run os-thread. so
// the default codegen emits no check at all — exactly zero cost — and only a
// build that asks for preemption (`PITH_GREEN_PREEMPT=1`, the same build you
// then run under `PITH_GREEN=1`) inserts the safe-points. `None` here means
// big fraction), and most programs have no compute-only task that would ever
// need descheduling. so the default codegen emits no check at all — exactly
// zero cost — and only a build that asks for preemption
// (`PITH_GREEN_PREEMPT=1`) inserts the safe-points. `None` here means
// "emit nothing"; `Some(flag)` carries the imported flag symbol.
let preempt_flag = if green_preempt_enabled() {
Some(
Expand Down
5 changes: 3 additions & 2 deletions cranelift/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ parking_lot = { workspace = true }
threadpool = { workspace = true }
libc = { workspace = true }
ring = "0.17.14"
# stackful coroutines for the experimental green-thread backend (PITH_GREEN).
# phase 0 validated 0.2 handles cranelift-generated code on an alternate stack.
# stackful coroutines for the green-thread backend (the default on linux; see
# PITH_GREEN). 0.2 is validated against cranelift-generated code on an alternate
# stack.
corosensei = "0.2"

[lib]
Expand Down
2 changes: 1 addition & 1 deletion cranelift/runtime/src/concurrency/green.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! green-thread backend (experimental, behind `PITH_GREEN`)
//! green-thread backend (the default on linux; `PITH_GREEN` overrides)
//!
//! an M:N scheduler: many pith tasks run as stackful coroutines on a small
//! fixed pool of worker OS threads. spawning a task is a userspace enqueue, not
Expand Down
118 changes: 95 additions & 23 deletions cranelift/runtime/src/concurrency/scheduler.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! task backend selection (the green-thread seam)
//!
//! spawn and await funnel through here so a future M:N green-thread backend
//! can slot in behind a flag without touching the codegen-facing FFI. today
//! there is exactly one backend — os threads, unchanged — and this module is
//! inert: every path lands in the same `os_thread_*` code that has always run.
//! spawn and await funnel through here so the backend choice is made in exactly
//! one place and the codegen-facing FFI never has to know which one it got.
//! there are two: os threads, one kernel thread per task, and green, M:N
//! coroutines on a worker pool.
//!
//! the point of landing the seam first, empty, is that turning `PITH_GREEN`
//! on right now must be a no-op. that gives the real scheduler a safe place to
//! grow into: when run queues + stackful coroutines arrive, only the `Green`
//! arms below change, and the default (flag off) path stays byte-for-byte the
//! os-thread behavior.
//! green is the default on linux, because that is where the epoll reactor lives
//! and where it is faster on every shape this repo measures. everywhere else the
//! default is os threads: `netpoll_fallback` has no reactor, so a green task
//! waiting on a socket would hold its worker for the whole wait. `PITH_GREEN`
//! overrides the choice in either direction on any platform.

use super::{green, task};
use std::sync::OnceLock;
Expand All @@ -19,26 +19,58 @@ use std::sync::OnceLock;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Backend {
/// One OS thread per task (`std::thread::spawn`), joined on await. The
/// default and, in phase 1, the only backend with real behavior.
/// default on every platform but linux, and the opt-out on linux via
/// `PITH_GREEN=0`.
OsThread,
/// M:N green threads on a worker pool: tasks run as stackful coroutines on
/// a fixed set of workers. Selected by `PITH_GREEN`. Experimental — see
/// `green` for the P1a scope and the independent-tasks-only constraint.
/// a fixed set of workers. The default on linux; available elsewhere with
/// `PITH_GREEN=1`, but without a reactor to yield socket waits to.
Green,
}

/// The backend used when `PITH_GREEN` says nothing.
///
/// Linux gets green: the epoll reactor in `netpoll` yields socket waits, and
/// dns resolution runs on a blocking pool, so the two things that used to hold a
/// worker for an unbounded time no longer do. Other platforms compile
/// `netpoll_fallback`, which has no reactor at all, so green there would turn
/// every socket wait into a stalled worker.
const fn platform_default() -> Backend {
if cfg!(target_os = "linux") {
Backend::Green
} else {
Backend::OsThread
}
}

/// Map a `PITH_GREEN` value to a backend. `None` is the variable being unset (or
/// unreadable), which means the caller expressed no preference.
///
/// Unrecognized values fall to the platform default rather than to os threads,
/// so the only values that move the backend are the ones spelled out here. A
/// typo like `PITH_GREEN=maybe` then behaves exactly like not setting the
/// variable, which is easier to reason about than a rule where some unknown
/// values mean "off" and the absent one means "default".
///
/// The off spellings are deliberately generous. `PITH_GREEN=0` is the escape
/// hatch for anyone the linux default hurts, and someone reaching for it who
/// writes `no` or `NO` must not silently get green instead — that is the one
/// misreading with a real cost. Values are trimmed and lowercased for the same
/// reason.
fn backend_from_env(value: Option<&str>) -> Backend {
match value.map(|v| v.trim().to_ascii_lowercase()).as_deref() {
Some("1") | Some("on") | Some("true") | Some("yes") | Some("y") => Backend::Green,
Some("0") | Some("off") | Some("false") | Some("no") | Some("n") => Backend::OsThread,
_ => platform_default(),
}
}

/// Read `PITH_GREEN` once and cache the choice. Follows the same env-flag
/// pattern as the struct freelist toggle in `runtime_core`: parse on first
/// use, store in a `OnceLock`, never look again.
///
/// Off / unset / anything unrecognized => `OsThread`, so the default path is
/// exactly today's behavior with zero overhead beyond one cached read.
pub(crate) fn backend() -> Backend {
static BACKEND: OnceLock<Backend> = OnceLock::new();
*BACKEND.get_or_init(|| match std::env::var("PITH_GREEN").as_deref() {
Ok("1") | Ok("on") | Ok("true") => Backend::Green,
_ => Backend::OsThread,
})
*BACKEND.get_or_init(|| backend_from_env(std::env::var("PITH_GREEN").ok().as_deref()))
}

/// Spawn a task running `closure_handle`, returning its task handle.
Expand Down Expand Up @@ -87,10 +119,50 @@ pub(crate) fn task_detach(task_handle: i64) {
mod tests {
use super::*;

// `backend()` caches in a `OnceLock` and reads the ambient process env, so
// it cannot be exercised more than once per test binary. The parsing lives
// in `backend_from_env`, which is pure, and that is what these cover.

#[test]
fn unset_falls_to_the_platform_default() {
assert_eq!(backend_from_env(None), platform_default());
}

#[test]
fn platform_default_is_green_only_on_linux() {
if cfg!(target_os = "linux") {
assert_eq!(platform_default(), Backend::Green);
} else {
assert_eq!(platform_default(), Backend::OsThread);
}
}

#[test]
fn on_spellings_force_green() {
for value in ["1", "on", "true", "yes", "y", "ON", "True", " on "] {
assert_eq!(backend_from_env(Some(value)), Backend::Green, "{value:?}");
}
}

#[test]
fn off_spellings_force_os_threads() {
for value in ["0", "off", "false", "no", "n", "OFF", "False", " off ", "NO"] {
assert_eq!(backend_from_env(Some(value)), Backend::OsThread, "{value:?}");
}
}

#[test]
fn unrecognized_values_behave_like_unset() {
for value in ["", "maybe", "2", "green", "osthread"] {
assert_eq!(backend_from_env(Some(value)), platform_default(), "{value:?}");
}
}

#[test]
fn backend_defaults_to_os_threads_when_flag_absent() {
// The cached backend reflects the ambient env at first read. In the
// test harness `PITH_GREEN` is unset, so the default must hold.
assert_eq!(backend(), Backend::OsThread);
fn the_cached_backend_agrees_with_the_ambient_env() {
// Whatever the harness was launched with, the cached choice has to be
// the one the parser would make for it.
let expected = backend_from_env(std::env::var("PITH_GREEN").ok().as_deref());
assert_eq!(backend(), expected);
}
}
6 changes: 4 additions & 2 deletions cranelift/runtime/src/netpoll_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
//! stall the rest of them while one waits on i/o.
//!
//! that makes this good enough for local development on a mac and wrong for
//! serving load. the fix is a kqueue reactor behind these same two functions;
//! until then, ship green-thread workloads on linux.
//! serving load, and it is why green is only the *default* backend on linux
//! (see `concurrency::scheduler`). `PITH_GREEN=1` still turns it on here for
//! anyone who wants it. the fix is a kqueue reactor behind these same two
//! functions; until then, ship green-thread workloads on linux.

use std::os::unix::io::RawFd;

Expand Down
Loading
Loading