wowlab_tidy/languages/rust/rules/performance/
busy_wait.rs1use 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 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});