datadog 2.41.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -3,6 +3,7 @@
3
3
  #include "ruby_helpers.h"
4
4
  #include "collectors_stack.h"
5
5
  #include "libdatadog_helpers.h"
6
+ #include "private_vm_api_access.h"
6
7
  #include "time_helpers.h"
7
8
 
8
9
  // note on calloc vs ruby_xcalloc use:
@@ -68,24 +69,24 @@ static int heap_record_cmp_st(st_data_t, st_data_t);
68
69
  static st_index_t heap_record_hash_st(st_data_t);
69
70
  static const struct st_hash_type st_hash_type_heap_record = { .compare = heap_record_cmp_st, .hash = heap_record_hash_st };
70
71
 
71
- // An object record is used for storing data about currently tracked live objects
72
+ // A pending recording is used to defer adding the object to the `weak_objects` map: doing so is a Ruby method
73
+ // call that allocates, and neither of those is safe to do during on_newobj_event
72
74
  typedef struct {
73
- long obj_id;
75
+ VALUE object_ref;
76
+ long record_id; // Always in Fixnum range
74
77
  heap_record *heap_record;
75
78
  live_object_data object_data;
76
- } object_record;
77
- static object_record* object_record_new(long, heap_record*, live_object_data);
78
- static void object_record_free(heap_recorder*, object_record*, bool should_unintern);
79
- static VALUE object_record_inspect(heap_recorder*, object_record*);
80
- static object_record SKIPPED_RECORD = {0};
79
+ } pending_recording;
81
80
 
82
- // A pending recording is used to defer the object_id call on Ruby 4+
83
- // where calling rb_obj_id during on_newobj_event is unsafe.
81
+ // An object record is used for storing data about currently tracked live objects
84
82
  typedef struct {
85
- VALUE object_ref;
83
+ long record_id;
86
84
  heap_record *heap_record;
87
85
  live_object_data object_data;
88
- } pending_recording;
86
+ } object_record;
87
+ static object_record* object_record_new(pending_recording pending);
88
+ static void object_record_free(heap_recorder*, object_record*, bool should_unintern);
89
+ static VALUE object_record_inspect(heap_recorder*, object_record*);
89
90
 
90
91
  #define MAX_PENDING_RECORDINGS 256
91
92
 
@@ -106,7 +107,7 @@ struct heap_recorder {
106
107
  // entire stacks for us, then we wouldn't need to do it on the Ruby side.
107
108
  st_table *heap_records;
108
109
 
109
- // Map[obj_id: long, record: object_record*]
110
+ // Map[record_id: long, record: object_record*]
110
111
  // NOTE: This table is currently only protected by the GVL since we never interact with it
111
112
  // outside the GVL.
112
113
  // NOTE: This table has ownership of its object_records. The keys are longs and so are
@@ -114,18 +115,17 @@ struct heap_recorder {
114
115
  //
115
116
  // TODO: @ivoanjo We've evolved to actually never need to look up on object_records (we only insert and iterate),
116
117
  // so right now this seems to be just a really really fancy self-resizing list/set.
117
- // If we replace this with a list, we could record the latest id and compare it when inserting to make sure our
118
- // assumption of ids never reused + always increasing always holds. (This as an alternative to checking for duplicates)
118
+ // Tests do use it for lookup currently though.
119
119
  st_table *object_records;
120
120
 
121
- // Map[obj_id: long, record: object_record*]
121
+ // Map[record_id: long, record: object_record*]
122
122
  // NOTE: This is a snapshot of object_records built ahead of a iteration. Outside of an
123
123
  // iteration context, this table will be NULL. During an iteration, there will be no
124
124
  // mutation of the data so iteration can occur without acquiring a lock.
125
125
  // NOTE: Contrary to object_records, this table has no ownership of its data.
126
126
  st_table *object_records_snapshot;
127
- // Are we currently updating or not?
128
- bool updating;
127
+ // "Lock" protecting the heap recorder bookkeeping; see "note on locking" below
128
+ uint8_t lock;
129
129
  // The GC gen/epoch/count in which we are updating (or last updated if not currently updating).
130
130
  //
131
131
  // This enables us to calculate the age of objects considered in the update by comparing it
@@ -137,16 +137,18 @@ struct heap_recorder {
137
137
  // When did we do the last update of heap recorder?
138
138
  long last_update_ns;
139
139
 
140
- // Data for a heap recording that was started but not yet ended
141
- object_record *active_recording;
142
-
143
- // Pending recordings that need to be finalized after on_newobj_event completes.
144
- // On Ruby 4+, we can't call rb_obj_id during the newobj event, so we store the
145
- // VALUE reference here and finalize it via a postponed job.
140
+ // ObjectSpace::WeakMap[record_id: Integer (Fixnum) => object]
141
+ // Weak references to every object we're tracking.
142
+ // Looking a record_id up returns the object while it's alive, and nil once it's been garbage collected, which
143
+ // is how we determine liveness. See `ruby_weak_map_new` for details on why this gives us a weak reference.
144
+ VALUE weak_objects;
145
+ // Source for the ids used as keys in `object_records` and `weak_objects`. Ids are never reused.
146
+ long next_record_id;
147
+
148
+ // Recordings that are waiting to be committed after on_newobj_event completes.
149
+ // We can't add the object to `weak_objects` during the newobj event, so we store the
150
+ // VALUE reference here and commit it via a postponed job.
146
151
  pending_recording pending_recordings[MAX_PENDING_RECORDINGS];
147
- // Temporary storage for the recording in progress, used between start and end
148
- VALUE active_deferred_object;
149
- live_object_data active_deferred_object_data;
150
152
  uint16_t pending_recordings_count;
151
153
 
152
154
  // Reusable arrays, implementing a flyweight pattern for things like iteration
@@ -184,14 +186,75 @@ struct heap_recorder {
184
186
  double ewma_objects_skipped;
185
187
 
186
188
  unsigned long deferred_recordings_skipped_buffer_full;
187
- unsigned long deferred_recordings_finalized;
189
+ unsigned long deferred_recordings_committed;
188
190
  } stats_lifetime;
189
191
  };
190
192
 
193
+ // note on locking:
194
+ //
195
+ // The state of the heap profiler (`pending_recordings`, `object_records`, `heap_records`, `weak_objects`) gets mutated from a
196
+ // few different places and, unlike most of the profiler, some of those operations lose the GVL while they work.
197
+ // Thus, unlike most of the profiler, relying only on knowing "we get called with the GVL" is not enough to keep them
198
+ // from stepping on each other, hence this extra "uint8_t lock".
199
+ //
200
+ // Yet, because we only ever touch while holding the GVL, and never lose the GVL between checking it and setting it, a
201
+ // plain field is enough -- no atomics needed. (Hence the "lock" and not a full actual lock)
202
+ //
203
+ // Operations that can be skipped use `heap_recorder_try_lock` and walk away when it's taken (which is almost all of them).
204
+ // The one operation that can't be skipped -- the full update that runs before serialization -- uses
205
+ // `heap_recorder_lock`, which waits.
206
+ //
207
+ // `heap_recorder_lock` is expected to be bounded because once `LOCK_WANTED` gets set, no `try_lock` will succeed, so
208
+ // the `heap_recorder_lock` will only need to wait for the previous user of the lock to finish it's work + Ruby to
209
+ // switch back to is thread.
210
+ #define HEAP_RECORDER_LOCK_HELD 0x1
211
+ #define HEAP_RECORDER_LOCK_WANTED 0x2
212
+
213
+ // Is someone in the middle of a locked operation? (Which, because they may lose the GVL, may be true even though we're
214
+ // the ones currently holding the GVL.)
215
+ static inline bool heap_recorder_is_locked(heap_recorder *heap_recorder) {
216
+ return (heap_recorder->lock & HEAP_RECORDER_LOCK_HELD) != 0;
217
+ }
218
+
219
+ // Takes the lock, unless it's held or someone is waiting for it. Returns whether it was taken.
220
+ __attribute__((warn_unused_result))
221
+ static inline bool heap_recorder_try_lock(heap_recorder *heap_recorder) {
222
+ if (heap_recorder->lock != 0) return false;
223
+
224
+ heap_recorder->lock = HEAP_RECORDER_LOCK_HELD;
225
+ return true;
226
+ }
227
+
228
+ // Takes the lock, waiting for the current holder to finish if needed. See "note on locking" above for why this wait is
229
+ // expected to be bounded.
230
+ //
231
+ // WARN: Do not call this while already holding the lock -- it would wait forever.
232
+ static void heap_recorder_lock(heap_recorder *heap_recorder) {
233
+ while (heap_recorder_is_locked(heap_recorder)) {
234
+ heap_recorder->lock |= HEAP_RECORDER_LOCK_WANTED;
235
+ rb_thread_schedule();
236
+ }
237
+
238
+ // Note that we go from observing the lock as free to taking it without ever losing the GVL, and thus without giving
239
+ // anyone else a chance to take it from under us
240
+ heap_recorder->lock = HEAP_RECORDER_LOCK_HELD;
241
+ }
242
+
243
+ static inline void heap_recorder_unlock(heap_recorder *heap_recorder) {
244
+ // Deliberately preserves `HEAP_RECORDER_LOCK_WANTED` (rather than just zero-ing the lock)
245
+ heap_recorder->lock &= ~HEAP_RECORDER_LOCK_HELD;
246
+ }
247
+
248
+ // Same as the above, in the shape `rb_ensure` wants
249
+ static VALUE heap_recorder_unlock_ensure(VALUE heap_recorder_as_value) {
250
+ heap_recorder_unlock((heap_recorder *) heap_recorder_as_value);
251
+ return Qnil;
252
+ }
253
+
191
254
  typedef struct {
192
255
  heap_recorder *heap_recorder;
193
- ddog_prof_Slice_Location locations;
194
- } end_heap_allocation_args;
256
+ bool full_update;
257
+ } heap_recorder_update_locked_args;
195
258
 
196
259
  static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location);
197
260
  static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*);
@@ -201,16 +264,66 @@ static int st_object_record_entry_free_no_unintern(st_data_t, st_data_t, st_data
201
264
  static int st_object_record_update(st_data_t, st_data_t, st_data_t);
202
265
  static int st_object_records_iterate(st_data_t, st_data_t, st_data_t);
203
266
  static int st_object_records_debug(st_data_t key, st_data_t value, st_data_t extra);
204
- static int update_object_record_entry(st_data_t*, st_data_t*, st_data_t, int);
205
267
  static void inc_tracked_objects_or_fail(heap_record *heap_record);
206
- static void commit_recording(heap_recorder *, heap_record *, object_record *active_recording);
207
- static VALUE end_heap_allocation_recording(VALUE end_heap_allocation_args);
268
+ static void commit_recording(heap_recorder *, pending_recording);
208
269
  static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update);
270
+ static VALUE heap_recorder_update_locked(VALUE heap_recorder_update_locked_args_as_value);
271
+ static VALUE heap_recorder_commit_recordings_may_lose_gvl_locked(VALUE heap_recorder_as_value);
209
272
  static inline double ewma_stat(double previous, double current);
210
273
  static void unintern_or_raise(heap_recorder *, ddog_prof_ManagedStringId);
211
274
  static void unintern_all_or_raise(heap_recorder *recorder, ddog_prof_Slice_ManagedStringId ids);
212
275
  static VALUE get_ruby_string_or_raise(heap_recorder*, ddog_prof_ManagedStringId);
213
- static long obj_id_or_fail(VALUE obj);
276
+
277
+ // The following global variables are initialized at startup to save expensive lookups later.
278
+ // They are not expected to be mutated outside of init.
279
+ static VALUE class_weak_map = Qnil;
280
+ static ID aref_id = Qnil;
281
+ static ID aset_id = Qnil;
282
+
283
+ void collectors_heap_recorder_init(void) {
284
+ rb_global_variable(&class_weak_map);
285
+
286
+ VALUE module_object_space = rb_const_get(rb_cObject, rb_intern("ObjectSpace"));
287
+ class_weak_map = rb_const_get(module_object_space, rb_intern("WeakMap"));
288
+ aref_id = rb_intern("[]");
289
+ aset_id = rb_intern("[]=");
290
+ }
291
+
292
+ // Native wrapper to create a new `ObjectSpace::WeakMap`.
293
+ //
294
+ // Because an `ObjectSpace::WeakMap` entry is dropped as soon as *either* its key or its value is garbage
295
+ // collected, pairing a key that can never be collected (such as a fixnum) with the object of interest as the
296
+ // value gives us a weak reference: reading the key back returns the object while it's alive, and nothing once
297
+ // it's been collected.
298
+ static VALUE ruby_weak_map_new(void) {
299
+ return rb_class_new_instance(0, NULL, class_weak_map);
300
+ }
301
+
302
+ // Native wrapper to get an object from an `ObjectSpace::WeakMap`.
303
+ // Returns the object on success and nil if the entry is gone, meaning
304
+ // the object has been garbage collected.
305
+ // We never store nil as a value, see ruby_weak_map_set_may_lose_gvl_and_allocate_objects(), so nil unambiguously
306
+ // means "the value was garbage collected".
307
+ //
308
+ // Note: GVL can be released and other threads may get to run before this method returns
309
+ static VALUE ruby_weak_map_get_may_lose_gvl(VALUE weak_map, VALUE key) {
310
+ return rb_funcall(weak_map, aref_id, 1, key);
311
+ }
312
+
313
+ // Native wrapper to add an entry to an `ObjectSpace::WeakMap`.
314
+ // Raises RuntimeError if passed nil as a value.
315
+ //
316
+ // Note: GVL can be released and other threads may get to run before this method returns.
317
+ //
318
+ // On Ruby < 3.3 this allocates Ruby objects (because underneath the weak map registers
319
+ // finalizers for the objects, which requires allocations). Later Rubies improved WeakMap to not need this.
320
+ // This we need to be careful not to recurse on the profiler (e.g. NEWOBJ tracepoint).
321
+ static void ruby_weak_map_set_may_lose_gvl_and_allocate_objects(VALUE weak_map, VALUE key, VALUE value) {
322
+ if (value == Qnil) {
323
+ raise_error(rb_eRuntimeError, "Can't use nil as the value in the WeakMap, otherwise #[] can't differentiate alive vs nil value");
324
+ }
325
+ rb_funcall(weak_map, aset_id, 2, key, value);
326
+ }
214
327
 
215
328
  // ==========================
216
329
  // Heap Recorder External API
@@ -230,11 +343,12 @@ heap_recorder* heap_recorder_new(ddog_prof_ManagedStringStorage string_storage)
230
343
  recorder->reusable_locations = ruby_xcalloc(REUSABLE_LOCATIONS_SIZE, sizeof(ddog_prof_Location));
231
344
  recorder->reusable_ids = ruby_xcalloc(REUSABLE_FRAME_DETAILS_SIZE, sizeof(ddog_prof_ManagedStringId));
232
345
  recorder->reusable_char_slices = ruby_xcalloc(REUSABLE_FRAME_DETAILS_SIZE, sizeof(ddog_CharSlice));
233
- recorder->active_recording = NULL;
234
346
  recorder->size_enabled = true;
235
347
  recorder->sample_rate = 1; // By default do no sampling on top of what allocation profiling already does
236
348
  recorder->string_storage = string_storage;
237
- recorder->active_deferred_object = Qnil;
349
+ // Note: This allocates, and thus can trigger a GC. That's fine: our caller only publishes the heap recorder on the
350
+ // stack recorder state after we return, so `heap_recorder_mark` will not observe a half-initialized recorder.
351
+ recorder->weak_objects = ruby_weak_map_new();
238
352
 
239
353
  return recorder;
240
354
  }
@@ -265,11 +379,6 @@ void heap_recorder_free(heap_recorder *heap_recorder) {
265
379
  st_foreach(heap_recorder->heap_records, st_heap_record_entry_free_no_unintern, (st_data_t) heap_recorder);
266
380
  st_free_table(heap_recorder->heap_records);
267
381
 
268
- if (heap_recorder->active_recording != NULL && heap_recorder->active_recording != &SKIPPED_RECORD) {
269
- // If there's a partial object record, clean it up as well
270
- object_record_free(heap_recorder, heap_recorder->active_recording, false);
271
- }
272
-
273
382
  ruby_xfree(heap_recorder->reusable_locations);
274
383
  ruby_xfree(heap_recorder->reusable_ids);
275
384
  ruby_xfree(heap_recorder->reusable_char_slices);
@@ -322,150 +431,95 @@ void heap_recorder_after_fork(heap_recorder *heap_recorder) {
322
431
  heap_recorder_finish_iteration(heap_recorder);
323
432
  }
324
433
 
434
+ // This could also be left over if fork happens in the middle of a locked operation
435
+ heap_recorder->lock = 0;
436
+
325
437
  // Clear lifetime stats since this is essentially a new heap recorder
326
438
  heap_recorder->stats_lifetime = (struct stats_lifetime) {0};
327
439
  }
328
440
 
329
- bool start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj, unsigned int weight, ddog_CharSlice alloc_class) {
441
+ // This method gets called from inside the RUBY_INTERNAL_EVENT_NEWOBJ tracepoint so it should neither allocate in the
442
+ // Ruby heap nor release the GVL (https://github.com/DataDog/dd-trace-rb/pull/4240).
443
+ //
444
+ // See the header for details on the arguments and on `needs_commit`.
445
+ void heap_recorder_record_allocation(
446
+ heap_recorder *heap_recorder,
447
+ VALUE new_object,
448
+ unsigned int weight,
449
+ ddog_CharSlice alloc_class,
450
+ ddog_prof_Slice_Location locations,
451
+ bool *needs_commit
452
+ ) {
330
453
  if (heap_recorder == NULL) {
331
- return false;
332
- }
333
-
334
- if (heap_recorder->active_recording != NULL) {
335
- raise_error(rb_eRuntimeError, "Detected consecutive heap allocation recording starts without end.");
454
+ *needs_commit = false;
455
+ return;
336
456
  }
337
457
 
338
- if (++heap_recorder->num_recordings_skipped < heap_recorder->sample_rate ||
339
- #ifdef NO_IMEMO_OBJECT_ID
340
- // On Ruby 4, we can't ask the object_id from IMEMOs (https://github.com/ruby/ruby/pull/13347)
341
- RB_BUILTIN_TYPE(new_obj) == RUBY_T_IMEMO
342
- #else
343
- false
344
- #endif
345
- // If we got really unlucky and an allocation showed up during an update (because it triggered an allocation
346
- // directly OR because the GVL got released in the middle of an update), let's skip this sample as well.
347
- // See notes on `heap_recorder_update` for details.
348
- || heap_recorder->updating
349
- ) {
350
- heap_recorder->active_recording = &SKIPPED_RECORD;
351
- return false;
458
+ // We always report `needs_commit` when there's anything pending, even if it's from a previous allocation.
459
+ //
460
+ // This can mean we "spam" a bit the postponed jobs mechanism (e.g. perhaps our postponed job hasn't run
461
+ // because there's a native extension doing Ruby object allocations in a row without giving Ruby the chance to
462
+ // run interrupts.
463
+ // That shouldn't be a problem because the allocation profiler has the dynamic sampling rate mechanism +
464
+ // triggering postponed jobs is cheap on current Rubies.
465
+ // (Note that asking for the same job multiple times de-duplicates -- Ruby will only run it once when it gets the chance)
466
+ *needs_commit = heap_recorder->pending_recordings_count > 0;
467
+
468
+ if (++heap_recorder->num_recordings_skipped < heap_recorder->sample_rate) return;
469
+
470
+ if (heap_recorder_is_locked(heap_recorder)) {
471
+ // If we got unlucky and an allocation showed up in the middle of a locked operation (because it triggered
472
+ // an allocation directly OR because that operation lost the GVL), let's skip this sample as well.
473
+ // (Note we don't take the lock ourselves: the current function never loses the GVL, and is thus already atomic.)
474
+ return;
352
475
  }
353
476
 
354
- bool needs_after_allocation = false;
477
+ // Do not sample internal objects in the heap profiler because they are added to a ObjectSpace::WeakMap, which would not be safe.
478
+ // This means we ignore internal VM objects samples for the heap profiler,
479
+ // which is a trade-off discussed in https://github.com/DataDog/dd-trace-rb/pull/6176#discussion_r3819776251
480
+ //
481
+ // Note this is checked after the sample rate above, as the sample rate is in number of allocation profiler samples,
482
+ // not in number of non-internal samples. Like the skips above, num_recordings_skipped is not reset here, so the
483
+ // next allocation sample still gets a chance to be tracked.
484
+ if (ddtrace_is_internal_object_p(new_object)) return;
355
485
 
356
- #ifdef USE_DEFERRED_HEAP_ALLOCATION_RECORDING
357
- // Skip if we've hit the pending recordings limit or if there's already a deferred object being recorded
486
+ // Skip if we've hit the pending recordings limit
358
487
  if (heap_recorder->pending_recordings_count >= MAX_PENDING_RECORDINGS) {
359
488
  heap_recorder->stats_lifetime.deferred_recordings_skipped_buffer_full++;
360
- heap_recorder->active_recording = &SKIPPED_RECORD;
361
- return true; // If the buffer is full, we keep asking for a callback (see `needs_after_allocation` below)
362
- } else {
363
- // The intuition here is: We start by asking for an `after_allocation` callback when the buffer is about to go
364
- // from empty -> non-empty, because this is going to be mapped onto a postponed job, so after it gets queued once
365
- // it doesn't seem worth it to keep spamming requests.
366
- //
367
- // Yet, if for some reason the postponed job doesn't flush the pending list (or if e.g. it ran with `during_sample == true` and thus
368
- // was skipped) we need to have some mechanism to recover -- and so if the buffer starts accumulating too much we
369
- // start always requesting the callback to happen so that we eventually flush the buffer.
370
- needs_after_allocation =
371
- heap_recorder->pending_recordings_count == 0 || heap_recorder->pending_recordings_count >= (MAX_PENDING_RECORDINGS / 2);
489
+ return; // `needs_commit` is already true: a full buffer definitely has something to commit
372
490
  }
373
- #endif
374
491
 
375
492
  heap_recorder->num_recordings_skipped = 0;
376
493
 
377
- live_object_data object_data = (live_object_data) {
494
+ // Note: Everything that can raise happens before we add the recording to `pending_recordings` below, so that a
495
+ // failure never leaves a half-built entry behind
496
+ live_object_data object_data = {
378
497
  .weight = weight * heap_recorder->sample_rate,
379
498
  .class = intern_or_raise(heap_recorder->string_storage, alloc_class),
380
499
  .alloc_gen = rb_gc_count(),
381
500
  };
382
501
 
383
- #ifdef USE_DEFERRED_HEAP_ALLOCATION_RECORDING
384
- // On Ruby 4+, we can't call rb_obj_id during on_newobj_event as it mutates the object.
385
- // Instead, we store the VALUE reference and will get the object_id later via a postponed job.
386
- // active_deferred_object != Qnil indicates we're in deferred mode.
387
- heap_recorder->active_deferred_object = new_obj;
388
- heap_recorder->active_deferred_object_data = object_data;
389
- #else
390
- heap_recorder->active_recording = object_record_new(obj_id_or_fail(new_obj), NULL, object_data);
391
- #endif
392
-
393
- return needs_after_allocation;
394
- }
395
-
396
- // end_heap_allocation_recording_with_rb_protect gets called while the stack_recorder is holding one of the profile
397
- // locks. To enable us to correctly unlock the profile on exception, we wrap the call to end_heap_allocation_recording
398
- // with an rb_protect.
399
- __attribute__((warn_unused_result))
400
- int end_heap_allocation_recording_with_rb_protect(heap_recorder *heap_recorder, ddog_prof_Slice_Location locations) {
401
- if (heap_recorder == NULL) {
402
- return 0;
403
- }
404
- if (heap_recorder->active_recording == &SKIPPED_RECORD) {
405
- // Short circuit, in this case there's nothing to be done
406
- heap_recorder->active_recording = NULL;
407
- return 0;
502
+ long record_id = ++heap_recorder->next_record_id;
503
+ if (record_id > RUBY_FIXNUM_MAX) {
504
+ // We rely on record_id always fitting in a FIXNUM to avoid extra overhead and
505
+ // allocation here. If we exhaust the record_id, we stop profiling.
506
+ // We don't expect this to happen in practice, as we don't heap sample every object, and RUBY_FIXNUM_MAX is (2**62 - 1)
507
+ raise_error(rb_eRuntimeError, "Heap profiling: Exhausted usable record_ids");
408
508
  }
409
509
 
410
- int exception_state;
411
- end_heap_allocation_args args = {
412
- .heap_recorder = heap_recorder,
413
- .locations = locations,
414
- };
415
- rb_protect(end_heap_allocation_recording, (VALUE) &args, &exception_state);
416
- return exception_state;
417
- }
418
-
419
- static VALUE end_heap_allocation_recording(VALUE protect_args) {
420
- end_heap_allocation_args *args = (end_heap_allocation_args *) protect_args;
421
-
422
- heap_recorder *heap_recorder = args->heap_recorder;
423
- ddog_prof_Slice_Location locations = args->locations;
424
-
425
- #ifdef USE_DEFERRED_HEAP_ALLOCATION_RECORDING
426
- if (heap_recorder->active_deferred_object == Qnil) {
427
- // Recording ended without having been started?
428
- raise_error(rb_eRuntimeError, "Ended a heap recording that was not started");
429
- }
430
- #else
431
- object_record *active_recording = heap_recorder->active_recording;
432
-
433
- if (active_recording == NULL) {
434
- // Recording ended without having been started?
435
- raise_error(rb_eRuntimeError, "Ended a heap recording that was not started");
436
- }
437
- // From now on, mark the global active recording as invalid so we can short-circuit at any point
438
- // and not end up with a still active recording. the local active_recording still holds the
439
- // data required for committing though.
440
- heap_recorder->active_recording = NULL;
441
-
442
- if (active_recording == &SKIPPED_RECORD) {
443
- raise_error(
444
- rb_eRuntimeError,
445
- "BUG: end_heap_allocation_recording should never observe SKIPPED_RECORDING because " \
446
- "end_heap_allocation_recording_with_rb_protect is supposed to test for it directly"
447
- );
448
- }
449
- #endif
450
-
451
510
  heap_record *heap_record = get_or_create_heap_record(heap_recorder, locations);
452
511
  inc_tracked_objects_or_fail(heap_record);
453
512
 
454
- #ifdef USE_DEFERRED_HEAP_ALLOCATION_RECORDING
455
- // Commit is delayed, so we need to record all we'll need for it
456
- pending_recording *pending = &heap_recorder->pending_recordings[heap_recorder->pending_recordings_count++];
457
- pending->object_ref = heap_recorder->active_deferred_object;
458
- pending->heap_record = heap_record;
459
- pending->object_data = heap_recorder->active_deferred_object_data;
460
-
461
- heap_recorder->active_deferred_object = Qnil;
462
- heap_recorder->active_deferred_object_data = (live_object_data) {0};
463
- #else
464
- // And then commit the new allocation
465
- commit_recording(heap_recorder, heap_record, active_recording);
466
- #endif
513
+ // We can't add the object to `weak_objects` from inside the NEWOBJ tracepoint, so the recording stays pending until
514
+ // `heap_recorder_commit_recordings_may_lose_gvl` gets to it
515
+ heap_recorder->pending_recordings[heap_recorder->pending_recordings_count++] = (pending_recording) {
516
+ .object_ref = new_object,
517
+ .record_id = record_id,
518
+ .heap_record = heap_record,
519
+ .object_data = object_data,
520
+ };
467
521
 
468
- return Qnil;
522
+ *needs_commit = true; // We just added a recording
469
523
  }
470
524
 
471
525
  void heap_recorder_update_young_objects(heap_recorder *heap_recorder) {
@@ -476,46 +530,68 @@ void heap_recorder_update_young_objects(heap_recorder *heap_recorder) {
476
530
  heap_recorder_update(heap_recorder, /* full_update: */ false);
477
531
  }
478
532
 
479
- void heap_recorder_finalize_pending_recordings(heap_recorder *heap_recorder) {
533
+ void heap_recorder_commit_recordings_may_lose_gvl(heap_recorder *heap_recorder) {
480
534
  if (heap_recorder == NULL) {
481
535
  return; // Nothing to do
482
536
  }
483
537
 
484
- uint count = heap_recorder->pending_recordings_count;
485
- if (count == 0) {
486
- return; // Nothing to do
538
+ if (!heap_recorder_try_lock(heap_recorder)) {
539
+ // Something else is busy; `heap_recorder_record_allocation` will ask for us to be called again later
540
+ return;
487
541
  }
488
542
 
489
- heap_recorder->stats_lifetime.deferred_recordings_finalized += count;
543
+ rb_ensure(
544
+ heap_recorder_commit_recordings_may_lose_gvl_locked,
545
+ (VALUE) heap_recorder,
546
+ heap_recorder_unlock_ensure,
547
+ (VALUE) heap_recorder
548
+ );
549
+ }
550
+
551
+ static VALUE heap_recorder_commit_recordings_may_lose_gvl_locked(VALUE heap_recorder_as_value) {
552
+ heap_recorder *heap_recorder = (struct heap_recorder *) heap_recorder_as_value;
490
553
 
491
- for (uint i = 0; i < count; i++) {
492
- pending_recording *pending = &heap_recorder->pending_recordings[i];
554
+ // Note: We consume the buffer back-to-front, and only drop each entry once it's fully committed. That makes the
555
+ // "can lose the GVL" bit below safe to retry: if anything raises, or another thread unwinds us, the entry stays
556
+ // pending and a later commit picks it up -- and setting the same entry on the weak map again is harmless. It also
557
+ // means the entry we're working on stays marked (see `heap_recorder_mark`) across any GC that happens while we don't
558
+ // have the GVL.
559
+ //
560
+ // Note as well that `ruby_weak_map_set_may_lose_gvl_and_allocate_objects` below can lose the GVL (and, on older
561
+ // Rubies, allocate). This function gets called while holding the heap recorder lock so that no other heap recorder
562
+ // operation can concurrently execute with us in that window (including recording more allocations, which on older Rubies
563
+ // would otherwise add entries to the very buffer we're draining).
564
+ while (heap_recorder->pending_recordings_count > 0) {
565
+ pending_recording pending = heap_recorder->pending_recordings[heap_recorder->pending_recordings_count - 1];
493
566
 
494
- // This is the step we couldn't do during the original sample call -- we're now expected to be called in a context
495
- // where it's finally safe to call this
496
- long obj_id = obj_id_or_fail(pending->object_ref);
567
+ ruby_weak_map_set_may_lose_gvl_and_allocate_objects(heap_recorder->weak_objects, LONG2FIX(pending.record_id), pending.object_ref);
497
568
 
498
- // Create the object record now that we have the object_id
499
- object_record *record = object_record_new(obj_id, pending->heap_record, pending->object_data);
569
+ // After here, we can no longer lose the GVL until we loop around again, so these steps are "atomic"
500
570
 
501
- commit_recording(heap_recorder, pending->heap_record, record);
571
+ commit_recording(heap_recorder, pending);
572
+ heap_recorder->pending_recordings_count--;
573
+ heap_recorder->stats_lifetime.deferred_recordings_committed++;
502
574
  }
503
575
 
504
- heap_recorder->pending_recordings_count = 0;
576
+ return Qnil;
505
577
  }
506
578
 
507
- // Mark pending recordings to prevent GC from collecting the objects
508
- // while they're waiting to be finalized
509
- void heap_recorder_mark_pending_recordings(heap_recorder *heap_recorder) {
579
+ // Mark the Ruby objects the heap recorder holds on to.
580
+ //
581
+ // Note that the objects we track are deliberately **not** marked here: `weak_objects` holds only weak references
582
+ // to them, which is exactly what lets us detect that they've been garbage collected.
583
+ void heap_recorder_mark(heap_recorder *heap_recorder) {
510
584
  if (heap_recorder == NULL) {
511
585
  return;
512
586
  }
513
587
 
588
+ // Mark pending recordings while they're waiting to be committed, otherwise it won't be safe to read them later.
589
+ // We would not mind if they are GC'd, but we would need to know that reliably and we can't.
514
590
  for (uint i = 0; i < heap_recorder->pending_recordings_count; i++) {
515
591
  rb_gc_mark(heap_recorder->pending_recordings[i].object_ref);
516
592
  }
517
593
 
518
- rb_gc_mark(heap_recorder->active_deferred_object);
594
+ rb_gc_mark(heap_recorder->weak_objects);
519
595
  }
520
596
 
521
597
  // NOTE: This function needs and assumes it gets called with the GVL being held.
@@ -523,32 +599,31 @@ void heap_recorder_mark_pending_recordings(heap_recorder *heap_recorder) {
523
599
  // so we can't assume a single update happens in a single "atomic" step -- other threads may get some running time
524
600
  // in the meanwhile.
525
601
  static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update) {
526
- if (heap_recorder->updating) {
527
- if (full_update) {
528
- // There's another thread that's already doing an update :(
529
- //
530
- // Because there's a lock on the `StackRecorder` (see @no_concurrent_serialize_mutex) then it's not possible that
531
- // the other update is a full update.
532
- // Thus we expect is happening is that the GVL got released by the other thread in the middle of a non-full update
533
- // and the scheduler thread decided now was a great time to serialize the profile.
534
- //
535
- // So, let's yield the time on the current thread until Ruby goes back to the other thread doing the update and
536
- // it finishes cleanly.
537
- while (heap_recorder->updating) { rb_thread_schedule(); }
538
- } else {
539
- // Non-full updates are optional, so let's walk away
540
- heap_recorder->stats_lifetime.updates_skipped_concurrent++;
541
- return;
542
- }
602
+ if (full_update) {
603
+ // A full update runs as part of serialization and can't be skipped, so we wait for our turn if needed
604
+ heap_recorder_lock(heap_recorder);
605
+ } else if (!heap_recorder_try_lock(heap_recorder)) {
606
+ // Non-full updates are optional, so let's walk away
607
+ heap_recorder->stats_lifetime.updates_skipped_concurrent++;
608
+ return;
543
609
  }
544
610
 
611
+ heap_recorder_update_locked_args args = {.heap_recorder = heap_recorder, .full_update = full_update};
612
+ rb_ensure(heap_recorder_update_locked, (VALUE) &args, heap_recorder_unlock_ensure, (VALUE) heap_recorder);
613
+ }
614
+
615
+ static VALUE heap_recorder_update_locked(VALUE heap_recorder_update_locked_args_as_value) {
616
+ heap_recorder_update_locked_args *args = (heap_recorder_update_locked_args *) heap_recorder_update_locked_args_as_value;
617
+ heap_recorder *heap_recorder = args->heap_recorder;
618
+ bool full_update = args->full_update;
619
+
545
620
  if (heap_recorder->object_records_snapshot != NULL) {
546
621
  // While serialization is happening, it runs without the GVL and uses the object_records_snapshot.
547
622
  // Although we iterate on a snapshot of object_records, these records point to other data that has not been
548
623
  // snapshotted for efficiency reasons (e.g. heap_records). Since updating may invalidate
549
624
  // some of that non-snapshotted data, let's refrain from doing updates during iteration. This also enforces the
550
625
  // semantic that iteration will operate as a point-in-time snapshot.
551
- return;
626
+ return Qnil;
552
627
  }
553
628
 
554
629
  size_t current_gc_gen = rb_gc_count();
@@ -563,17 +638,16 @@ static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update)
563
638
  // object records to do an update, let's wait until all steps for a particular GC generation
564
639
  // have finished to do so. We may revisit this once we have a better liveness checking mechanism.
565
640
  heap_recorder->stats_lifetime.updates_skipped_gcgen++;
566
- return;
641
+ return Qnil;
567
642
  }
568
643
 
569
644
  if (now_ns > 0 && (now_ns - heap_recorder->last_update_ns) < MIN_TIME_BETWEEN_HEAP_RECORDER_UPDATES_NS) {
570
645
  // We did an update not too long ago. Let's skip this one to avoid over-taxing the system.
571
646
  heap_recorder->stats_lifetime.updates_skipped_time++;
572
- return;
647
+ return Qnil;
573
648
  }
574
649
  }
575
650
 
576
- heap_recorder->updating = true;
577
651
  // Reset last update stats, we'll be building them from scratch during the st_foreach call below
578
652
  heap_recorder->stats_last_update = (struct stats_last_update) {0};
579
653
 
@@ -596,7 +670,7 @@ static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update)
596
670
  heap_recorder->stats_lifetime.ewma_objects_skipped = ewma_stat(heap_recorder->stats_lifetime.ewma_objects_skipped, heap_recorder->stats_last_update.objects_skipped);
597
671
  }
598
672
 
599
- heap_recorder->updating = false;
673
+ return Qnil;
600
674
  }
601
675
 
602
676
  void heap_recorder_prepare_iteration(heap_recorder *heap_recorder) {
@@ -690,7 +764,7 @@ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder) {
690
764
  ID2SYM(rb_intern("lifetime_ewma_objects_skipped")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_objects_skipped),
691
765
 
692
766
  ID2SYM(rb_intern("lifetime_deferred_recordings_skipped_buffer_full")), /* => */ ULONG2NUM(heap_recorder->stats_lifetime.deferred_recordings_skipped_buffer_full),
693
- ID2SYM(rb_intern("lifetime_deferred_recordings_finalized")), /* => */ ULONG2NUM(heap_recorder->stats_lifetime.deferred_recordings_finalized),
767
+ ID2SYM(rb_intern("lifetime_deferred_recordings_committed")), /* => */ ULONG2NUM(heap_recorder->stats_lifetime.deferred_recordings_committed),
694
768
  };
695
769
  VALUE hash = rb_hash_new();
696
770
  for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(hash, arguments[i], arguments[i+1]);
@@ -737,12 +811,10 @@ static int st_object_record_entry_free_no_unintern(DDTRACE_UNUSED st_data_t key,
737
811
 
738
812
  // NOTE: Some operations inside this function can cause the GVL to be released! Plan accordingly.
739
813
  static int st_object_record_update(st_data_t key, st_data_t value, st_data_t extra_arg) {
740
- long obj_id = (long) key;
814
+ long record_id = (long) key;
741
815
  object_record *record = (object_record*) value;
742
816
  heap_recorder *recorder = (heap_recorder*) extra_arg;
743
817
 
744
- VALUE ref;
745
-
746
818
  size_t update_gen = recorder->update_gen;
747
819
  size_t alloc_gen = record->object_data.alloc_gen;
748
820
  // Guard against potential overflows given unsigned types here.
@@ -761,8 +833,10 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
761
833
  return ST_CONTINUE;
762
834
  }
763
835
 
764
- if (!ruby_ref_from_id(LONG2NUM(obj_id), &ref)) { // Note: This function call can cause the GVL to be released
765
- // Id no longer associated with a valid ref. Need to delete this object record!
836
+ // Note: This function call can cause the GVL to be released
837
+ VALUE ref = ruby_weak_map_get_may_lose_gvl(recorder->weak_objects, LONG2FIX(record_id));
838
+ if (ref == Qnil) {
839
+ // The weak reference is gone, meaning the object was garbage collected. Need to delete this object record!
766
840
  on_committed_object_record_cleanup(recorder, record);
767
841
  recorder->stats_last_update.objects_dead++;
768
842
  return ST_DELETE;
@@ -845,15 +919,6 @@ static int st_object_records_debug(DDTRACE_UNUSED st_data_t key, st_data_t value
845
919
  return ST_CONTINUE;
846
920
  }
847
921
 
848
- static int update_object_record_entry(DDTRACE_UNUSED st_data_t *key, st_data_t *value, st_data_t new_object_record, int existing) {
849
- if (!existing) {
850
- (*value) = (st_data_t) new_object_record; // Expected to be a `object_record *`
851
- } else {
852
- // If key already existed, we don't touch the existing value, so it can be used for diagnostics
853
- }
854
- return ST_CONTINUE;
855
- }
856
-
857
922
  static void inc_tracked_objects_or_fail(heap_record *heap_record) {
858
923
  if (heap_record->num_tracked_objects == UINT32_MAX) {
859
924
  raise_error(rb_eRuntimeError, "Reached maximum number of tracked objects for heap record");
@@ -861,21 +926,12 @@ static void inc_tracked_objects_or_fail(heap_record *heap_record) {
861
926
  heap_record->num_tracked_objects++;
862
927
  }
863
928
 
864
- static void commit_recording(heap_recorder *heap_recorder, heap_record *heap_record, object_record *active_recording) {
865
- // Link the object record with the corresponding heap record. This was the last remaining thing we
866
- // needed to fully build the object_record.
867
- active_recording->heap_record = heap_record;
868
-
869
- int existing_error = st_update(heap_recorder->object_records, active_recording->obj_id, update_object_record_entry, (st_data_t) active_recording);
870
- if (existing_error) {
871
- object_record *existing_record = NULL;
872
- st_lookup(heap_recorder->object_records, active_recording->obj_id, (st_data_t *) &existing_record);
873
- if (existing_record == NULL) raise_error(rb_eRuntimeError, "Unexpected NULL when reading existing record");
874
-
875
- VALUE existing_inspect = object_record_inspect(heap_recorder, existing_record);
876
- VALUE new_inspect = object_record_inspect(heap_recorder, active_recording);
877
- raise_error(rb_eRuntimeError, "Object ids are supposed to be unique. We got 2 allocation recordings with "
878
- "the same id. previous={%"PRIsVALUE"} new={%"PRIsVALUE"}", existing_inspect, new_inspect);
929
+ static void commit_recording(heap_recorder *heap_recorder, pending_recording pending) {
930
+ object_record *new_record = object_record_new(pending);
931
+
932
+ int existing = st_insert(heap_recorder->object_records, new_record->record_id, (st_data_t) new_record);
933
+ if (existing) {
934
+ raise_error(rb_eRuntimeError, "Unexpected record with the same record_id in heap_recorder->object_records");
879
935
  }
880
936
  }
881
937
 
@@ -895,7 +951,7 @@ static heap_record* get_or_create_heap_record(heap_recorder *heap_recorder, ddog
895
951
  heap_record *stack = heap_record_new(heap_recorder, locations);
896
952
 
897
953
  heap_record *new_or_existing_record = NULL; // Will be set inside update_heap_record_entry_with_new_allocation
898
- bool existing = st_update(heap_recorder->heap_records, (st_data_t) stack, update_heap_record_entry_with_new_allocation, (st_data_t) &new_or_existing_record);
954
+ int existing = st_update(heap_recorder->heap_records, (st_data_t) stack, update_heap_record_entry_with_new_allocation, (st_data_t) &new_or_existing_record);
899
955
  if (existing) {
900
956
  heap_record_free(heap_recorder, stack, true);
901
957
  }
@@ -940,15 +996,16 @@ static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, obj
940
996
  // =================
941
997
  // Object Record API
942
998
  // =================
943
- object_record* object_record_new(long obj_id, heap_record *heap_record, live_object_data object_data) {
999
+ static object_record* object_record_new(pending_recording pending) {
944
1000
  object_record *record = calloc(1, sizeof(object_record)); // See "note on calloc vs ruby_xcalloc use" above
945
- record->obj_id = obj_id;
946
- record->heap_record = heap_record;
947
- record->object_data = object_data;
1001
+ record->record_id = pending.record_id;
1002
+ // Link the object record with the corresponding heap record
1003
+ record->heap_record = pending.heap_record;
1004
+ record->object_data = pending.object_data;
948
1005
  return record;
949
1006
  }
950
1007
 
951
- void object_record_free(heap_recorder *recorder, object_record *record, bool should_unintern) {
1008
+ static void object_record_free(heap_recorder *recorder, object_record *record, bool should_unintern) {
952
1009
  // When tearing down the whole recorder state, we skip uninterning as it's not needed (the managed
953
1010
  // string table is going to be destroyed anyway) and if there's any failures we can't raise
954
1011
  // in the middle of a dfree callback.
@@ -957,13 +1014,13 @@ void object_record_free(heap_recorder *recorder, object_record *record, bool sho
957
1014
  free(record); // See "note on calloc vs ruby_xcalloc use" above
958
1015
  }
959
1016
 
960
- VALUE object_record_inspect(heap_recorder *recorder, object_record *record) {
1017
+ static VALUE object_record_inspect(heap_recorder *recorder, object_record *record) {
961
1018
  heap_frame top_frame = record->heap_record->frames[0];
962
1019
  VALUE filename = get_ruby_string_or_raise(recorder, top_frame.filename);
963
1020
  live_object_data object_data = record->object_data;
964
1021
 
965
- VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%"PRIsVALUE":%d alloc_gen=%zu gen_age=%zu frozen=%d ",
966
- record->obj_id, object_data.weight, object_data.size, filename,
1022
+ VALUE inspect = rb_sprintf("record_id=%ld weight=%d size=%zu location=%"PRIsVALUE":%d alloc_gen=%zu gen_age=%zu frozen=%d ",
1023
+ record->record_id, object_data.weight, object_data.size, filename,
967
1024
  (int) top_frame.line, object_data.alloc_gen, object_data.gen_age, object_data.is_frozen);
968
1025
 
969
1026
  if (record->object_data.class.value > 0) {
@@ -971,9 +1028,9 @@ VALUE object_record_inspect(heap_recorder *recorder, object_record *record) {
971
1028
 
972
1029
  rb_str_catf(inspect, "class=%"PRIsVALUE" ", class);
973
1030
  }
974
- VALUE ref;
975
1031
 
976
- if (!ruby_ref_from_id(LONG2NUM(record->obj_id), &ref)) {
1032
+ VALUE ref = ruby_weak_map_get_may_lose_gvl(recorder->weak_objects, LONG2FIX(record->record_id));
1033
+ if (ref == Qnil) {
977
1034
  rb_str_catf(inspect, "object=<invalid>");
978
1035
  } else {
979
1036
  rb_str_catf(inspect, "value=%p ", (void *) ref);
@@ -991,7 +1048,7 @@ VALUE object_record_inspect(heap_recorder *recorder, object_record *record) {
991
1048
  // ==============
992
1049
  // Heap Record API
993
1050
  // ==============
994
- heap_record* heap_record_new(heap_recorder *recorder, ddog_prof_Slice_Location locations) {
1051
+ static heap_record* heap_record_new(heap_recorder *recorder, ddog_prof_Slice_Location locations) {
995
1052
  uint16_t frames_len = locations.len;
996
1053
  if (frames_len > MAX_FRAMES_LIMIT) {
997
1054
  // This is not expected as MAX_FRAMES_LIMIT is shared with the stacktrace construction mechanism
@@ -1025,7 +1082,7 @@ heap_record* heap_record_new(heap_recorder *recorder, ddog_prof_Slice_Location l
1025
1082
  return stack;
1026
1083
  }
1027
1084
 
1028
- void heap_record_free(heap_recorder *recorder, heap_record *stack, bool should_unintern) {
1085
+ static void heap_record_free(heap_recorder *recorder, heap_record *stack, bool should_unintern) {
1029
1086
  // When tearing down the whole recorder state, we skip uninterning as it's not needed (the managed
1030
1087
  // string table is going to be destroyed anyway) and if there's any failures we can't raise
1031
1088
  // in the middle of a dfree callback.
@@ -1045,7 +1102,7 @@ void heap_record_free(heap_recorder *recorder, heap_record *stack, bool should_u
1045
1102
 
1046
1103
  // The entire stack is represented by ids (name, filename) and lines (integers) so we can treat is as just
1047
1104
  // a big string of bytes and compare it all in one go.
1048
- int heap_record_cmp_st(st_data_t key1, st_data_t key2) {
1105
+ static int heap_record_cmp_st(st_data_t key1, st_data_t key2) {
1049
1106
  heap_record *stack1 = (heap_record*) key1;
1050
1107
  heap_record *stack2 = (heap_record*) key2;
1051
1108
 
@@ -1061,7 +1118,7 @@ int heap_record_cmp_st(st_data_t key1, st_data_t key2) {
1061
1118
 
1062
1119
  // The entire stack is represented by ids (name, filename) and lines (integers) so we can treat is as just
1063
1120
  // a big string of bytes and hash it all in one go.
1064
- st_index_t heap_record_hash_st(st_data_t key) {
1121
+ static st_index_t heap_record_hash_st(st_data_t key) {
1065
1122
  heap_record *stack = (heap_record*) key;
1066
1123
  return st_hash(stack->frames, stack->frames_len * sizeof(heap_frame), FNV1_32A_INIT);
1067
1124
  }
@@ -1093,29 +1150,55 @@ static VALUE get_ruby_string_or_raise(heap_recorder *recorder, ddog_prof_Managed
1093
1150
  return ruby_string;
1094
1151
  }
1095
1152
 
1096
- static long obj_id_or_fail(VALUE obj) {
1097
- VALUE ruby_obj_id = rb_obj_id(obj);
1098
- if (!FIXNUM_P(ruby_obj_id)) {
1099
- // Bignum object ids indicate the fixnum range is exhausted - all future IDs will also be bignums.
1100
- // Heap profiling cannot continue.
1101
- raise_error(rb_eRuntimeError, "Heap profiling: bignum object id detected. Heap profiling cannot continue.");
1153
+ static inline double ewma_stat(double previous, double current) {
1154
+ double alpha = 0.3;
1155
+ return (1 - alpha) * previous + alpha * current;
1156
+ }
1157
+
1158
+ VALUE heap_recorder_testonly_is_object_recorded(heap_recorder *heap_recorder, long record_id) {
1159
+ if (heap_recorder == NULL) {
1160
+ raise_error(rb_eArgError, "heap_recorder is NULL");
1161
+ }
1162
+
1163
+ // Check if object records contains an object with this record_id
1164
+ return st_is_member(heap_recorder->object_records, record_id) ? Qtrue : Qfalse;
1165
+ }
1166
+
1167
+ typedef struct {
1168
+ heap_recorder *recorder;
1169
+ VALUE target;
1170
+ VALUE result;
1171
+ } record_id_for_context;
1172
+
1173
+ static int st_object_record_id_for(st_data_t key, DDTRACE_UNUSED st_data_t value, st_data_t extra) {
1174
+ record_id_for_context *context = (record_id_for_context*) extra;
1175
+ long record_id = (long) key;
1176
+
1177
+ VALUE ref = ruby_weak_map_get_may_lose_gvl(context->recorder->weak_objects, LONG2FIX(record_id));
1178
+ if (ref != Qnil && ref == context->target) {
1179
+ context->result = LONG2FIX(record_id);
1180
+ return ST_STOP;
1102
1181
  }
1103
1182
 
1104
- return FIX2LONG(ruby_obj_id);
1183
+ return ST_CONTINUE;
1105
1184
  }
1106
1185
 
1107
- static inline double ewma_stat(double previous, double current) {
1108
- double alpha = 0.3;
1109
- return (1 - alpha) * previous + alpha * current;
1186
+ VALUE heap_recorder_testonly_record_id_for(heap_recorder *heap_recorder, VALUE obj) {
1187
+ // Heap profiling is disabled, so nothing is being tracked
1188
+ if (heap_recorder == NULL) return Qnil;
1189
+
1190
+ record_id_for_context context = (record_id_for_context) {.recorder = heap_recorder, .target = obj, .result = Qnil};
1191
+ st_foreach(heap_recorder->object_records, st_object_record_id_for, (st_data_t) &context);
1192
+
1193
+ return context.result;
1110
1194
  }
1111
1195
 
1112
- VALUE heap_recorder_testonly_is_object_recorded(heap_recorder *heap_recorder, VALUE obj_id) {
1196
+ void heap_recorder_testonly_exhaust_record_ids(heap_recorder *heap_recorder) {
1113
1197
  if (heap_recorder == NULL) {
1114
1198
  raise_error(rb_eArgError, "heap_recorder is NULL");
1115
1199
  }
1116
1200
 
1117
- // Check if object records contains an object with this object_id
1118
- return st_is_member(heap_recorder->object_records, FIX2LONG(obj_id)) ? Qtrue : Qfalse;
1201
+ heap_recorder->next_record_id = RUBY_FIXNUM_MAX;
1119
1202
  }
1120
1203
 
1121
1204
  void heap_recorder_testonly_reset_last_update(heap_recorder *heap_recorder) {