Skip to content

Codex/perf benchmark improvements - #184

Open
hodgesds wants to merge 18 commits into
mainfrom
codex/perf-benchmark-improvements
Open

Codex/perf benchmark improvements#184
hodgesds wants to merge 18 commits into
mainfrom
codex/perf-benchmark-improvements

Conversation

@hodgesds

Copy link
Copy Markdown
Owner

No description provided.

hodgesds and others added 18 commits August 22, 2026 13:19
A stress-ng MM/process matrix under KASAN (SMP=16) panicked with a store
to a poisoned address in close_kernel_span's kern_span_start_ns.swap(0),
reached from the CPL0 timer-preempt accounting hook:

  try_preempt -> pause_user_kernel_span -> pause_current_kernel_span
    -> pause_kernel_span_for -> close_kernel_span   (store to freed uctx)

try_preempt had already validated the preempted KernelTask as live (its
own-stack tripwire passed), so current_user_task() was not resolving that
task -- it fell through to the legacy per-CPU CURRENT cell. In the
own-stack model that cell is published once per poll (install_current)
but its clear site is the longjmp poll tail (clear_current), which
own-stack execution never reaches: it diverges into user mode via
kernel_switch and parks/exits without returning through the poll. So
after any own-stack task exits and its Arc<Task>/uctx is RCU-freed, the
cell dangles at freed memory.

The race: a timer preempts a freshly polled task before its poll reaches
publish_current_task, so its owner context (user_context) is still null.
current_user_task() then fell back to the stale cell and the accounting
pause stored into the previous task's freed uctx. KASAN-slow fresh-task
startup (address-space activate, page-table walks) is what widened the
window enough to hit reliably.

Fix: in the own-stack model, resolve the current uctx only from the
scheduler-published owner context (lifetime-coupled to the live
KernelTask, and correct across direct resume + migration). When no owner
is published, return None instead of reading the legacy cell -- a task
that has not published a context has no open kernel span to account
against. The legacy cell remains authoritative only in the longjmp
model. The own-stack-vs-legacy precedence is extracted into a pure
current_user_task_source() helper so it is deterministically testable
without toggling the global own-stack latch (shared across the
concurrent kernel-test run).

Regression test smoke_own_stack_never_reads_stale_legacy_cell asserts
the split: own-stack + no owner + non-null legacy cell resolves to None
(the exact pre-fix UAF path). Validated: the KASAN stress-ng matrix that
reproduced the panic now completes clean (36/36 stressors); kernel-test
7291 pass / 0 fail; clippy x86_64 + aarch64 and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking stream send(2)/sendmsg(2) now park on the connected ring's
durable capacity instead of failing early. O_NONBLOCK and MSG_DONTWAIT
return EAGAIN immediately. sendmmsg(2) returns an already-transmitted
prefix without retrying it. Adds socket_send_would_block() as the shared
non-blocking predicate and covers the paths with abi_socket_tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a wide-fanout BPF program image to the bench suite (bpf/src/bench.rs)
with the verifier fixpoint support it needs, and extends the xtask
bpf_bench runner to accept a previous green-main record via --baseline and
a release record via --release-baseline, archiving series for comparison.
Specs updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chroot_run harness now mounts a fresh tmpfs at /mnt/dev/shm and a
fresh /mnt/tmp before entering the root, so the POSIX shm stressor finds
its shared-memory mount (a plain bind of /dev omits the nested /dev/shm).
Rebuilds chroot_run_x86_64 and refreshes REGEN_stress_rootfs.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
madvise_dontneed cleared each resident page with a separate
unmap_4kb_local call, and that helper acquires the page-table-root lock
and walks PML4→PDPT→PD→PT every time. A large MADV_DONTNEED/MADV_FREE
therefore paid one root-lock acquisition and a full upper-level walk per
page. Clear the whole page-aligned per-region intersection with a single
unmap_4kb_local_range (one root lock, one cached upper-level walk),
mirroring what the aarch64 path already did with unmap_4kb_range and what
punch_fixed / rewrite_perms_pages already use on x86_64. The per-page
loop now only collects frames to release.

Behavior is unchanged: the same leaves are cleared (missing leaves over
unfaulted holes are benign), the same private frames are freed, LOCKED
and SHARED regions are still skipped, and the single post-teardown
cross-CPU broadcast still precedes any frame reuse.

Adds smoke_memory_madvise_dontneed_range_frees_all_and_keeps_hole: a
multi-page region with an interior unfaulted hole must free every
resident frame and zero its slot while leaving the hole's slot untouched
— coverage the single-page release test could not provide. Validated:
kernel-test 7292 pass / 0 fail; clippy x86_64 + aarch64 and fmt clean;
the KASAN stress-ng madvise stressor passes with no sanitizer catch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A stress-ng os-class sweep (bad-altstack) panicked NARF with an
unrecoverable kernel #PF: error=0x3 (supervisor write to a present,
write-protected page), cr2 in the user half, RIP in deliver_signal's
CPL=0 signal-frame write under the SMAP window.

deliver_signal pre-flights the frame's target pages with
ensure_user_range_writable, which backed demand/guard pages and treated
demand_alloc_page's AlignmentMismatch (page already present) as "writable
now". But presence is not writability: bad-altstack points sigaltstack at
a PROT_READ page, so the pre-flight passed, the CPL=0 frame write faulted
on the read-only page, and the #PF handler's supervisor-COW recovery
correctly refused it (not a COW page) — leaving only the panic path.
Userspace could thus panic the whole kernel by taking a signal with its
(alt)stack on a write-protected page.

Add AddressSpace::user_page_writable_or_resolve: for a present page it
gates on RegionPerms::WRITE and resolves a COW copy in place (cow_split +
remap); a genuinely read-only mapping returns false. ensure_user_range_
writable now calls it after backing each page, so an unwritable target
makes deliver_signal return false and the caller applies the signal's
default action (terminate the task) — Linux's force_sigsegv model — while
the kernel survives. Legitimate deliveries to writable or COW stacks are
unaffected.

Regression test smoke_memory_user_page_writable_gates_readonly asserts a
PROT_READ page is refused, a writable page accepted, an unmapped page
refused. Validated: kernel-test 7293 pass / 0 fail; clippy x86_64 +
aarch64 and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Listing a directory of N entries cost O(N²): sys_getdents64 called
Dir::enumerate_async(cursor, 1) once per emitted record, and the memfs
backend's enumerate does entries.iter().skip(cursor) — re-walking the
BTreeMap from the start on every call. Mass directory workloads
(stress-ng chdir/dirdeep create 8192 dirs, getdents them all, remove
them) were three orders of magnitude slower than their filesystem-class
peers (chdir 0.27 vs dir 1445 bogo-ops/s).

Take a single tail snapshot enumerate_async(cursor, usize::MAX) once per
syscall and serve records from it — one BTreeMap traversal per buffer
fill instead of one per record. This reuses the snapshot pattern the
handler already had for the procfs iter() fallback (preserved). Semantics
are unchanged: identical entry order, cursor advances exactly once per
written record, a buffer-full stop breaks without advancing the cursor so
the next call resumes from the persisted position (block-at-a-time
getdents), and "."/".." handling, d_type mapping, and EBADF/ENOTDIR are
untouched.

smoke_memfs_large_dir_enumerate_walks_all drives 2048 entries through the
exact handler snapshot/advance protocol and asserts each name appears
once in sorted order, then lookup+unlink of all N.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pipe transfers moved one byte at a time: PipeRead::read popped with a
q.pop_front() loop and PipeWrite::write pushed with a q.push_back() loop,
so a 64 KiB transfer did 64 K individual VecDeque ops. splice compounded
it — copy_fd_to_fd looped in 4096-byte chunks, each a fresh heap Vec plus
two fd-table lock acquisitions (16 iterations for a full pipe). splice
ran at 360 bogo-ops/s vs the plain pipe stressor's 4438.

vmsplice additionally made no forward progress (0 bogo-ops): with the
pipe full, PipeWrite::write returned Ok(0) and the handler reported a
0-byte success instead of blocking or signalling EAGAIN, so the
vmsplice→splice loop stalled once the pipe filled.

- pipe.rs: bulk ring I/O — PipeRead::read uses q.drain(..n),
  PipeWrite::write uses q.extend(...). Benefits splice, vmsplice, pipe,
  fifo, ring-pipe.
- core.inc.rs: copy_fd_to_fd CHUNK 4096 → 65536 (one default pipe
  buffer), so a full-pipe splice completes in a single read+write pair.
- sys_vmsplice.rs: check for a full pipe before gathering any bytes
  (keeps re-execution idempotent) — SPLICE_F_NONBLOCK returns EAGAIN,
  blocking parks via park_reexecute_on_io. Return value stays
  bytes-gathered; EINVAL/EFAULT semantics unchanged.

smoke_abi_fdio_vmsplice_full_pipe_eagain fills the 64 KiB pipe, asserts a
SPLICE_F_NONBLOCK vmsplice returns EAGAIN and queues nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every brk grow registered a NEW VMA and then called whole-address-space
materialize(), which re-walks every region and its backing vector. N
small grows therefore created N VMAs and cost O(N²). Shrink took a deep
regions_snapshot() before selecting regions to unmap.

Add AddressSpace::brk_extend_region: under the region lock it checks the
ordered index's successor for tail overlap (O(log VMA)) and either
extends the single existing heap VMA in place (phys.extend + len bump) or
inserts the one heap region on the first grow. sys_brk's grow now calls
it and materializes ONLY the appended range (materialize_range), and
shrink is a single punch_fixed of the freed tail selected from the
ordered index — no whole-AS snapshot. Growth is O(log VMA + pages added).

Preserved: the AS-scoped break shared across CLONE_VM siblings; exact
partial-page break semantics (a within-page move records the break with
no PTE work); OLD-break rollback on alloc / register / PTE-install
failure; and fork inheritance (the single heap VMA clones like any
region, brk_top already inherited).

smoke_brk_single_growable_vma does 64 one-page grows asserting exactly
one heap VMA throughout and zeroed backing, a fresh-page grow that adds
exactly one page, a within-page grow/shrink that leaves the VMA
unchanged, an over-ceiling grow that reports the old break, and a real
shrink that tail-punches to a single truncated VMA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each growable-stack #PF ran AddressSpace::try_grow_stack, which found the
STACK_GUARD via regions.iter().find() — a linear scan over every VMA —
and then promoted the guard into a fresh STANDALONE R+W region without
merging it into the stack VMA directly above. Every fault therefore added
one more region, so N growths cost O(N²) scanning an ever-growing table
(plus VMA churn and a dense-phys clone per fault). stress-ng's stack
stressor hammers this: NARF 517 vs Linux ~247k bogo-ops/s.

Locate the guard in O(log n) via the ordered index
(containing().or_else(successor())) and extend the adjacent stack VMA in
place: consume the one-page guard, prepend its frame to the stack
region's phys, lower the region base, grow its len, and reinstall one
fresh guard below — keeping the stack a SINGLE VMA across arbitrarily
many growths and preserving its own perms (an executable stack stays
executable, which the old hardcoded READ|WRITE dropped). A fallback still
installs a standalone span when no stack VMA sits above the guard.

Guard/overflow semantics are unchanged: the MAX_GROW bound, the
MMAP_WINDOW_TOP floor, and the guard-collision -> Overlap (real SIGSEGV)
checks are preserved, so a wild write far below the stack still faults.
Fresh pages are alloc_user_frame'd + zeroed (refcount 1), writable even
if the merged region carries COW.

Tests: smoke_memory_try_grow_stack_extends_one_vma (16 growths stay one
EXEC VMA, base moves down 16 pages, all backed+zeroed) and an updated
smoke_memory_try_grow_stack_sequential (three growths coalesce into one
3-page VMA based two pages below the guard, single trailing guard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sysvipc.rs was already keyed by BTreeMap and locked per-syscall, so the
gap versus Linux (sem-sysv 363x, msg 144x) was not contention — it was
heap allocation on every hot-path op:
- semop cloned the whole semaphore-value vector (set.sems.clone()) and
  reassigned it, plus copy_from_user_vec'd the sops array — three heap
  ops per semop for what is a couple of integer compares;
- msgsnd copied the message into a combined Vec then .to_vec()'d the
  payload again (two allocs per send);
- msgrcv allocated a fresh combined mtype+payload Vec per receive.

Read the sops array into a fixed on-stack buffer (entered only after the
existing nsops<=MAX_SOPS guard) and apply the semop in place with
rollback: bounds-check every sem_num, apply each op against the running
value (preserving repeated-sem_num accumulation, i.e. Linux's atomic
block), and on the first blocking op undo the applied deltas. msgsnd
validates the mtype header from a stack slot and allocates the queued
payload exactly once; msgrcv writes the header and payload with two
direct copy_to_user calls. No heap allocation on the semop/msgsnd/msgrcv
hot paths.

Semantics preserved: id/key allocation, semop all-or-nothing + EAGAIN,
msg type selection (>0 first-of-type, <0 lowest-<=|type|, 0 first) and
byte layout, EIDRM, and the *ctl fields. The in-place rollback is sound
because verify+apply run under one SEMS lock with no await between.

Tests: semop atomic rollback (blocked multi-sop leaves values intact),
multi-sop commit with repeated sem_num accumulation, 64-set keyed
lookup, and msgrcv type selection with the two-write output path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sigqueue's pending-signal store is already per-task bucket-sharded (no
global lock) and the return-to-user path early-outs on an empty pending
mask, but each rt_sigqueueinfo still paid two avoidable costs (sigq: NARF
2639 vs Linux 366450 bogo-ops/s):
- capture_queued_siginfo read the 32-byte siginfo via copy_from_user_vec,
  which heap-allocates a throwaway Vec on every send;
- after enqueue, each send handler called sigqueue_depth(), a SECOND
  SIGQUEUE_INFO lock acquisition and another range().sum() scan over the
  task's queued signals, purely to decide the back-pressure yield — O(n)
  per enqueue, O(n^2) under a lagging consumer.

Read the siginfo into a fixed [u8; 32] stack buffer. Fold the depth into
the enqueue: store_sigqueue_info_depth returns the post-insert queued
depth (from the sum the RT cap check already computes), and the send
handlers use it directly — one SIGQUEUE_INFO round-trip per send instead
of two.

Semantics preserved: RT signals (>=32) queue every instance FIFO with
per-instance payload; standard signals coalesce to the latest; si_value/
si_code/si_pid bytes unchanged; the RLIMIT_SIGPENDING cap still returns
EAGAIN; mask blocking, SIGRETURN_SAVED_MASK, and deliver-on-return
untouched.

Test: enqueue three RT instances (depth 1/2/3), a standard signal twice
(depth rises by one, coalesced), then drain asserting RT FIFO order +
payloads and the single coalesced standard instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolve_vfs_symlink_path was O(depth²): for each of the D path components
it rebuilt the /c0/…/ci prefix and re-resolved it from the mount root
(1+2+…+D walks). It is on the hot path of open/mount/chdir, so a deep
tree (dirdeep) or many chdirs cost quadratically — dirdeep 167x, chdir
39x slower than Linux.

Add a single O(depth) forward walk (resolve_vfs_symlink_path_fast) that
carries the parent-dir handle and looks up each component NOFOLLOW
(lookup_async returns the raw node), so a symlink is SEEN, not silently
followed. It returns the path unchanged only after proving every
component is a real directory (last may be a real file) with no symlink
and no mount ambiguity; ANY symlink component, or a path that crosses /
ancestors a nested mount (fast_walk_stays_in_one_mount), bails to the
unchanged slow per-prefix loop, which still does splice / absolute-target
re-root / ELOOP / chroot exactly as before. No lock is held across
block I/O (the fs Arc is cloned out first).

This is the corrected form of an earlier attempt that used auto-following
directory handles and so skipped symlink expansion; the difference is the
NOFOLLOW single-component lookup.

Tests: a deep symlink-free tree create/chdir/open/rmdir (exercises the
fast path) and an intermediate directory-symlink case (forces the slow
path); both the mount-crossing and directory-symlink gate cases are
covered. "."/".."/trailing-slash (normalize_abs), ELOOP, mount
traversal, chroot scoping and all path errors are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sys_shmat called AddressSpace::materialize() — the whole-address-space
walk — on every attach, so a shmget/shmat/shmdt loop re-installed PTEs
for every VMA in the address space per attach (shm-sysv ~28x slower than
Linux).

Use the range-scoped materialize_range(base, map_len) primitive (the same
one sys_mmap uses) over exactly the attached segment. Derive the region
len and the VA reservation from the page-aligned frame count so the
Region's len matches its scatter list — which also fixes a latent
sub-page-segment attach that failed map_region_inner's page-alignment
assert.

Shared-frame lifetime is unchanged: the mapping stays RegionPerms::SHARED
under with_shared_mapping_transaction, attach retains via
retain_shared_frames and detach/drop releases via release_shared_phys
(never free_frame), so the cross-AS marginal-buddy double-free guard is
intact; materialize_range only installs PTEs, mapping the SAME frames
(shared visibility, no copy). Address selection and SHM_RDONLY unchanged.

Test: two address spaces attach the same frame via materialize_range,
both translate to the same phys, a sentinel written through one is read
via the other (shared, not copied), and dropping one AS leaves the frame
mapped+live in the other (no free-while-attached).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two problems from the sweep: vm-splice did 0 bogo-ops and splice was 132x
slower than Linux.

vm-splice made no progress because sys_vmsplice treated every fd as a
gather target and called ops.write() on it. The stress-ng vm-splice loop
issues vmsplice on the pipe READ end (SPLICE_TO_USER — drain pipe to
memory); PipeRead::write returns BadFd, so the handler hit its EINVAL arm
and the stressor aborted on iteration one. Split sys_vmsplice on
direction (pipe_peek is Some only on the read end): the read end drains
the pipe out to user memory (vmsplice_from_pipe, copy_to_user
iovec-at-a-time honoring EAGAIN/park/EOF); the write end gathers as
before. The iovec array is validated once up front so a bad iovec
reports EFAULT regardless of the fd; a bad fd still reports EBADF.

splice paid a per-call 64 KiB vec![0u8; N] memset plus two memcpys and
two fd-table locks in copy_fd_to_fd. Add PipeRead::pipe_take (drain the
ring into a Vec via drain(..n).collect(), no zero-init/second copy) and
pipe_unread (prepend a sink's unwritten tail back, zero-loss); splice
from a pipe source now uses splice_pipe_source (drain straight into the
sink write), while a non-pipe source keeps the offset-aware
copy_fd_to_fd. Draining a pipe now also bumps the readiness generation so
a blocked writer/reader is actually woken (a latent lost-wakeup).

Semantics preserved: return value = bytes moved (partial sink write
pushes the tail back), EAGAIN on nonblocking empty/full, SPLICE_F_NONBLOCK,
offset write-back for non-pipe fds (pipe sources are ESPIPE), PIPE_BUF
atomicity, EOF/HUP.

Tests: splice N bytes pipe->pipe (count + drained), vmsplice drain from a
pipe read end, and the vmsplice-in -> splice-out -> vmsplice-in
round-trip that used to abort now makes progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Syscall::from_raw ran a linear scan of the ~300-row LINUX_TABLE (then the
NARF extension table) on EVERY syscall — via kernel_syscall_entry and the
plain-path dispatch — and again on every parked-syscall backstop re-poll.
High Linux numbers (futex=202, clock_gettime=228, rseq=293) landed deep
in the scan, so the reverse lookup was a real per-syscall tax on top of
the entry/exit register shuffle. A 1:1 benchmark (both in QEMU) showed
NARF 34–541x slower than Linux with the gap tracking syscalls-per-op, so
per-syscall fixed cost is the dominant lever.

Build a compile-time direct-indexed map [Option<Syscall>; 1024] keyed by
wire number (a const-eval assert fails the build if a new Linux row ever
exceeds the range), and index it in O(1). NARF extensions (0x4000+) stay
a short linear scan since they're sparse and far out of the Linux range.
from_raw drops `const` (no const callers); raw() is unchanged.

Test smoke_syscall_from_raw_matches_tables checks the O(1) result equals
the original top-down linear scan for every wire number in both tables
and across the whole dense range (a variant can be aliased to more than
one number, so it compares against the scan oracle, not a raw()
round-trip). Validated: kernel-test 7310 pass / 0 fail; clippy x86_64 +
aarch64 and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Scheduler policy trait had pick_next + enqueue/dequeue notifications +
CPU lifecycle hooks, but nothing for the WAKEUP path — the piece Linux's
try_to_wake_up centers on (select_task_rq -> enqueue -> wakeup_preempt ->
task_woken). A wake in NARF is a bare flag=true + resched IPI on the
task's home CPU, with no CPU selection, no wakeup preemption, and no wake
event, so a task woken onto a busy CPU waits at that CPU's queue tail
while other cores sit idle — the scheduling hop that dominates
syscall-rate-bound latency, and (with no centralized wake) the surface
for the executor-halt lost-wakeup.

Add the missing advisory hooks to the trait, mirroring Linux's sched_class
(kernel/sched/sched.h) and this trait's existing "core keeps authority,
policy advises" contract:

- select_wake_cpu(task, waker, home, idle_mask) -> CpuId  (select_task_rq)
- wakeup_preempt(cpu, woken, current) -> bool             (wakeup_preempt)
- task_woken(cpu, task)                                   (task_woken)
- task_tick(cpu, current) -> bool                         (task_tick)
- update_curr(cpu, current, delta_ns)                     (update_curr)
- yield_task(cpu, current)                                (yield_task)

All have behavior-preserving defaults (select_wake_cpu keeps the task on
its home CPU; wakeup_preempt/task_tick return false; the rest no-op), so
this commit is purely additive with no runtime change — it establishes
the interface. Wiring the wake path through select_wake_cpu/wakeup_preempt
/task_woken (idle-first placement + a reliable enqueue+IPI handshake) and
implementing them in FifoScheduler land in follow-up commits, each
validated on its own. Kernel-test 7310 pass / 0 fail; clippy x86_64 +
aarch64 and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the "run the woken task promptly" slice of Linux's
try_to_wake_up. A wake was a bare flag=true on the task's home CPU, so a
woken task waited at that CPU's ready-queue tail behind every other slot
(the scheduling hop that dominates multi-task wakeup latency). Now a wake
also publishes the task id into a per-CPU RUN_NEXT hint, and
pick_next_slot picks that task ahead of the FIFO backlog.

Correctness guards (a naive hint starves the queue):
- WakeCell gains a task id; the hint is bounded to best-tier AND
  best-priority slots, so it reorders a woken task ahead of its EQUALS
  only — never inverting priority or bypassing throttling;
- LAST_PICKED rejects a hint that names the just-picked task, so a
  cooperatively-yielding task's SELF-wake (the yield heartbeat) goes to
  the tail like any yield instead of re-boosting itself forever;
- a one-pick cooldown caps the boost to at most every other pick, so a
  ping-pong pair waking each other cannot monopolize the CPU.
The hint only reorders already-runnable slots — it never sets the
runnable bit or touches the halt handshake, so it cannot create or mask a
lost wakeup; a stale/migrated id is simply ignored.

Measured neutral on single-worker stress-ng (pipe/msg/sem-sysv/sigq
within run-to-run noise): with one task per CPU there is no FIFO backlog
to jump, so the hop it removes is absent there — those gaps are
per-syscall-overhead-bound, not scheduling-hop-bound. Its win is
multi-task-per-CPU wakeup latency (the redis/mt-echo class). Committed as
the foundation the select_wake_cpu placement + wakeup_preempt steps build
on. Validated: kernel-test 7310 pass / 0 fail (incl. the concurrent-reader
and sleepable-timeout tests that a first, unguarded version starved);
clippy x86_64 + aarch64 and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant