seq_runtime/scheduler/yield_ops.rs
1//! Cooperative yield: `yield_strand` (explicit) and `maybe_yield` (safety valve).
2//!
3//! ## Cooperative Yield Safety Valve
4//!
5//! Prevents tight TCO loops from starving other strands and making the process
6//! unresponsive. When enabled via `SEQ_YIELD_INTERVAL`, yields after N tail calls.
7//!
8//! Configuration:
9//! `SEQ_YIELD_INTERVAL=10000` - Yield every 10,000 tail calls (default: 0 = disabled)
10//!
11//! Scope:
12//! - Covers: User-defined word tail calls (musttail) and quotation tail calls
13//! - Does NOT cover: Closure calls (they use regular calls, bounded by stack)
14//! - Does NOT cover: Non-tail recursive calls (bounded by stack)
15//!
16//! This is intentional: the safety valve targets unbounded TCO loops.
17//!
18//! Design:
19//! - Zero overhead when disabled (threshold=0 short-circuits immediately)
20//! - Thread-local counter avoids synchronization overhead
21//! - Called before every musttail in generated code
22//! - Threshold is cached on first access via OnceLock
23//!
24//! Thread-Local Counter Behavior:
25//! The counter is per-OS-thread, not per-coroutine. Multiple coroutines on the
26//! same OS thread share the counter, which may cause yields slightly more
27//! frequently than the configured interval. This is intentional:
28//! - Avoids coroutine-local storage overhead
29//! - Still achieves the goal of preventing starvation
30//! - Actual yield frequency is still bounded by the threshold
31
32use crate::stack::Stack;
33use may::coroutine;
34use std::cell::Cell;
35use std::sync::OnceLock;
36
37/// Cached yield interval threshold (0 = disabled)
38static YIELD_THRESHOLD: OnceLock<u64> = OnceLock::new();
39
40thread_local! {
41 /// Per-thread tail call counter
42 pub(super) static TAIL_CALL_COUNTER: Cell<u64> = const { Cell::new(0) };
43}
44
45/// Get the yield threshold from environment (cached)
46///
47/// Returns 0 (disabled) if SEQ_YIELD_INTERVAL is not set or invalid.
48/// Prints a warning to stderr if the value is set but invalid.
49fn get_yield_threshold() -> u64 {
50 *YIELD_THRESHOLD.get_or_init(|| {
51 match std::env::var("SEQ_YIELD_INTERVAL") {
52 Ok(s) if s.is_empty() => 0,
53 Ok(s) => match s.parse::<u64>() {
54 Ok(n) => n,
55 Err(_) => {
56 eprintln!(
57 "Warning: SEQ_YIELD_INTERVAL='{}' is not a valid positive integer, yield safety valve disabled",
58 s
59 );
60 0
61 }
62 },
63 Err(_) => 0,
64 }
65 })
66}
67
68/// Yield execution to allow other coroutines to run
69///
70/// # Safety
71/// Always safe to call from within a May coroutine.
72#[unsafe(no_mangle)]
73pub unsafe extern "C" fn patch_seq_yield_strand(stack: Stack) -> Stack {
74 coroutine::yield_now();
75 stack
76}
77
78/// Maybe yield to other coroutines based on tail call count
79///
80/// Called before every tail call in generated code. When SEQ_YIELD_INTERVAL
81/// is set, yields after that many tail calls to prevent starvation.
82///
83/// # Performance
84/// - Disabled (default): Single branch on cached threshold (< 1ns)
85/// - Enabled: Increment + compare + occasional yield (~10-20ns average)
86///
87/// # Safety
88/// Always safe to call. No-op when not in a May coroutine context.
89#[unsafe(no_mangle)]
90pub extern "C" fn patch_seq_maybe_yield() {
91 let threshold = get_yield_threshold();
92
93 // Fast path: disabled
94 if threshold == 0 {
95 return;
96 }
97
98 TAIL_CALL_COUNTER.with(|counter| {
99 let count = counter.get().wrapping_add(1);
100 counter.set(count);
101
102 if count >= threshold {
103 counter.set(0);
104 coroutine::yield_now();
105 }
106 });
107}