1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! A process-wide memory budget.
//!
//! The `ish` binary installs [`CountingAlloc`] as its global allocator. It
//! wraps the system allocator and keeps a count of live heap bytes, so the
//! evaluator can ask, cheaply, whether the program has outgrown its budget
//! ([`check`]) and answer `E_BUDGET_EXCEEDED` instead of the process being
//! killed by a failed allocation.
//!
//! A library user (or a test binary) that does not install the allocator sees
//! a count of zero, so [`check`] never fires there.
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
/// Live heap bytes, as counted by [`CountingAlloc`].
static LIVE: AtomicUsize = AtomicUsize::new(0);
/// The budget in bytes. `usize::MAX` means no limit has been set.
static LIMIT: AtomicUsize = AtomicUsize::new(usize::MAX);
/// The system allocator, counting live bytes.
pub struct CountingAlloc;
// SAFETY: every call is forwarded unchanged to `System`; the counter only
// observes sizes and never touches the memory.
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = System.alloc(layout);
if !p.is_null() {
LIVE.fetch_add(layout.size(), Ordering::Relaxed);
}
p
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let p = System.alloc_zeroed(layout);
if !p.is_null() {
LIVE.fetch_add(layout.size(), Ordering::Relaxed);
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout);
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let p = System.realloc(ptr, layout, new_size);
if !p.is_null() {
if new_size >= layout.size() {
LIVE.fetch_add(new_size - layout.size(), Ordering::Relaxed);
} else {
LIVE.fetch_sub(layout.size() - new_size, Ordering::Relaxed);
}
}
p
}
}
/// Live heap bytes (zero when [`CountingAlloc`] is not installed).
pub fn live_bytes() -> usize {
LIVE.load(Ordering::Relaxed)
}
/// Set the budget, in megabytes. 0 removes the limit.
pub fn set_limit_mb(mb: usize) {
let bytes = if mb == 0 {
usize::MAX
} else {
mb.saturating_mul(1024 * 1024)
};
LIMIT.store(bytes, Ordering::Relaxed);
}
/// The budget in megabytes, or `None` when there is no limit.
pub fn limit_mb() -> Option<usize> {
match LIMIT.load(Ordering::Relaxed) {
usize::MAX => None,
bytes => Some(bytes / (1024 * 1024)),
}
}
/// The default budget in megabytes: the smaller of 2048 and half the physical
/// memory (2048 when that cannot be read).
pub fn default_limit_mb() -> usize {
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let total_mb = (sys.total_memory() / (1024 * 1024)) as usize;
if total_mb == 0 {
2048
} else {
(total_mb / 2).clamp(64, 2048)
}
}
/// Set the budget from `IRONSHELL_MEMORY_LIMIT_MB` if it is a number (0 means
/// no limit), else to the default. Returns the megabytes set, 0 for none.
pub fn init_from_env() -> usize {
let mb = std::env::var("IRONSHELL_MEMORY_LIMIT_MB")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or_else(default_limit_mb);
set_limit_mb(mb);
mb
}
/// Fail with `E_BUDGET_EXCEEDED` when live bytes are over the budget.
///
/// Two relaxed atomic loads and a compare; called at every evaluator step.
#[inline]
pub fn check() -> anyhow::Result<()> {
let limit = LIMIT.load(Ordering::Relaxed);
if LIVE.load(Ordering::Relaxed) > limit {
return Err(over(limit));
}
Ok(())
}
#[cold]
fn over(limit: usize) -> anyhow::Error {
crate::safety::budget_exceeded(
"eval",
format!("memory budget of {} MB exceeded", limit / (1024 * 1024)),
"work on the data in smaller pieces, or raise the budget with --memory-limit MB or IRONSHELL_MEMORY_LIMIT_MB",
)
}