Skip to main content

ort_openrouter_cli/common/
alloc.rs

1//! ort: Open Router CLI
2//! https://github.com/grahamking/ort
3//!
4//! MIT License
5//! Copyright (c) 2025 Graham King
6//!
7//! Arena allocator. No heap allocations for entire program run.
8//! We statically allocate a large chunk of memory (`.bss` segment),
9//! and use that to fill allocation requests. This avoids doing
10//! any syscalls to get memory.
11
12#![allow(static_mut_refs)]
13
14use core::alloc::Layout;
15use core::ffi::c_void;
16use core::sync::atomic::{AtomicUsize, Ordering};
17
18use crate::syscall;
19
20#[cfg(feature = "panic-on-realloc")]
21static mut IS_FIRST_REALLOC: bool = true;
22
23#[cfg(feature = "print-allocations")]
24use crate::common::utils::to_ascii;
25
26/// All allocated memory locations will have this alignment, max_align_t
27const ALIGN: usize = 16;
28
29// How much memory to allocate total. Don't exceed this!
30//const MEM_SIZE: usize = 2 * 1024 * 1024;
31// In debug mode to print panics, need > 8MiB
32const MEM_SIZE: usize = 16 * 1024 * 1024;
33
34#[repr(align(16))]
35struct Heap(pub [u8; MEM_SIZE]);
36
37static mut HEAP: Heap = Heap([0u8; MEM_SIZE]);
38static mut OFFSET: AtomicUsize = AtomicUsize::new(0);
39
40pub struct ArenaAlloc;
41
42// In case you were wondering, yes all three methods get used. Rust does
43// a bnuch of alloc_zeroed and realloc.
44//
45// Build with feature "print-allocations" to see memory being allocated:
46// cargo build --features="print-allocations"
47// cargo build --release --features="print-allocations" -Zbuild-std="core,alloc"
48//
49// There's a Python script at the end of this file to summarize the output.
50//
51// Normal / prompt usage seems to peak under 64 Kib of active memory.
52//
53// `ort list` peaks around 180 Kib because it has one large (~128 KiB)
54// allocation which is a string holding the names of all models, so we can sort them.
55unsafe impl core::alloc::GlobalAlloc for ArenaAlloc {
56    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
57        #[cfg(feature = "print-allocations")]
58        {
59            let mut buf = [0u8; 16];
60            buf[0] = b'+';
61            let len = to_ascii(layout.size(), &mut buf[1..]);
62            crate::syscall::write(2, buf.as_ptr().cast(), len);
63
64            // Also print alignment
65            //let mut buf = [0u8; 16];
66            //buf[0] = b' ';
67            //let len = to_ascii(layout.align(), &mut buf[1..]);
68            //unsafe { crate::libc::write(2, buf.as_ptr().cast(), len) };
69        }
70
71        // Allocate in multiples of 16 bytes so that we stay aligned for next alloc.
72        let alloc_size = layout.size().next_multiple_of(ALIGN);
73
74        unsafe {
75            // Read current offset, for this allocation, and move it by alloc_size for next
76            // allocation. fetch_add returns the value before addition.
77            let current_offset = OFFSET.fetch_add(alloc_size, Ordering::Relaxed);
78
79            if OFFSET.load(Ordering::Relaxed) > MEM_SIZE {
80                let msg = c"Out of memory, common/alloc.rs MEM_SIZE\n";
81                syscall::write(2, msg.as_ptr() as *const c_void, msg.count_bytes());
82                syscall::exit(1);
83            }
84            HEAP.0.as_mut_ptr().add(current_offset)
85        }
86    }
87
88    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
89        #[cfg(feature = "print-allocations")]
90        {
91            let mut buf = [0u8; 16];
92            buf[0] = b'+';
93            let len = to_ascii(layout.size(), &mut buf[1..]);
94            crate::syscall::write(2, buf.as_ptr().cast(), len);
95
96            // Also print alignment
97            //let mut buf = [0u8; 16];
98            //buf[0] = b' ';
99            //let len = to_ascii(layout.align(), &mut buf[1..]);
100            //unsafe { crate::libc::write(2, buf.as_ptr().cast(), len) };
101        }
102
103        // .bss segment is already zeroed
104        unsafe { self.alloc(layout) }
105    }
106
107    #[allow(unused_variables)]
108    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
109        #[cfg(feature = "print-allocations")]
110        {
111            let mut buf = [0u8; 16];
112            buf[0] = b'-';
113            let len = to_ascii(layout.size(), &mut buf[1..]);
114            crate::syscall::write(2, buf.as_ptr().cast(), len);
115        }
116
117        // we never free, program runtime is short
118    }
119
120    // ort is tuned to avoid reallocations, so we don't override it.
121    // Parent implements realloc as alloc-and-copy, which is fine because it should not happen.
122    //
123    // Keep here to panic-on-realloc when code changes.
124    /*
125    #[allow(unused_variables)]
126    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
127        #[cfg(feature = "print-allocations")]
128        {
129            let mut buf = [0u8; 16];
130            buf[0] = b'\\';
131            let len = to_ascii(layout.size(), &mut buf[1..]);
132            crate::syscall::write(2, buf.as_ptr().cast(), len);
133
134            buf[0] = b'/';
135            let len = to_ascii(new_size, &mut buf[1..]);
136            crate::syscall::write(2, buf.as_ptr().cast(), len);
137
138            // Also print alignment
139            //let mut buf = [0u8; 16];
140            //buf[0] = b' ';
141            //let len = to_ascii(layout.align(), &mut buf[1..]);
142            //unsafe { crate::libc::write(2, buf.as_ptr().cast(), len) };
143        }
144
145        #[cfg(feature = "panic-on-realloc")]
146        unsafe {
147            // The panic machinery uses realloc, so we must immediately disable this or we would
148            // panic during the panic, and get no useful stack trace.
149            if IS_FIRST_REALLOC {
150                IS_FIRST_REALLOC = false;
151                panic!("realloc {} -> {}", layout.size(), new_size);
152            }
153        }
154    }
155    */
156}
157
158/*
159"""Print running totals from allocs.txt and report the maximum cumulative value."""
160
161from pathlib import Path
162
163
164def main() -> None:
165    path = Path("allocs.txt")
166    if not path.is_file():
167        raise SystemExit("allocs.txt not found in the current directory")
168
169    total = 0
170    max_total = None  # Highest cumulative total
171    max_plus = None   # Largest individual + value
172    max_minus = None  # Largest (most negative) individual - value
173
174    with path.open() as fh:
175        for raw_line in fh:
176            line = raw_line.strip()
177            if not line:
178                continue  # Skip blank lines silently
179
180            try:
181                delta = int(line)
182            except ValueError as exc:
183                raise SystemExit(f"Invalid line in allocs.txt: {line!r}") from exc
184            # realloc indicators
185            line = line.replace('/', '+').replace('\\', '-')
186
187            if delta > 0:
188                max_plus = delta if max_plus is None else max(max_plus, delta)
189            elif delta < 0:
190                max_minus = delta if max_minus is None else min(max_minus, delta)
191
192            total += delta
193            max_total = total if max_total is None else max(max_total, total)
194            print(f"{line} {total}")
195
196    print()  # Blank line before the summary, matching the example
197    print(f"Max: {max_total if max_total is not None else 0}")
198    print(f"Largest +: {max_plus if max_plus is not None else 0}")
199    print(f"Largest -: {max_minus if max_minus is not None else 0}")
200
201
202if __name__ == "__main__":
203    main()
204*/