Skip to main content

objects/util/
budget.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shared resource caps for scratch-budgeted LCS and resumable blame slices.
3
4use std::fmt;
5
6/// Kind of bounded resource consumed by a line-diff or blame slice.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ResourceKind {
9    ScratchBytes,
10    Lines,
11    Work,
12    States,
13    DecodedBytes,
14}
15
16impl ResourceKind {
17    pub fn as_str(self) -> &'static str {
18        match self {
19            Self::ScratchBytes => "scratch_bytes",
20            Self::Lines => "lines",
21            Self::Work => "work",
22            Self::States => "states",
23            Self::DecodedBytes => "decoded_bytes",
24        }
25    }
26}
27
28impl fmt::Display for ResourceKind {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.write_str(self.as_str())
31    }
32}
33
34/// Terminal, typed budget failure. Distinct from malformed input or I/O.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct BudgetExceeded {
37    pub kind: ResourceKind,
38    pub limit: u64,
39    pub needed: u64,
40}
41
42impl fmt::Display for BudgetExceeded {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(
45            f,
46            "budget exceeded for {}: needed {}, limit {}",
47            self.kind, self.needed, self.limit
48        )
49    }
50}
51
52impl std::error::Error for BudgetExceeded {}
53
54/// Observable consumption after a bounded operation.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub struct ResourceUsage {
57    pub scratch_bytes: u64,
58    pub lines: u64,
59    pub work: u64,
60    pub states: u64,
61    pub decoded_bytes: u64,
62}
63
64impl ResourceUsage {
65    pub fn get(self, kind: ResourceKind) -> u64 {
66        match kind {
67            ResourceKind::ScratchBytes => self.scratch_bytes,
68            ResourceKind::Lines => self.lines,
69            ResourceKind::Work => self.work,
70            ResourceKind::States => self.states,
71            ResourceKind::DecodedBytes => self.decoded_bytes,
72        }
73    }
74}
75
76/// One shared cap/usage tracker. LCS and blame slices use this type, not
77/// per-call helper counters.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct ResourceBudget {
80    limits: ResourceUsage,
81    used: ResourceUsage,
82}
83
84impl ResourceBudget {
85    pub fn new(limits: ResourceUsage) -> Self {
86        Self {
87            limits,
88            used: ResourceUsage::default(),
89        }
90    }
91
92    pub fn unlimited() -> Self {
93        Self::new(ResourceUsage {
94            scratch_bytes: u64::MAX,
95            lines: u64::MAX,
96            work: u64::MAX,
97            states: u64::MAX,
98            decoded_bytes: u64::MAX,
99        })
100    }
101
102    pub fn limit(&self, kind: ResourceKind) -> u64 {
103        self.limits.get(kind)
104    }
105
106    pub fn used(&self) -> ResourceUsage {
107        self.used
108    }
109
110    pub fn require(&mut self, kind: ResourceKind, needed: u64) -> Result<(), BudgetExceeded> {
111        let limit = self.limit(kind);
112        if needed > limit {
113            return Err(BudgetExceeded {
114                kind,
115                limit,
116                needed,
117            });
118        }
119        if needed > self.used.get(kind) {
120            self.set_used(kind, needed);
121        }
122        Ok(())
123    }
124
125    pub fn consume(&mut self, kind: ResourceKind, amount: u64) -> Result<(), BudgetExceeded> {
126        let used = self.used.get(kind).saturating_add(amount);
127        self.require(kind, used)?;
128        Ok(())
129    }
130
131    pub fn record(&mut self, kind: ResourceKind, used: u64) {
132        self.set_used(kind, used);
133    }
134
135    fn set_used(&mut self, kind: ResourceKind, used: u64) {
136        match kind {
137            ResourceKind::ScratchBytes => self.used.scratch_bytes = used,
138            ResourceKind::Lines => self.used.lines = used,
139            ResourceKind::Work => self.used.work = used,
140            ResourceKind::States => self.used.states = used,
141            ResourceKind::DecodedBytes => self.used.decoded_bytes = used,
142        }
143    }
144}