mz_ore/stack.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Stack management utilities.
17
18#[cfg(feature = "tracing")]
19use std::backtrace::Backtrace;
20use std::cell::RefCell;
21use std::error::Error;
22use std::fmt;
23#[cfg(feature = "tracing")]
24use std::sync::atomic::{AtomicU64, Ordering};
25
26#[cfg(feature = "tracing")]
27use tracing::error;
28
29/// The red zone is the amount of stack space that must be available on the
30/// current stack in order for [`maybe_grow`] to call the supplied closure
31/// without allocating a new stack.
32///
33/// We use a much larger red zone in debug builds because several functions have
34/// been observed to have 32KB+ stack frames when compiled without
35/// optimizations. In particular, match statements on large enums are
36/// problematic, because *each arm* of the match statement gets its own
37/// dedicated stack space. For example, consider the following function:
38///
39/// ```ignore
40/// fn big_stack(input: SomeEnum) {
41/// match input {
42/// SomeEnum::Variant1 => {
43/// let a_local = SomeBigType::new();
44/// }
45/// SomeEnum::Variant2 => {
46/// let b_local = SomeBigType::new();
47/// }
48/// // ...
49/// SomeEnum::Variant10 => {
50/// let z_local = SomeBigType::new();
51/// }
52/// }
53/// }
54/// ```
55///
56/// In debug builds, the compiler will generate a stack frame that contains
57/// space for 10 separate copies of `SomeBigType`. This can quickly result in
58/// massive stack frames for perfectly reasonable code.
59pub const STACK_RED_ZONE: usize = {
60 #[cfg(debug_assertions)]
61 {
62 1024 << 10 // 1024KiB
63 }
64 #[cfg(not(debug_assertions))]
65 {
66 64 << 10 // 64KiB
67 }
68};
69
70/// The size of any freshly allocated stacks. It was chosen to match the default
71/// stack size for threads in Rust.
72///
73/// The default stack size is larger in debug builds to correspond to the
74/// larger [`STACK_RED_ZONE`].
75pub const STACK_SIZE: usize = {
76 #[cfg(debug_assertions)]
77 {
78 16 << 20 // 16MiB
79 }
80 #[cfg(not(debug_assertions))]
81 {
82 2 << 20 // 2 MiB
83 }
84};
85
86/// Grows the stack if necessary before invoking `f`.
87///
88/// This function is intended to be called at manually instrumented points in a
89/// program where arbitrarily deep recursion is known to happen. This function
90/// will check to see if it is within `STACK_RED_ZONE` bytes of the end of the
91/// stack, and if so it will allocate a new stack of at least `STACK_SIZE`
92/// bytes.
93///
94/// The closure `f` is guaranteed to run on a stack with at least
95/// `STACK_RED_ZONE` bytes, and it will be run on the current stack if there's
96/// space available.
97///
98/// It is generally better to use [`CheckedRecursion`] to enforce a limit on the
99/// stack growth. Not all recursive code paths support returning errors,
100/// however, in which case unconditionally growing the stack with this function
101/// is still preferable to panicking.
102#[inline(always)]
103pub fn maybe_grow<F, R>(f: F) -> R
104where
105 F: FnOnce() -> R,
106{
107 stacker::maybe_grow(STACK_RED_ZONE, STACK_SIZE, f)
108}
109
110/// A trait for types which support bounded recursion to prevent stack overflow.
111///
112/// The rather odd design of this trait allows checked recursion to be added to
113/// existing mutually recursive functions without threading an explicit `depth:
114/// &mut usize` parameter through each function. As long as there is an
115/// existing context structure, or if the mutually recursive functions are
116/// methods on a context structure, the [`RecursionGuard`] can be embedded
117/// inside this existing structure.
118///
119/// # Examples
120///
121/// Consider a simple expression evaluator:
122///
123/// ```
124/// # use std::collections::BTreeMap;
125///
126/// enum Expr {
127/// Var { name: String },
128/// Add { left: Box<Expr>, right: Box<Expr> },
129/// }
130///
131/// struct Evaluator {
132/// vars: BTreeMap<String, i64>,
133/// }
134///
135/// impl Evaluator {
136/// fn eval(&mut self, expr: &Expr) -> i64 {
137/// match expr {
138/// Expr::Var { name } => self.vars[name],
139/// Expr::Add { left, right } => self.eval(left) + self.eval(right),
140/// }
141/// }
142/// }
143/// ```
144///
145/// Calling `eval` could overflow the stack and crash with a sufficiently large
146/// `expr`. This is the situation `CheckedRecursion` is designed to solve, like
147/// so:
148///
149/// ```
150/// # use std::collections::BTreeMap;
151/// # enum Expr {
152/// # Var { name: String },
153/// # Add { left: Box<Expr>, right: Box<Expr> },
154/// # }
155/// use mz_ore::stack::{CheckedRecursion, RecursionGuard, RecursionLimitError};
156///
157/// struct Evaluator {
158/// vars: BTreeMap<String, i64>,
159/// recursion_guard: RecursionGuard,
160/// }
161///
162/// impl Evaluator {
163/// fn eval(&mut self, expr: &Expr) -> Result<i64, RecursionLimitError> {
164/// // ADDED: call to `self.checked_recur`.
165/// self.checked_recur_mut(|e| match expr {
166/// Expr::Var { name } => Ok(e.vars[name]),
167/// Expr::Add { left, right } => Ok(e.eval(left)? + e.eval(right)?),
168/// })
169/// }
170/// }
171///
172/// impl CheckedRecursion for Evaluator {
173/// fn recursion_guard(&self) -> &RecursionGuard {
174/// &self.recursion_guard
175/// }
176/// }
177/// ```
178pub trait CheckedRecursion {
179 /// Extracts a reference to the recursion guard embedded within the type.
180 fn recursion_guard(&self) -> &RecursionGuard;
181
182 /// Checks whether it is safe to recur and calls `f` if so.
183 ///
184 /// If the recursion limit for the recursion guard returned by
185 /// [`CheckedRecursion::recursion_guard`] has been reached, returns a
186 /// `RecursionLimitError`. Otherwise, it will call `f`, possibly growing the
187 /// stack if necessary.
188 ///
189 /// Calls to this function must be manually inserted at any point that
190 /// mutual recursion occurs.
191 #[inline(always)]
192 fn checked_recur<F, T, E>(&self, f: F) -> Result<T, E>
193 where
194 F: FnOnce(&Self) -> Result<T, E>,
195 E: From<RecursionLimitError>,
196 {
197 self.recursion_guard().descend()?;
198 let out = maybe_grow(|| f(self));
199 self.recursion_guard().ascend();
200 out
201 }
202
203 /// Like [`CheckedRecursion::checked_recur`], but operates on a mutable
204 /// reference to `Self`.
205 #[inline(always)]
206 fn checked_recur_mut<F, T, E>(&mut self, f: F) -> Result<T, E>
207 where
208 F: FnOnce(&mut Self) -> Result<T, E>,
209 E: From<RecursionLimitError>,
210 {
211 self.recursion_guard().descend()?;
212 let out = maybe_grow(|| f(self));
213 self.recursion_guard().ascend();
214 out
215 }
216}
217
218/// Tracks recursion depth.
219///
220/// See the [`CheckedRecursion`] trait for usage instructions.
221#[derive(Default, Debug, Clone)]
222pub struct RecursionGuard {
223 depth: RefCell<usize>,
224 limit: usize,
225}
226
227impl CheckedRecursion for RecursionGuard {
228 fn recursion_guard(&self) -> &RecursionGuard {
229 self
230 }
231}
232
233impl RecursionGuard {
234 /// Constructs a new recursion guard with the specified recursion
235 /// limit.
236 pub fn with_limit(limit: usize) -> RecursionGuard {
237 RecursionGuard {
238 depth: RefCell::new(0),
239 limit,
240 }
241 }
242
243 fn descend(&self) -> Result<(), RecursionLimitError> {
244 let mut depth = self.depth.borrow_mut();
245 if *depth < self.limit {
246 *depth += 1;
247 Ok(())
248 } else {
249 Err(RecursionLimitError::new(self.limit))
250 }
251 }
252
253 fn ascend(&self) {
254 *self.depth.borrow_mut() -= 1;
255 }
256}
257
258/// A [`RecursionGuard`]'s recursion limit was reached.
259#[derive(Debug)]
260pub struct RecursionLimitError {
261 limit: usize,
262 id: Option<u64>,
263}
264
265impl RecursionLimitError {
266 /// Emits a backtrace of the too-deep recursion to logs
267 /// and produces
268 pub fn new(limit: usize) -> Self {
269 #[cfg(feature = "tracing")]
270 let id = {
271 static COUNTER: AtomicU64 = AtomicU64::new(0);
272 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
273
274 let backtrace = Backtrace::force_capture().to_string();
275 error!(backtrace = %backtrace, "recursion limit error (backtrace id #{id})");
276
277 Some(id)
278 };
279 #[cfg(not(feature = "tracing"))]
280 let id = None;
281
282 RecursionLimitError { limit, id }
283 }
284}
285
286impl fmt::Display for RecursionLimitError {
287 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
288 let RecursionLimitError { limit, id } = self;
289 write!(f, "exceeded recursion limit of {limit}",)?;
290 if let Some(id) = id {
291 write!(f, " (backtrace id #{id} in logs)")?;
292 }
293 writeln!(f)
294 }
295}
296
297impl Error for RecursionLimitError {}