Skip to main content

proofframe/execution/
resource.rs

1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::sync::{Arc, Mutex};
3
4use serde::{Deserialize, Serialize};
5
6use crate::ProofFrameError;
7
8const DEFAULT_MEMORY_BYTES: u64 = 512 * 1024 * 1024;
9const DEFAULT_TEMP_BYTES: u64 = 4 * 1024 * 1024 * 1024;
10const DEFAULT_OUTPUT_RECORDS: u64 = 100_000;
11const DEFAULT_SAMPLES: usize = 100;
12
13/// Hard limits shared by validation, distinct, diff and evidence operations.
14#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct ResourceLimits {
17    pub max_memory_bytes: u64,
18    pub max_temp_bytes: u64,
19    pub max_output_records: u64,
20    pub max_samples: usize,
21}
22
23impl Default for ResourceLimits {
24    fn default() -> Self {
25        Self {
26            max_memory_bytes: DEFAULT_MEMORY_BYTES,
27            max_temp_bytes: DEFAULT_TEMP_BYTES,
28            max_output_records: DEFAULT_OUTPUT_RECORDS,
29            max_samples: DEFAULT_SAMPLES,
30        }
31    }
32}
33
34/// Cooperative cancellation shared across cloned operation handles.
35#[derive(Debug, Clone, Default)]
36pub struct CancellationToken {
37    cancelled: Arc<AtomicBool>,
38}
39
40impl CancellationToken {
41    #[must_use]
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    pub fn cancel(&self) {
47        self.cancelled.store(true, Ordering::Release);
48    }
49
50    #[must_use]
51    pub fn is_cancelled(&self) -> bool {
52        self.cancelled.load(Ordering::Acquire)
53    }
54
55    pub fn check(&self) -> Result<(), ProofFrameError> {
56        if self.is_cancelled() {
57            Err(ProofFrameError::Cancelled)
58        } else {
59            Ok(())
60        }
61    }
62}
63
64#[derive(Debug)]
65struct Counters {
66    memory_cap: u64,
67    temp_cap: u64,
68    memory: AtomicU64,
69    peak_memory: AtomicU64,
70    temp: AtomicU64,
71    peak_temp: AtomicU64,
72    reservation_lock: Mutex<()>,
73}
74
75impl Counters {
76    fn new(memory_cap: u64, temp_cap: u64) -> Self {
77        Self {
78            memory_cap,
79            temp_cap,
80            memory: AtomicU64::new(0),
81            peak_memory: AtomicU64::new(0),
82            temp: AtomicU64::new(0),
83            peak_temp: AtomicU64::new(0),
84            reservation_lock: Mutex::new(()),
85        }
86    }
87}
88
89/// A node in a resource hierarchy. Reservations charge this node and every ancestor.
90#[derive(Debug, Clone)]
91pub struct ResourceAccount {
92    nodes: Arc<[Arc<Counters>]>,
93    limits: ResourceLimits,
94}
95
96impl ResourceAccount {
97    #[must_use]
98    pub fn root(limits: ResourceLimits) -> Self {
99        Self {
100            nodes: Arc::from([Arc::new(Counters::new(
101                limits.max_memory_bytes,
102                limits.max_temp_bytes,
103            ))]),
104            limits,
105        }
106    }
107
108    #[must_use]
109    pub fn child(&self, memory_cap: u64, temp_cap: u64) -> Self {
110        let mut nodes = Vec::with_capacity(self.nodes.len() + 1);
111        nodes.extend(self.nodes.iter().cloned());
112        nodes.push(Arc::new(Counters::new(memory_cap, temp_cap)));
113        Self {
114            nodes: nodes.into(),
115            limits: self.limits,
116        }
117    }
118
119    pub fn try_reserve_memory(&self, bytes: u64) -> Result<MemoryReservation, ProofFrameError> {
120        Reservation::try_new(self.nodes.clone(), bytes, ResourceKind::Memory).map(|reservation| {
121            MemoryReservation {
122                _reservation: reservation,
123            }
124        })
125    }
126
127    pub fn try_reserve_temp(&self, bytes: u64) -> Result<TempReservation, ProofFrameError> {
128        Reservation::try_new(self.nodes.clone(), bytes, ResourceKind::Temp).map(|reservation| {
129            TempReservation {
130                _reservation: reservation,
131            }
132        })
133    }
134
135    #[must_use]
136    pub fn memory_used(&self) -> u64 {
137        self.current().memory.load(Ordering::Acquire)
138    }
139
140    #[must_use]
141    pub fn peak_memory_used(&self) -> u64 {
142        self.current().peak_memory.load(Ordering::Acquire)
143    }
144
145    #[must_use]
146    pub fn temp_used(&self) -> u64 {
147        self.current().temp.load(Ordering::Acquire)
148    }
149
150    #[must_use]
151    pub fn peak_temp_used(&self) -> u64 {
152        self.current().peak_temp.load(Ordering::Acquire)
153    }
154
155    #[must_use]
156    pub const fn limits(&self) -> ResourceLimits {
157        self.limits
158    }
159
160    fn current(&self) -> &Counters {
161        self.nodes.last().expect("a resource account has a root")
162    }
163}
164
165#[derive(Debug, Clone, Copy)]
166enum ResourceKind {
167    Memory,
168    Temp,
169}
170
171impl ResourceKind {
172    const fn name(self) -> &'static str {
173        match self {
174            Self::Memory => "memory",
175            Self::Temp => "temporary storage",
176        }
177    }
178
179    fn used_and_limit(self, counters: &Counters) -> (&AtomicU64, u64) {
180        match self {
181            Self::Memory => (&counters.memory, counters.memory_cap),
182            Self::Temp => (&counters.temp, counters.temp_cap),
183        }
184    }
185
186    fn peak(self, counters: &Counters) -> &AtomicU64 {
187        match self {
188            Self::Memory => &counters.peak_memory,
189            Self::Temp => &counters.peak_temp,
190        }
191    }
192}
193
194#[derive(Debug)]
195struct Reservation {
196    nodes: Arc<[Arc<Counters>]>,
197    bytes: u64,
198    kind: ResourceKind,
199}
200
201impl Reservation {
202    fn try_new(
203        nodes: Arc<[Arc<Counters>]>,
204        bytes: u64,
205        kind: ResourceKind,
206    ) -> Result<Self, ProofFrameError> {
207        let transaction_root = Arc::clone(&nodes[0]);
208        let _transaction = transaction_root
209            .reservation_lock
210            .lock()
211            .unwrap_or_else(|poisoned| poisoned.into_inner());
212        let mut committed_usage = Vec::with_capacity(nodes.len());
213        for (charged, counters) in nodes.iter().enumerate() {
214            let (used, cap) = kind.used_and_limit(counters);
215            let usage = match try_charge(used, cap, bytes) {
216                Ok(usage) => usage,
217                Err((current, limit)) => {
218                    for rollback in &nodes[..charged] {
219                        let (used, _) = kind.used_and_limit(rollback);
220                        used.fetch_sub(bytes, Ordering::AcqRel);
221                    }
222                    return Err(ProofFrameError::ResourceLimit {
223                        resource: kind.name(),
224                        requested: bytes,
225                        used: current,
226                        limit,
227                    });
228                }
229            };
230            committed_usage.push(usage);
231        }
232        for (counters, usage) in nodes.iter().zip(committed_usage) {
233            kind.peak(counters).fetch_max(usage, Ordering::AcqRel);
234        }
235        Ok(Self { nodes, bytes, kind })
236    }
237}
238
239impl Drop for Reservation {
240    fn drop(&mut self) {
241        for counters in self.nodes.iter() {
242            let (used, _) = self.kind.used_and_limit(counters);
243            used.fetch_sub(self.bytes, Ordering::AcqRel);
244        }
245    }
246}
247
248/// RAII charge for engine-owned memory.
249#[derive(Debug)]
250pub struct MemoryReservation {
251    _reservation: Reservation,
252}
253
254/// RAII charge for operation-owned temporary storage.
255#[derive(Debug)]
256pub struct TempReservation {
257    _reservation: Reservation,
258}
259
260fn try_charge(used: &AtomicU64, limit: u64, bytes: u64) -> Result<u64, (u64, u64)> {
261    let mut current = used.load(Ordering::Acquire);
262    loop {
263        let Some(next) = current.checked_add(bytes) else {
264            return Err((current, limit));
265        };
266        if next > limit {
267            return Err((current, limit));
268        }
269        match used.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire) {
270            Ok(_) => {
271                return Ok(next);
272            }
273            Err(observed) => current = observed,
274        }
275    }
276}