@@ -21,6 +21,7 @@ use imageflow_types::ImageInfo;
2121use 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
2425pub 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]
617695fn 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}
0 commit comments