@@ -79,6 +79,18 @@ static void object_record_free(heap_recorder*, object_record*);
7979static VALUE object_record_inspect (heap_recorder * , object_record * );
8080static object_record SKIPPED_RECORD = {0 };
8181
82+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
83+ // A pending recording is used to defer the object_id call on Ruby 4+
84+ // where calling rb_obj_id during on_newobj_event is unsafe.
85+ typedef struct {
86+ VALUE object_ref ;
87+ heap_record * heap_record ;
88+ live_object_data object_data ;
89+ } pending_recording ;
90+
91+ #define MAX_PENDING_RECORDINGS 64
92+ #endif
93+
8294struct heap_recorder {
8395 // Config
8496 // Whether the recorder should try to determine approximate sizes for tracked objects.
@@ -130,6 +142,17 @@ struct heap_recorder {
130142 // Data for a heap recording that was started but not yet ended
131143 object_record * active_recording ;
132144
145+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
146+ // Pending recordings that need to be finalized after on_newobj_event completes.
147+ // On Ruby 4+, we can't call rb_obj_id during the newobj event, so we store the
148+ // VALUE reference here and finalize it via a postponed job.
149+ pending_recording pending_recordings [MAX_PENDING_RECORDINGS ];
150+ uint pending_recordings_count ;
151+ // Temporary storage for the recording in progress, used between start and end
152+ VALUE active_deferred_object ;
153+ live_object_data active_deferred_object_data ;
154+ #endif
155+
133156 // Reusable arrays, implementing a flyweight pattern for things like iteration
134157 #define REUSABLE_LOCATIONS_SIZE MAX_FRAMES_LIMIT
135158 ddog_prof_Location * reusable_locations ;
@@ -210,6 +233,9 @@ heap_recorder* heap_recorder_new(ddog_prof_ManagedStringStorage string_storage)
210233 recorder -> size_enabled = true;
211234 recorder -> sample_rate = 1 ; // By default do no sampling on top of what allocation profiling already does
212235 recorder -> string_storage = string_storage ;
236+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
237+ recorder -> active_deferred_object = Qnil ;
238+ #endif
213239
214240 return recorder ;
215241}
@@ -294,6 +320,7 @@ void heap_recorder_after_fork(heap_recorder *heap_recorder) {
294320 heap_recorder -> stats_lifetime = (struct stats_lifetime ) {0 };
295321}
296322
323+
297324void start_heap_allocation_recording (heap_recorder * heap_recorder , VALUE new_obj , unsigned int weight , ddog_CharSlice alloc_class ) {
298325 if (heap_recorder == NULL ) {
299326 return ;
@@ -314,13 +341,29 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
314341 // directly OR because the GVL got released in the middle of an update), let's skip this sample as well.
315342 // See notes on `heap_recorder_update` for details.
316343 || heap_recorder -> updating
344+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
345+ // Skip if we've hit the pending recordings limit or if there's already a deferred object being recorded
346+ || heap_recorder -> pending_recordings_count >= MAX_PENDING_RECORDINGS
347+ || heap_recorder -> active_deferred_object != Qnil
348+ #endif
317349 ) {
318350 heap_recorder -> active_recording = & SKIPPED_RECORD ;
319351 return ;
320352 }
321353
322354 heap_recorder -> num_recordings_skipped = 0 ;
323355
356+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
357+ // On Ruby 4+, we can't call rb_obj_id during on_newobj_event as it mutates the object.
358+ // Instead, we store the VALUE reference and will get the object_id later via a postponed job.
359+ // active_deferred_object != Qnil indicates we're in deferred mode.
360+ heap_recorder -> active_deferred_object = new_obj ;
361+ heap_recorder -> active_deferred_object_data = (live_object_data ) {
362+ .weight = weight * heap_recorder -> sample_rate ,
363+ .class = intern_or_raise (heap_recorder -> string_storage , alloc_class ),
364+ .alloc_gen = rb_gc_count (),
365+ };
366+ #else
324367 VALUE ruby_obj_id = rb_obj_id (new_obj );
325368 if (!FIXNUM_P (ruby_obj_id )) {
326369 rb_raise (rb_eRuntimeError , "Detected a bignum object id. These are not supported by heap profiling." );
@@ -335,6 +378,7 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
335378 .alloc_gen = rb_gc_count (),
336379 }
337380 );
381+ #endif
338382}
339383
340384// end_heap_allocation_recording_with_rb_protect gets called while the stack_recorder is holding one of the profile
@@ -367,6 +411,24 @@ static VALUE end_heap_allocation_recording(VALUE protect_args) {
367411 heap_recorder * heap_recorder = args -> heap_recorder ;
368412 ddog_prof_Slice_Location locations = args -> locations ;
369413
414+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
415+ // On Ruby 4+, active_deferred_object != Qnil indicates deferred mode
416+ if (heap_recorder -> active_deferred_object != Qnil ) {
417+ // Store the recording in the pending list for later finalization
418+ if (heap_recorder -> pending_recordings_count < MAX_PENDING_RECORDINGS ) {
419+ heap_record * heap_record = get_or_create_heap_record (heap_recorder , locations );
420+ heap_record -> num_tracked_objects ++ ; // Pre-increment since we're going to commit this later
421+
422+ pending_recording * pending = & heap_recorder -> pending_recordings [heap_recorder -> pending_recordings_count ++ ];
423+ pending -> object_ref = heap_recorder -> active_deferred_object ;
424+ pending -> heap_record = heap_record ;
425+ pending -> object_data = heap_recorder -> active_deferred_object_data ;
426+ }
427+ heap_recorder -> active_deferred_object = Qnil ;
428+ return Qnil ;
429+ }
430+ #endif
431+
370432 object_record * active_recording = heap_recorder -> active_recording ;
371433
372434 if (active_recording == NULL ) {
@@ -399,6 +461,89 @@ void heap_recorder_update_young_objects(heap_recorder *heap_recorder) {
399461 heap_recorder_update (heap_recorder , /* full_update: */ false);
400462}
401463
464+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
465+ bool heap_recorder_has_pending_recordings (heap_recorder * heap_recorder ) {
466+ if (heap_recorder == NULL ) {
467+ return false;
468+ }
469+ return heap_recorder -> pending_recordings_count > 0 ;
470+ }
471+
472+ bool heap_recorder_finalize_pending_recordings (heap_recorder * heap_recorder ) {
473+ if (heap_recorder == NULL ) {
474+ return false;
475+ }
476+
477+ uint count = heap_recorder -> pending_recordings_count ;
478+ if (count == 0 ) {
479+ return false;
480+ }
481+
482+ for (uint i = 0 ; i < count ; i ++ ) {
483+ pending_recording * pending = & heap_recorder -> pending_recordings [i ];
484+
485+ VALUE obj = pending -> object_ref ;
486+
487+ // Check if the object is still valid (it might have been GC'd between
488+ // the allocation and this finalization)
489+ if (obj == Qnil || !RTEST (obj )) {
490+ // Object is gone, decrement the pre-incremented count and cleanup
491+ pending -> heap_record -> num_tracked_objects -- ;
492+ cleanup_heap_record_if_unused (heap_recorder , pending -> heap_record );
493+ unintern_or_raise (heap_recorder , pending -> object_data .class );
494+ continue ;
495+ }
496+
497+ // Now it's safe to get the object_id
498+ VALUE ruby_obj_id = rb_obj_id (obj );
499+ if (!FIXNUM_P (ruby_obj_id )) {
500+ // Bignum object id - not supported, skip this recording
501+ pending -> heap_record -> num_tracked_objects -- ;
502+ cleanup_heap_record_if_unused (heap_recorder , pending -> heap_record );
503+ unintern_or_raise (heap_recorder , pending -> object_data .class );
504+ continue ;
505+ }
506+
507+ long obj_id = FIX2LONG (ruby_obj_id );
508+
509+ // Create the object record now that we have the object_id
510+ object_record * record = object_record_new (obj_id , pending -> heap_record , pending -> object_data );
511+
512+ // Commit the recording
513+ int existing_error = st_update (heap_recorder -> object_records , record -> obj_id , update_object_record_entry , (st_data_t ) record );
514+ if (existing_error ) {
515+ // Duplicate object_id - this shouldn't happen normally, but handle it gracefully
516+ pending -> heap_record -> num_tracked_objects -- ;
517+ cleanup_heap_record_if_unused (heap_recorder , pending -> heap_record );
518+ object_record_free (heap_recorder , record );
519+ }
520+ }
521+
522+ heap_recorder -> pending_recordings_count = 0 ;
523+ return true;
524+ }
525+
526+ // Mark pending recordings to prevent GC from collecting the objects
527+ // while they're waiting to be finalized
528+ void heap_recorder_mark_pending_recordings (heap_recorder * heap_recorder ) {
529+ if (heap_recorder == NULL ) {
530+ return ;
531+ }
532+
533+ for (uint i = 0 ; i < heap_recorder -> pending_recordings_count ; i ++ ) {
534+ VALUE obj = heap_recorder -> pending_recordings [i ].object_ref ;
535+ if (obj != Qnil ) {
536+ rb_gc_mark (obj );
537+ }
538+ }
539+
540+ // Also mark the active deferred object if it's set
541+ if (heap_recorder -> active_deferred_object != Qnil ) {
542+ rb_gc_mark (heap_recorder -> active_deferred_object );
543+ }
544+ }
545+ #endif
546+
402547// NOTE: This function needs and assumes it gets called with the GVL being held.
403548// But importantly **some of the operations inside `st_object_record_update` may cause a thread switch**,
404549// so we can't assume a single update happens in a single "atomic" step -- other threads may get some running time
0 commit comments