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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -2
- data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
- data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
- data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
- data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
- data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
- data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
- data/ext/datadog_profiling_native_extension/profiling.c +2 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
- data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
- data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
- data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
- data/ext/libdatadog_api/di.c +10 -0
- data/ext/libdatadog_api/extconf.rb +3 -0
- data/ext/libdatadog_api/init.c +2 -0
- data/ext/libdatadog_api/otel_thread_context.c +232 -0
- data/ext/libdatadog_api/otel_thread_context.h +5 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/assets/blocked.html +1 -108
- data/lib/datadog/core/configuration/components.rb +1 -0
- data/lib/datadog/core/crashtracking/component.rb +5 -1
- data/lib/datadog/data_streams/pathway_context.rb +20 -22
- data/lib/datadog/data_streams/processor.rb +31 -0
- data/lib/datadog/di/instrumenter.rb +41 -1
- data/lib/datadog/di/logger.rb +2 -2
- data/lib/datadog/di/probe.rb +9 -1
- data/lib/datadog/di/probe_notification_builder.rb +1 -0
- data/lib/datadog/di/remote.rb +3 -3
- data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
- data/lib/datadog/open_feature/exposures/event.rb +10 -3
- data/lib/datadog/open_feature/ext.rb +19 -0
- data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
- data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
- data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
- data/lib/datadog/open_feature/native_evaluator.rb +33 -6
- data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
- data/lib/datadog/open_feature/provider.rb +11 -2
- data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
- data/lib/datadog/opentelemetry/trace.rb +3 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
- data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
- data/lib/datadog/profiling/component.rb +8 -16
- data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
- data/lib/datadog/tracing/distributed/baggage.rb +0 -1
- data/lib/datadog/tracing/distributed/datadog.rb +3 -3
- data/lib/datadog/tracing/distributed/propagation.rb +3 -0
- data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
- data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
- data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
- data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
- data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
- data/lib/datadog/tracing/otel_thread_context.rb +30 -0
- data/lib/datadog/tracing/remote.rb +195 -27
- data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
- data/lib/datadog/tracing/trace_digest.rb +22 -4
- data/lib/datadog/tracing/trace_operation.rb +22 -10
- data/lib/datadog/tracing/tracer.rb +5 -5
- data/lib/datadog/version.rb +1 -1
- metadata +14 -8
- 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
|
-
//
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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
|
-
|
|
83
|
+
long record_id;
|
|
86
84
|
heap_record *heap_record;
|
|
87
85
|
live_object_data object_data;
|
|
88
|
-
}
|
|
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[
|
|
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
|
-
//
|
|
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[
|
|
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
|
-
//
|
|
128
|
-
|
|
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
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
//
|
|
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
|
|
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
|
-
|
|
194
|
-
}
|
|
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 *,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
return;
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
576
|
+
return Qnil;
|
|
505
577
|
}
|
|
506
578
|
|
|
507
|
-
// Mark
|
|
508
|
-
//
|
|
509
|
-
|
|
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->
|
|
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 (
|
|
527
|
-
if
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
765
|
-
|
|
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,
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
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(
|
|
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->
|
|
946
|
-
record
|
|
947
|
-
record->
|
|
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("
|
|
966
|
-
record->
|
|
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
|
-
|
|
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
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
|
1183
|
+
return ST_CONTINUE;
|
|
1105
1184
|
}
|
|
1106
1185
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|