Skip to main content

wowlab_tidy/languages/rust/rules/performance/
busy_wait.rs

1use ra_ap_syntax::{AstNode, ast};
2
3use super::support;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8    Example {
9        label: "try_recv spin loop without sleep",
10        code: "fn f(rx: std::sync::mpsc::Receiver<u8>) { loop { if let Ok(v) = rx.try_recv() { drop(v); } } }",
11        pass: false,
12    },
13    Example {
14        label: "while spinning on atomic load condition",
15        code: "fn f(flag: &std::sync::atomic::AtomicBool) { while flag.load(std::sync::atomic::Ordering::Acquire) {} }",
16        pass: false,
17    },
18    Example {
19        label: "compare_exchange spin lock",
20        code: "fn f(lock: &std::sync::atomic::AtomicBool) { loop { match lock.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) { Ok(_) => break, Err(_) => {} } } }",
21        pass: false,
22    },
23    Example {
24        label: "while spinning on try_lock condition",
25        code: "fn f(m: &std::sync::Mutex<u8>) { while m.try_lock().is_err() {} }",
26        pass: false,
27    },
28    Example {
29        label: "try_recv loop with sleep",
30        code: "fn f(rx: std::sync::mpsc::Receiver<u8>) { loop { if let Ok(v) = rx.try_recv() { drop(v); } std::thread::sleep(std::time::Duration::from_millis(1)); } }",
31        pass: true,
32    },
33    Example {
34        label: "async poll loop with yield_now",
35        code: "async fn f(q: &Queue) { loop { if q.try_recv().is_none() { tokio::task::yield_now().await; } } }",
36        pass: true,
37    },
38    Example {
39        label: "try_lock loop with park",
40        code: "fn f(m: &std::sync::Mutex<u8>) { loop { if let Ok(g) = m.try_lock() { drop(g); break; } std::thread::park(); } }",
41        pass: true,
42    },
43    Example {
44        label: "blocking recv is not a spin",
45        code: "fn f(rx: std::sync::mpsc::Receiver<u8>) { loop { let Ok(v) = rx.recv() else { break }; drop(v); } }",
46        pass: true,
47    },
48    Example {
49        label: "plain computation loop",
50        code: "fn f(mut n: u32) { while n > 0 { n -= 1; } }",
51        pass: true,
52    },
53    Example {
54        label: "spin loop in test module",
55        code: "#[cfg(test)]\nmod tests {\n    fn t(rx: std::sync::mpsc::Receiver<u8>) { loop { if let Ok(v) = rx.try_recv() { drop(v); } } }\n}",
56        pass: true,
57    },
58];
59
60crate::ast_rule!(
61    busy_wait,
62    "Flag spin loops polling `try_recv`/`try_lock`/atomics without sleeping, yielding, or blocking.",
63    "Hot spinning burns CPU cycles when no work is present. Sleep, yield_now, park, or block on recv() between polls.",
64    Medium,
65);
66
67fn check_busy_wait(ctx: &AstCtx<'_>) -> Vec<Violation> {
68    let loops = ctx.nodes::<ast::LoopExpr>().filter_map(|node| {
69        if ctx.is_in_test(&node) {
70            return None;
71        }
72
73        let body = support::loop_body(&node).map(|body| SpinScan::of_node(body.syntax()))?;
74
75        (body.polls && !body.waits).then(|| ctx.violation(&node, MSG))
76    });
77    let whiles = ctx.nodes::<ast::WhileExpr>().filter_map(|node| {
78        if ctx.is_in_test(&node) {
79            return None;
80        }
81
82        let body = support::loop_body(&node).map(|body| SpinScan::of_node(body.syntax()))?;
83        let condition = support::loop_source(&node)
84            .map_or_else(SpinScan::default, |expr| SpinScan::of_node(expr.syntax()));
85        let polling = body.polls || condition.polls || condition.loads;
86
87        (polling && !(body.waits || condition.waits)).then(|| ctx.violation(&node, MSG))
88    });
89
90    loops.chain(whiles).collect()
91}
92
93const POLL_METHODS: &[&str] = &[
94    "compare_exchange",
95    "compare_exchange_weak",
96    "try_lock",
97    "try_read",
98    "try_recv",
99    "try_write",
100];
101const WAIT_FNS: &[&str] = &["park", "sleep", "yield_now"];
102
103const MSG: &str = "busy-wait loop polls without sleeping or yielding — sleep, yield_now, park, or block on recv() when idle";
104
105#[derive(Default)]
106struct SpinScan {
107    loads: bool,
108    polls: bool,
109    waits: bool,
110}
111
112impl SpinScan {
113    fn of_node(node: &ra_ap_syntax::SyntaxNode) -> Self {
114        let mut scan = Self::default();
115
116        for syntax in node.descendants() {
117            if ast::AwaitExpr::can_cast(syntax.kind()) {
118                scan.waits = true;
119            }
120
121            // #t(rust_clone_in_loop) SyntaxNode clone is reference-counted and O(1)
122            if let Some(call) = ast::CallExpr::cast(syntax.clone()) {
123                if call
124                    .expr()
125                    .and_then(|expr| support::path_expr_last_name(&expr))
126                    .is_some_and(|name| WAIT_FNS.contains(&name.as_str()))
127                {
128                    scan.waits = true;
129                }
130            }
131
132            if let Some(call) = ast::MethodCallExpr::cast(syntax) {
133                let Some(method) = support::method_name(&call) else {
134                    continue;
135                };
136
137                if POLL_METHODS.contains(&method.as_str()) {
138                    scan.polls = true;
139                }
140
141                if method == "load" {
142                    scan.loads = true;
143                }
144
145                if method == "recv" && support::has_no_args(&call) {
146                    scan.waits = true;
147                }
148            }
149        }
150
151        scan
152    }
153}
154
155crate::tidy_ast_test!(check_busy_wait, {
156    crate::example_tests!(EXAMPLES, check_busy_wait);
157});