Skip to main content

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 {}