Skip to content

Commit 2d7f8ce

Browse files
committed
Cut the size of a boxed Context to 930 bytes + 6 allocs and a boxed
ThreadSafeContext to 1097 bytes + 6 allocs, and profiling actual usage and reducing initial allocation/capacity for data structures
1 parent 1432f4c commit 2d7f8ce

File tree

4 files changed

+142
-15
lines changed

4 files changed

+142
-15
lines changed

imageflow_abi/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! As of ABI 3.1, calls are thread-safe.
99
//!
1010
//! Don't worry about the performance of creating/destroying contexts.
11-
//! A context weighs less than 2kb: (384 + 1400) as of 2017-8-29.
11+
//! A context weighs only 1100 bytes (6 allocations) as of Oct 2025.
1212
//!
1313
//! # Thread Safety
1414
//!
@@ -949,7 +949,7 @@ pub unsafe extern "C" fn imageflow_context_send_json(
949949
json_buffer: *const u8,
950950
json_buffer_size: libc::size_t,
951951
) -> *const JsonResponse {
952-
let mut c = context!(context);
952+
let c = context!(context);
953953
if c.outward_error().has_error() {
954954
let json_error = c.outward_error().get_json_response_for_error();
955955
if let Some(json_error) = json_error {

imageflow_core/src/context.rs

Lines changed: 127 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use imageflow_types::ImageInfo;
2121
use itertools::Itertools;
2222

2323
/// Something of a god object (which is necessary for a reasonable FFI interface).
24+
/// 1025 bytes including 5 heap allocations as of Oct 2025. If on the stack, 312 bytes are taken up
2425
pub struct Context {
2526
pub debug_job_id: i32,
2627
pub next_stable_node_id: i32,
@@ -88,7 +89,7 @@ impl ThreadSafeContext {
8889
}))
8990
}
9091
pub fn create_cant_panic() -> Result<Box<ThreadSafeContext>> {
91-
std::panic::catch_unwind(|| ThreadSafeContext::create_can_panic())
92+
std::panic::catch_unwind(ThreadSafeContext::create_can_panic)
9293
.unwrap_or_else(|_| Err(err_oom!())) //err_oom because it doesn't allocate anything.
9394
}
9495

@@ -132,6 +133,25 @@ impl ThreadSafeContext {
132133
}
133134
true
134135
}
136+
137+
/// Calculates the total size and count of all heap allocations in a new ThreadSafeContext
138+
/// Returns (total_bytes, num_allocations)
139+
///
140+
/// This includes:
141+
/// - Initial heap allocations for collections (codecs, io_id_list, bitmaps, allocations)
142+
/// - Arc allocations for shared state
143+
///
144+
/// Note: RwLock and Mutex store their contents inline, not on the heap
145+
pub(crate) fn calculate_heap_allocations() -> (usize, usize) {
146+
// Get Context's heap allocations (this is shared via RwLock but stored inline in ThreadSafeContext)
147+
let (context_bytes, context_allocs) = Context::calculate_heap_allocations();
148+
149+
(
150+
context_bytes + std::mem::size_of::<ThreadSafeContext>()
151+
- std::mem::size_of::<Context>(),
152+
context_allocs,
153+
)
154+
}
135155
/// mem_calloc should not panic
136156
pub unsafe fn mem_calloc(
137157
&self,
@@ -206,11 +226,13 @@ impl Context {
206226
next_stable_node_id: 0,
207227
max_calc_flatten_execute_passes: 40,
208228
graph_recording: s::Build001GraphRecording::off(),
209-
codecs: AddRemoveSet::with_capacity(4),
210-
io_id_list: RefCell::new(Vec::with_capacity(2)),
229+
codecs: AddRemoveSet::with_capacity(Self::default_codecs_capacity()),
230+
io_id_list: RefCell::new(Vec::with_capacity(Self::default_codecs_capacity())),
211231
cancellation_token: CancellationToken::new(),
212232
enabled_codecs: EnabledCodecs::default(),
213-
bitmaps: RefCell::new(crate::graphics::bitmaps::BitmapsContainer::with_capacity(16)),
233+
bitmaps: RefCell::new(
234+
crate::graphics::bitmaps::BitmapsContainer::with_default_capacity(),
235+
),
214236
security: imageflow_types::ExecutionSecurity {
215237
max_decode_size: None,
216238
max_frame_size: Some(imageflow_types::FrameSizeLimit {
@@ -223,18 +245,23 @@ impl Context {
223245
allocations: RefCell::new(AllocationContainer::new()),
224246
}))
225247
}
248+
fn default_codecs_capacity() -> usize {
249+
2
250+
}
226251
pub(crate) fn create_can_panic_unboxed() -> Result<Context> {
227252
Ok(Context {
228253
debug_job_id: unsafe { JOB_ID },
229254
next_graph_version: 0,
230255
next_stable_node_id: 0,
231256
max_calc_flatten_execute_passes: 40,
232257
graph_recording: s::Build001GraphRecording::off(),
233-
codecs: AddRemoveSet::with_capacity(4),
234-
io_id_list: RefCell::new(Vec::with_capacity(2)),
258+
codecs: AddRemoveSet::with_capacity(Self::default_codecs_capacity()),
259+
io_id_list: RefCell::new(Vec::with_capacity(Self::default_codecs_capacity())),
235260
cancellation_token: CancellationToken::new(),
236261
enabled_codecs: EnabledCodecs::default(),
237-
bitmaps: RefCell::new(crate::graphics::bitmaps::BitmapsContainer::with_capacity(16)),
262+
bitmaps: RefCell::new(
263+
crate::graphics::bitmaps::BitmapsContainer::with_default_capacity(),
264+
),
238265
security: imageflow_types::ExecutionSecurity {
239266
max_decode_size: None,
240267
max_frame_size: Some(imageflow_types::FrameSizeLimit {
@@ -256,11 +283,13 @@ impl Context {
256283
next_stable_node_id: 0,
257284
max_calc_flatten_execute_passes: 40,
258285
graph_recording: s::Build001GraphRecording::off(),
259-
codecs: AddRemoveSet::with_capacity(4),
260-
io_id_list: RefCell::new(Vec::with_capacity(2)),
286+
codecs: AddRemoveSet::with_capacity(Self::default_codecs_capacity()),
287+
io_id_list: RefCell::new(Vec::with_capacity(Self::default_codecs_capacity())),
261288
cancellation_token,
262289
enabled_codecs: EnabledCodecs::default(),
263-
bitmaps: RefCell::new(crate::graphics::bitmaps::BitmapsContainer::with_capacity(16)),
290+
bitmaps: RefCell::new(
291+
crate::graphics::bitmaps::BitmapsContainer::with_default_capacity(),
292+
),
264293
security: imageflow_types::ExecutionSecurity {
265294
max_decode_size: None,
266295
max_frame_size: Some(imageflow_types::FrameSizeLimit {
@@ -592,6 +621,55 @@ impl Context {
592621
.to_owned(),
593622
})
594623
}
624+
625+
/// Calculates the total size and count of all stack andheap allocations in a new Context
626+
/// Returns (total_bytes, num_allocations)
627+
///
628+
/// This includes:
629+
/// - Initial capacity allocations for collections (codecs, io_id_list, bitmaps, allocations)
630+
/// - Arc allocation for shared state (cancellation_token)
631+
///
632+
/// Note: RefCell stores its contents inline, not on the heap
633+
pub(crate) fn calculate_heap_allocations() -> (usize, usize) {
634+
let mut total_bytes = 0;
635+
let mut num_allocations = 0;
636+
637+
total_bytes += std::mem::size_of::<Context>();
638+
// AddRemoveSet<CodecInstanceContainer> with capacity 4
639+
// This is typically backed by a Vec, so 1 allocation for the buffer
640+
if std::mem::size_of::<CodecInstanceContainer>() * Self::default_codecs_capacity() > 0 {
641+
total_bytes +=
642+
std::mem::size_of::<CodecInstanceContainer>() * Self::default_codecs_capacity();
643+
num_allocations += 1;
644+
}
645+
646+
// Vec<i32> with capacity 2 (inside RefCell, but RefCell is inline)
647+
if std::mem::size_of::<i32>() * Self::default_codecs_capacity() > 0 {
648+
total_bytes += std::mem::size_of::<i32>() * Self::default_codecs_capacity();
649+
num_allocations += 1;
650+
}
651+
652+
// DenseSlotMap in BitmapsContainer with capacity 16
653+
// DenseSlotMap typically uses 2 Vec allocations (one for slots, one for keys)
654+
let slot_size = std::mem::size_of::<RefCell<crate::graphics::bitmaps::Bitmap>>();
655+
let key_size = std::mem::size_of::<crate::graphics::bitmaps::BitmapKey>();
656+
if slot_size * crate::graphics::bitmaps::BitmapsContainer::default_capacity() > 0 {
657+
total_bytes +=
658+
slot_size * crate::graphics::bitmaps::BitmapsContainer::default_capacity();
659+
num_allocations += 1;
660+
}
661+
if key_size * crate::graphics::bitmaps::BitmapsContainer::default_capacity() > 0 {
662+
total_bytes +=
663+
key_size * crate::graphics::bitmaps::BitmapsContainer::default_capacity();
664+
num_allocations += 1;
665+
}
666+
667+
// Arc<AtomicBool> for cancellation_token - 1 heap allocation
668+
total_bytes += std::mem::size_of::<AtomicBool>();
669+
num_allocations += 1;
670+
671+
(total_bytes, num_allocations)
672+
}
595673
}
596674

597675
#[cfg(test)]
@@ -615,6 +693,43 @@ impl Drop for Context {
615693

616694
#[test]
617695
fn test_context_size() {
618-
println!("std::mem::sizeof(Context) = {}", std::mem::size_of::<Context>());
619-
assert!(std::mem::size_of::<Context>() < 500);
696+
eprintln!("std::mem::sizeof(Context) = {}", std::mem::size_of::<Context>());
697+
assert!(std::mem::size_of::<Context>() < 320);
698+
}
699+
700+
#[test]
701+
fn test_thread_safe_context_size() {
702+
println!("std::mem::sizeof(ThreadSafeContext) = {}", std::mem::size_of::<ThreadSafeContext>());
703+
eprintln!("std::mem::sizeof(ThreadSafeContext) = {}", std::mem::size_of::<ThreadSafeContext>());
704+
assert!(std::mem::size_of::<ThreadSafeContext>() <= 488);
705+
}
706+
707+
#[test]
708+
fn test_calculate_context_heap_size() {
709+
let (context_bytes, context_allocs) = Context::calculate_heap_allocations();
710+
let (thread_safe_bytes, thread_safe_allocs) = ThreadSafeContext::calculate_heap_allocations();
711+
712+
eprintln!(
713+
"Context::calculate_heap_allocations() = ({} bytes, {} allocations)",
714+
context_bytes, context_allocs
715+
);
716+
eprintln!(
717+
"ThreadSafeContext::calculate_heap_allocations() = ({} bytes, {} allocations)",
718+
thread_safe_bytes, thread_safe_allocs
719+
);
720+
721+
// ThreadSafeContext and Context share the same heap allocations (Context is inside RwLock)
722+
assert!(thread_safe_bytes > context_bytes);
723+
assert!(thread_safe_allocs >= context_allocs);
724+
725+
// Sanity check: should have some allocations
726+
assert!(context_allocs > 0);
727+
assert!(context_bytes > 0);
728+
729+
// Fail if this grows so we can notice it
730+
assert!(context_allocs <= 6);
731+
assert!(context_bytes <= 930);
732+
733+
assert!(context_allocs <= 6);
734+
assert!(thread_safe_bytes <= 1097);
620735
}

imageflow_core/src/graphics/bitmaps.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ pub struct BitmapsContainer {
2222
map: ::slotmap::DenseSlotMap<BitmapKey, RefCell<Bitmap>>,
2323
}
2424

25+
// impl Drop for BitmapsContainer {
26+
// fn drop(&mut self) {
27+
// eprintln!("Dropping BitmapsContainer with {} bitmaps", self.map.len());
28+
// }
29+
// }
30+
2531
impl BitmapsContainer {
2632
pub fn with_capacity(capacity: usize) -> BitmapsContainer {
2733
BitmapsContainer {
@@ -30,6 +36,12 @@ impl BitmapsContainer {
3036
),
3137
}
3238
}
39+
pub fn with_default_capacity() -> BitmapsContainer {
40+
Self::with_capacity(Self::default_capacity())
41+
}
42+
pub fn default_capacity() -> usize {
43+
3
44+
}
3345
pub fn get(&self, key: BitmapKey) -> Option<&RefCell<Bitmap>> {
3446
self.map.get(key)
3547
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
25c79175077e3250d368939492d2954c
1+
94d84502e2b43c3605ef1f55fb2e9d39

0 commit comments

Comments
 (0)