diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e39fe4ff..794f53b2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ using this cargo binary (not the musl `target/release/` install). ### What works on each OS Linux is the **supported operator IO** target (`io_uring` when the ring opens). -Darwin defaults to the **pool** session; Windows to **IOCP** (no IoRing). Matrix: +Darwin defaults to libc **pread** (`RBITCOIN_IO=pool` opts into the pool session); Windows to **IOCP** (no IoRing). Matrix: [`docs/io-modality.md`](./docs/io-modality.md). PR CI still smokes native store IO on Windows and Darwin. diff --git a/changelog.d/darwin-pread-default.md b/changelog.d/darwin-pread-default.md new file mode 100644 index 000000000..8df0e119e --- /dev/null +++ b/changelog.d/darwin-pread-default.md @@ -0,0 +1,8 @@ +Changed + +- **macOS reads with libc pread by default.** + The worker-pool session handed every read to a pool thread through one + shared lock. In signet IBD that cost more than the reads: about 20% slower + end to end with a warm or cold cache, and four times the system CPU. + `RBITCOIN_IO=pool` still opens the pool session. Linux and Windows are + unchanged. diff --git a/crates/rbitcoin-store/src/bulk_io.rs b/crates/rbitcoin-store/src/bulk_io.rs index 280d23e2f..99e01259d 100644 --- a/crates/rbitcoin-store/src/bulk_io.rs +++ b/crates/rbitcoin-store/src/bulk_io.rs @@ -23,8 +23,9 @@ //! //! # Non-Linux //! -//! Darwin default is the **pool** completion session (kqueue is not a -//! regular-file backend). Windows uses IOCP. +//! Darwin default is libc **pread** (kqueue is not a regular-file backend, +//! and the pool session's per-read handoff cost more than the reads in IBD); +//! `RBITCOIN_IO=pool` opens the pool session there. Windows uses IOCP. //! Machines stay staged; they do not flatten to one-shot `pread`. See //! `docs/io-modality.md` and `docs/concurrency.md`. @@ -66,11 +67,7 @@ pub fn io_uring_enabled() -> bool { 1 => true, 2 => false, _ => { - let want = match parse_io_token() { - Some(IoToken::Pread) => false, - Some(_) | None => true, - }; - if !want { + if !session_wanted(parse_io_token()) { URING_MODE.store(2, Ordering::Relaxed); return false; } @@ -87,6 +84,16 @@ pub fn io_uring_enabled() -> bool { } } +fn session_wanted(token: Option) -> bool { + match token { + Some(IoToken::Pread) => false, + Some(_) => true, + // Darwin's only session is the worker pool. Its per-read handoff cost + // more than the read in signet IBD (about 20% slower, warm or cold). + None => !cfg!(target_os = "macos"), + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum IoToken { Uring, @@ -129,15 +136,11 @@ fn default_session_kind() -> crate::uring_session::SessionKind { { SessionKind::Uring } - #[cfg(target_os = "macos")] - { - SessionKind::Pool - } #[cfg(windows)] { SessionKind::Iocp } - #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] + #[cfg(not(any(target_os = "linux", windows)))] { SessionKind::Pool } @@ -600,6 +603,19 @@ fn pwrite_one(op: &mut WriteOp<'_>) { #[cfg(test)] mod tests { use super::*; + + #[test] + fn unset_io_env_opens_a_session_except_on_darwin() { + #[cfg(target_os = "macos")] + assert!(!session_wanted(None), "Darwin defaults to libc pread"); + #[cfg(not(target_os = "macos"))] + assert!(session_wanted(None)); + assert!( + session_wanted(Some(IoToken::Pool)), + "RBITCOIN_IO=pool still opens the pool session" + ); + assert!(!session_wanted(Some(IoToken::Pread))); + } use std::io::Write; fn dummy_handle() -> IoHandle { diff --git a/crates/rbitcoin-store/src/io_session_pool.rs b/crates/rbitcoin-store/src/io_session_pool.rs index c67b14db0..97eea9b3b 100644 --- a/crates/rbitcoin-store/src/io_session_pool.rs +++ b/crates/rbitcoin-store/src/io_session_pool.rs @@ -1,8 +1,9 @@ //! Completion-ring facade over a bounded worker pool. //! //! Same harvest contract as io_uring (`user_data`, drain, in-flight cap). -//! Darwin's file AIO is a thread pool; this is the honest session there and -//! the Linux CI pin (`RBITCOIN_IO=pool`). +//! Darwin's file AIO is a thread pool; this is the honest session there when +//! `RBITCOIN_IO=pool` asks for it (Darwin defaults to libc pread), and the +//! Linux CI pin. //! //! Workers are **process-shared**. Each [`PoolEngine`] is a session handle //! with its own CQE queue so harvest does not steal another thread's diff --git a/crates/rbitcoin-store/src/uring_session.rs b/crates/rbitcoin-store/src/uring_session.rs index 29de1e37c..7c761bc70 100644 --- a/crates/rbitcoin-store/src/uring_session.rs +++ b/crates/rbitcoin-store/src/uring_session.rs @@ -176,7 +176,7 @@ fn finish_drain_hard_cap(pending: usize) -> Result<(), StoreError> { pub enum SessionKind { /// Linux io_uring. On Windows this opens IOCP. Uring, - /// Worker-pool completion ring (Darwin default; Linux `RBITCOIN_IO=pool`). + /// Worker-pool completion ring (`RBITCOIN_IO=pool` on Darwin or Linux). Pool, /// Windows IOCP. Iocp, @@ -1835,14 +1835,13 @@ mod tests { } #[test] + #[cfg(any(target_os = "linux", windows))] fn default_kind_follows_os_when_env_unset() { if std::env::var_os("RBITCOIN_IO").is_some() { return; } #[cfg(target_os = "linux")] assert_eq!(crate::bulk_io::resolved_session_kind(), SessionKind::Uring); - #[cfg(target_os = "macos")] - assert_eq!(crate::bulk_io::resolved_session_kind(), SessionKind::Pool); #[cfg(windows)] assert_eq!(crate::bulk_io::resolved_session_kind(), SessionKind::Iocp); } diff --git a/docs/io-modality.md b/docs/io-modality.md index b42cc60d3..95b85474a 100644 --- a/docs/io-modality.md +++ b/docs/io-modality.md @@ -37,11 +37,11 @@ Unknown tokens (including deleted `mmap`) fall through to the platform default. | Token | Backend | |-------|---------| | `uring` / `io_uring` | Linux `io_uring`. On Windows the token opens **IOCP** | -| `pool` | Worker-pool completion ring (Darwin **default**; Linux CI pin) | +| `pool` | Worker-pool completion ring (opt-in on Darwin; Linux CI pin) | | `iocp` | Windows IOCP (Windows **default**) | | `pread` / `fd` / `libc` / `pwrite` | Disable session; libc positional IO (+ workers for reads) | -**Defaults:** Linux `io_uring` if the ring opens, else libc **pread** (session off). Darwin **pool**. +**Defaults:** Linux `io_uring` if the ring opens, else libc **pread** (session off). Darwin libc **pread** (session off). Windows **IOCP**. Windows IoRing is not supported. **Windows table handles** are `FILE_FLAG_OVERLAPPED` so IOCP can bind. @@ -56,8 +56,11 @@ heap OVERLAPPEDs). **kqueue is not a regular-file backend.** Darwin files report ready immediately; `read` still blocks. POSIX AIO (`EVFILT_AIO`) and `dispatch_io` are also thread pools (`kern.aiomax` default 16) — same class as `pool`, not an -SQ/CQ. The pool session is the honest Darwin completion ring. Pool -**workers** are process-shared; each TLS session keeps its own CQE queue. +SQ/CQ. The pool session is the honest Darwin completion ring, but each read +costs a shared-queue lock and two thread wakeups. In signet IBD that cost more +than the reads, warm or cold cache (about 20% slower end to end, four times the +system CPU), so Darwin defaults to libc pread and `RBITCOIN_IO=pool` opts in. +Pool **workers** are process-shared; each TLS session keeps its own CQE queue. **TLS session:** one completion session per OS thread (`with_thread_local`). Nested calls panic. Harvest tracks pending `(kind, epoch, slot)`. A CQE that @@ -149,7 +152,7 @@ RAM fence (~15 MiB at 1M blocks), not a file. | Block index build (filters + tweaks) | `RBITCOIN_IO` | `read_index_window`: one held TLS session per window, four stages (`create.loc` / `input.loc` / `seqsigwit.loc` windows → block `txout` / `input.body` / `txid.body` spans → parent `create.loc` + P2TR-output `seqsigwit.body` + parent `txid.body` → parent `txout`), offsets from RAM checkpoints so nothing nests. Parents read once per window. Short/errno on a live session libc-completes; no session → serial pread through the same stages. Writes are not on the ring: one filter put and one tweak put per window. | | SP-tweak hole serve | `RBITCOIN_IO` | `tweaks_for_height` for heights the index has not sealed: idx ranges **before** TLS ring; uring/pread `txout.body`, then `seqsigwit` + parent `txout` for P2TR only. | -Default: Linux uring if the ring opens else libc pread; Darwin pool; Windows +Default: Linux uring if the ring opens else libc pread; Darwin pread; Windows IOCP. Ring depth **128** (merge may grow). `RBITCOIN_IO=pread` forces libc. ### Table transport (all fd) diff --git a/scripts/ci-os-smoke.sh b/scripts/ci-os-smoke.sh index 75fd04ee8..d4a2b2880 100755 --- a/scripts/ci-os-smoke.sh +++ b/scripts/ci-os-smoke.sh @@ -16,7 +16,7 @@ STORE_PLATFORM_FILTERS=( sorted_run::tests::host_mem sorted_run::tests::darwin_vm io_backend::tests - uring_session::tests::default_kind_follows_os + bulk_io::tests::unset_io_env_opens_a_session uring_session::tests::pool_ fuse8_filter::tests::no_false_negatives_and_roundtrip segmented_head::tests::insert_roll_seal_lookup_roundtrip @@ -44,6 +44,8 @@ case "${CI_OS_SMOKE_UNAME:-$(uname -s)}" in STORE_PLATFORM_SKIPS+=(concurrent_readers_during_append_and_grow) # `mod io_session_iocp` is #[cfg(windows)] — the tests do not exist elsewhere. STORE_PLATFORM_FILTERS+=(io_session_iocp) + # Linux and Windows only: Darwin opens no session by default. + STORE_PLATFORM_FILTERS+=(uring_session::tests::default_kind_follows_os) PRIMITIVES_PLATFORM_FILTERS+=(loc_simd::tests) ;; Darwin*) diff --git a/scripts/ci-os-smoke.test.sh b/scripts/ci-os-smoke.test.sh index 8298fceb7..a0f15b87f 100755 --- a/scripts/ci-os-smoke.test.sh +++ b/scripts/ci-os-smoke.test.sh @@ -36,8 +36,10 @@ assert_ok "dry-run lists SH RAM / host_mem probes" \ grep -q "sorted_run::tests::host_mem" <<<"$out" assert_ok "dry-run lists Darwin vm page math" \ grep -q "sorted_run::tests::darwin_vm" <<<"$out" -assert_ok "dry-run lists default session kind" \ - grep -q "uring_session::tests::default_kind_follows_os" <<<"$out" +assert_ok "dry-run lists the default IO session per OS" \ + grep -q "bulk_io::tests::unset_io_env_opens_a_session" <<<"$out" +assert_ok "only Windows lists the default session kind test" \ + test "$(grep -c 'uring_session::tests::default_kind_follows_os' <<<"$out" || true)" = "0" assert_ok "dry-run lists pool session tests" \ grep -q "uring_session::tests::pool_" <<<"$out" @@ -54,6 +56,8 @@ assert_ok "Windows dry-run still confirms a few blocks" \ grep -q "chain_view_pin_asof_reorg" <<<"$out" assert_ok "Windows dry-run lists IOCP session tests" \ grep -q "io_session_iocp" <<<"$out" +assert_ok "Windows dry-run pins the IOCP default session kind" \ + grep -q "uring_session::tests::default_kind_follows_os" <<<"$out" assert_ok "Windows dry-run pins loc SIMD (SSE2) vs scalar" \ grep -qx "primitives=loc_simd::tests" <<<"$out"