@@ -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 ;
@@ -294,6 +317,12 @@ void heap_recorder_after_fork(heap_recorder *heap_recorder) {
294317 heap_recorder -> stats_lifetime = (struct stats_lifetime ) {0 };
295318}
296319
320+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
321+ // Sentinel marker for active_recording indicating we're using deferred recording.
322+ // Only used for pointer comparison, no data is read from or written to this.
323+ static object_record DEFERRED_RECORD = {.obj_id = -1 };
324+ #endif
325+
297326void start_heap_allocation_recording (heap_recorder * heap_recorder , VALUE new_obj , unsigned int weight , ddog_CharSlice alloc_class ) {
298327 if (heap_recorder == NULL ) {
299328 return ;
@@ -314,13 +343,29 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
314343 // directly OR because the GVL got released in the middle of an update), let's skip this sample as well.
315344 // See notes on `heap_recorder_update` for details.
316345 || heap_recorder -> updating
346+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
347+ // Skip if we've hit the pending recordings limit or if there's already a deferred object being recorded
348+ || heap_recorder -> pending_recordings_count >= MAX_PENDING_RECORDINGS
349+ || heap_recorder -> active_deferred_object != Qnil
350+ #endif
317351 ) {
318352 heap_recorder -> active_recording = & SKIPPED_RECORD ;
319353 return ;
320354 }
321355
322356 heap_recorder -> num_recordings_skipped = 0 ;
323357
358+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
359+ // On Ruby 4+, we can't call rb_obj_id during on_newobj_event as it mutates the object.
360+ // Instead, we store the VALUE reference and will get the object_id later via a postponed job.
361+ heap_recorder -> active_deferred_object = new_obj ;
362+ heap_recorder -> active_deferred_object_data = (live_object_data ) {
363+ .weight = weight * heap_recorder -> sample_rate ,
364+ .class = intern_or_raise (heap_recorder -> string_storage , alloc_class ),
365+ .alloc_gen = rb_gc_count (),
366+ };
367+ heap_recorder -> active_recording = & DEFERRED_RECORD ;
368+ #else
324369 VALUE ruby_obj_id = rb_obj_id (new_obj );
325370 if (!FIXNUM_P (ruby_obj_id )) {
326371 rb_raise (rb_eRuntimeError , "Detected a bignum object id. These are not supported by heap profiling." );
@@ -335,6 +380,7 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
335380 .alloc_gen = rb_gc_count (),
336381 }
337382 );
383+ #endif
338384}
339385
340386// end_heap_allocation_recording_with_rb_protect gets called while the stack_recorder is holding one of the profile
@@ -383,6 +429,23 @@ static VALUE end_heap_allocation_recording(VALUE protect_args) {
383429 return Qnil ;
384430 }
385431
432+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
433+ if (active_recording == & DEFERRED_RECORD ) {
434+ // On Ruby 4+, we defer the object_id call. Store the recording in the pending list.
435+ if (heap_recorder -> pending_recordings_count < MAX_PENDING_RECORDINGS ) {
436+ heap_record * heap_record = get_or_create_heap_record (heap_recorder , locations );
437+ heap_record -> num_tracked_objects ++ ; // Pre-increment since we're going to commit this later
438+
439+ pending_recording * pending = & heap_recorder -> pending_recordings [heap_recorder -> pending_recordings_count ++ ];
440+ pending -> object_ref = heap_recorder -> active_deferred_object ;
441+ pending -> heap_record = heap_record ;
442+ pending -> object_data = heap_recorder -> active_deferred_object_data ;
443+ }
444+ heap_recorder -> active_deferred_object = Qnil ;
445+ return Qnil ;
446+ }
447+ #endif
448+
386449 heap_record * heap_record = get_or_create_heap_record (heap_recorder , locations );
387450
388451 // And then commit the new allocation.
@@ -399,6 +462,89 @@ void heap_recorder_update_young_objects(heap_recorder *heap_recorder) {
399462 heap_recorder_update (heap_recorder , /* full_update: */ false);
400463}
401464
465+ #ifdef DEFERRED_HEAP_ALLOCATION_RECORDING
466+ bool heap_recorder_has_pending_recordings (heap_recorder * heap_recorder ) {
467+ if (heap_recorder == NULL ) {
468+ return false;
469+ }
470+ return heap_recorder -> pending_recordings_count > 0 ;
471+ }
472+
473+ bool heap_recorder_finalize_pending_recordings (heap_recorder * heap_recorder ) {
474+ if (heap_recorder == NULL ) {
475+ return false;
476+ }
477+
478+ uint count = heap_recorder -> pending_recordings_count ;
479+ if (count == 0 ) {
480+ return false;
481+ }
482+
483+ for (uint i = 0 ; i < count ; i ++ ) {
484+ pending_recording * pending = & heap_recorder -> pending_recordings [i ];
485+
486+ VALUE obj = pending -> object_ref ;
487+
488+ // Check if the object is still valid (it might have been GC'd between
489+ // the allocation and this finalization)
490+ if (obj == Qnil || !RTEST (obj )) {
491+ // Object is gone, decrement the pre-incremented count and cleanup
492+ pending -> heap_record -> num_tracked_objects -- ;
493+ cleanup_heap_record_if_unused (heap_recorder , pending -> heap_record );
494+ unintern_or_raise (heap_recorder , pending -> object_data .class );
495+ continue ;
496+ }
497+
498+ // Now it's safe to get the object_id
499+ VALUE ruby_obj_id = rb_obj_id (obj );
500+ if (!FIXNUM_P (ruby_obj_id )) {
501+ // Bignum object id - not supported, skip this recording
502+ pending -> heap_record -> num_tracked_objects -- ;
503+ cleanup_heap_record_if_unused (heap_recorder , pending -> heap_record );
504+ unintern_or_raise (heap_recorder , pending -> object_data .class );
505+ continue ;
506+ }
507+
508+ long obj_id = FIX2LONG (ruby_obj_id );
509+
510+ // Create the object record now that we have the object_id
511+ object_record * record = object_record_new (obj_id , pending -> heap_record , pending -> object_data );
512+
513+ // Commit the recording
514+ int existing_error = st_update (heap_recorder -> object_records , record -> obj_id , update_object_record_entry , (st_data_t ) record );
515+ if (existing_error ) {
516+ // Duplicate object_id - this shouldn't happen normally, but handle it gracefully
517+ pending -> heap_record -> num_tracked_objects -- ;
518+ cleanup_heap_record_if_unused (heap_recorder , pending -> heap_record );
519+ object_record_free (heap_recorder , record );
520+ }
521+ }
522+
523+ heap_recorder -> pending_recordings_count = 0 ;
524+ return true;
525+ }
526+
527+ // Mark pending recordings to prevent GC from collecting the objects
528+ // while they're waiting to be finalized
529+ void heap_recorder_mark_pending_recordings (heap_recorder * heap_recorder ) {
530+ if (heap_recorder == NULL ) {
531+ return ;
532+ }
533+
534+ for (uint i = 0 ; i < heap_recorder -> pending_recordings_count ; i ++ ) {
535+ VALUE obj = heap_recorder -> pending_recordings [i ].object_ref ;
536+ if (obj != Qnil ) {
537+ rb_gc_mark (obj );
538+ }
539+ }
540+
541+ // Also mark the active deferred object if it's set
542+ if (heap_recorder -> active_deferred_object != Qnil ) {
543+ rb_gc_mark (heap_recorder -> active_deferred_object );
544+ }
545+ }
546+ #endif
547+
402548// NOTE: This function needs and assumes it gets called with the GVL being held.
403549// But importantly **some of the operations inside `st_object_record_update` may cause a thread switch**,
404550// so we can't assume a single update happens in a single "atomic" step -- other threads may get some running time
0 commit comments