Skip to main content

axiolid_overlay/
budget.rs

1//! A memory budget for the straight-edge boolean (#338).
2//!
3//! [`crate::overlay_within`] and [`crate::union_soup_within`] read the
4//! caller's [`axiolid_contracts::ExecutionOptions::memory_budget_bytes`].
5//! The subdivision charges what it is about to hold before it allocates
6//! it, and refuses with [`OverlayRefusal::BudgetExceeded`] as soon as the
7//! charges pass the budget:
8//!
9//! - the operands' rings, by vertex, before they are snapped and reduced;
10//! - the edges, before their boxes, trees and winding parts are built;
11//! - every point a pair of edges meets at, before it is recorded (the
12//!   pairs come from the box tree, so this stops a soup whose edges cross
13//!   far more often than its output has corners before those crossings
14//!   are held);
15//! - the pieces, counted from the recorded points, before any is built;
16//! - each piece's rings and carriers, as they are found.
17//!
18//! The charges are upper bounds on the heap each holds, measured with a
19//! counting allocator by `tests/overlay_budget.rs` (which also checks that
20//! a call never holds more than its budget, refused or not), so a call
21//! admitted holds at most its budget in the subdivision, and a refused
22//! call stops before it. Without a budget nothing is refused: the charges
23//! are counted and never compared. The rounding and settling of the
24//! output hold about the output, which no budget refuses.
25
26use crate::OverlayError;
27
28/// Bytes charged per input vertex: the copies the snap and the chain
29/// reduction take, and the rings handed to the subdivision.
30pub(crate) const PER_VERTEX: usize = 320;
31/// Bytes charged per edge of the subdivision: the edge, its boxes in the
32/// edge tree, its winding part and tree, and its first two stops.
33pub(crate) const PER_EDGE: usize = 2_000;
34/// Bytes charged per point recorded on an edge: the meeting, with room
35/// for its vector to double.
36pub(crate) const PER_MEETING: usize = 400;
37/// Bytes charged per crossing computed once for repeated segments.
38pub(crate) const PER_SHARED_CROSSING: usize = 200;
39/// Bytes charged per piece: the piece, its raw edge, its stop, its
40/// interned vertex and its share of the linking.
41pub(crate) const PER_PIECE: usize = 600;
42/// Bytes charged per ring containing or carrying a piece.
43pub(crate) const PER_MEMBER: usize = 40;
44
45/// Why a budgeted overlay gave no result.
46#[derive(Debug, Clone, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum OverlayRefusal {
49    /// An operand failed the overlay's checks, as [`crate::overlay_with`]
50    /// and [`crate::union_soup_with`] refuse it.
51    Invalid(OverlayError),
52    /// The subdivision would hold more than the memory budget: refused
53    /// before allocating past it. `needed` is what had been charged when
54    /// the budget was passed, a lower bound on what the operation needs;
55    /// `budget` is the caller's.
56    BudgetExceeded {
57        /// Bytes charged when the budget was passed.
58        needed: usize,
59        /// The caller's memory budget in bytes.
60        budget: usize,
61    },
62}
63
64impl From<OverlayError> for OverlayRefusal {
65    fn from(error: OverlayError) -> Self {
66        Self::Invalid(error)
67    }
68}
69
70impl std::fmt::Display for OverlayRefusal {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            Self::Invalid(error) => write!(f, "invalid overlay operand: {error}"),
74            Self::BudgetExceeded { needed, budget } => write!(
75                f,
76                "overlay exceeded its memory budget: {needed} bytes charged, {budget} allowed"
77            ),
78        }
79    }
80}
81
82impl std::error::Error for OverlayRefusal {
83    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
84        match self {
85            Self::Invalid(error) => Some(error),
86            Self::BudgetExceeded { .. } => None,
87        }
88    }
89}
90
91/// The running charge against an optional budget.
92#[derive(Debug, Clone, Copy)]
93pub(crate) struct Charge {
94    budget: Option<usize>,
95    used: usize,
96}
97
98impl Charge {
99    /// No budget: every charge is admitted.
100    pub(crate) const fn unlimited() -> Self {
101        Self {
102            budget: None,
103            used: 0,
104        }
105    }
106
107    /// A charge against `budget` bytes, if any.
108    pub(crate) const fn within(budget: Option<usize>) -> Self {
109        Self { budget, used: 0 }
110    }
111
112    /// Charge `count` items of `each` bytes before allocating them.
113    pub(crate) fn add(&mut self, count: usize, each: usize) -> Result<(), OverlayRefusal> {
114        self.used = self.used.saturating_add(count.saturating_mul(each));
115        match self.budget {
116            Some(budget) if self.used > budget => Err(OverlayRefusal::BudgetExceeded {
117                needed: self.used,
118                budget,
119            }),
120            _ => Ok(()),
121        }
122    }
123}
124
125/// The error of an unbudgeted call: an unlimited charge never refuses.
126pub(crate) fn unbudgeted(refusal: OverlayRefusal) -> OverlayError {
127    match refusal {
128        OverlayRefusal::Invalid(error) => error,
129        OverlayRefusal::BudgetExceeded { .. } => {
130            unreachable!("an unlimited charge admits every charge")
131        }
132    }
133}