Skip to main content

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}