datadog 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -2
- data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +57 -18
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +93 -106
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +8 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +8 -8
- data/ext/datadog_profiling_native_extension/heap_recorder.c +174 -28
- data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
- data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
- data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
- data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/configuration/settings.rb +8 -0
- data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -5
- data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
- data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
- data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
- data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
- data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
- data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
- data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
- data/lib/datadog/appsec/event.rb +24 -0
- data/lib/datadog/appsec/ext.rb +4 -0
- data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
- data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
- data/lib/datadog/appsec/processor/context.rb +109 -0
- data/lib/datadog/appsec/processor.rb +7 -71
- data/lib/datadog/appsec/scope.rb +1 -4
- data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
- data/lib/datadog/appsec/utils.rb +2 -0
- data/lib/datadog/appsec.rb +1 -0
- data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
- data/lib/datadog/core/configuration/settings.rb +12 -0
- data/lib/datadog/core/configuration.rb +1 -3
- data/lib/datadog/core/crashtracking/component.rb +8 -5
- data/lib/datadog/core/environment/yjit.rb +5 -0
- data/lib/datadog/core/remote/transport/http.rb +5 -0
- data/lib/datadog/core/remote/worker.rb +1 -1
- data/lib/datadog/core/runtime/ext.rb +1 -0
- data/lib/datadog/core/runtime/metrics.rb +4 -0
- data/lib/datadog/core/semaphore.rb +35 -0
- data/lib/datadog/core/telemetry/logging.rb +10 -10
- data/lib/datadog/core/transport/ext.rb +1 -0
- data/lib/datadog/core/workers/async.rb +1 -1
- data/lib/datadog/di/code_tracker.rb +11 -13
- data/lib/datadog/di/instrumenter.rb +301 -0
- data/lib/datadog/di/probe.rb +29 -0
- data/lib/datadog/di/probe_builder.rb +7 -1
- data/lib/datadog/di/probe_notification_builder.rb +207 -0
- data/lib/datadog/di/probe_notifier_worker.rb +244 -0
- data/lib/datadog/di/serializer.rb +23 -1
- data/lib/datadog/di/transport.rb +67 -0
- data/lib/datadog/di/utils.rb +39 -0
- data/lib/datadog/di.rb +43 -0
- data/lib/datadog/profiling/collectors/thread_context.rb +9 -11
- data/lib/datadog/profiling/component.rb +1 -0
- data/lib/datadog/profiling/stack_recorder.rb +37 -9
- data/lib/datadog/tracing/component.rb +13 -0
- data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
- data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
- data/lib/datadog/tracing/contrib/faraday/middleware.rb +3 -0
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +5 -2
- data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
- data/lib/datadog/tracing/contrib/http/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
- data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
- data/lib/datadog/tracing/sampling/rule_sampler.rb +6 -4
- data/lib/datadog/tracing/tracer.rb +15 -10
- data/lib/datadog/tracing/transport/http.rb +4 -0
- data/lib/datadog/tracing/workers.rb +1 -1
- data/lib/datadog/tracing/writer.rb +26 -28
- data/lib/datadog/version.rb +1 -1
- metadata +22 -14
@@ -256,21 +256,21 @@ if Datadog::Profiling::NativeExtensionHelpers::CAN_USE_MJIT_HEADER
|
|
256
256
|
create_makefile EXTENSION_NAME
|
257
257
|
else
|
258
258
|
# The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on
|
259
|
-
# the
|
259
|
+
# the datadog-ruby_core_source gem to get access to private VM headers.
|
260
260
|
# This gem ships source code copies of these VM headers for the different Ruby VM versions;
|
261
|
-
# see https://github.com/
|
261
|
+
# see https://github.com/DataDog/datadog-ruby_core_source for details
|
262
262
|
|
263
263
|
create_header
|
264
264
|
|
265
|
-
require "
|
265
|
+
require "datadog/ruby_core_source"
|
266
266
|
dir_config("ruby") # allow user to pass in non-standard core include directory
|
267
267
|
|
268
268
|
# This is a workaround for a weird issue...
|
269
269
|
#
|
270
|
-
# The mkmf tool defines a `with_cppflags` helper that
|
270
|
+
# The mkmf tool defines a `with_cppflags` helper that datadog-ruby_core_source uses. This helper temporarily
|
271
271
|
# replaces `$CPPFLAGS` (aka the C pre-processor [not c++!] flags) with a different set when doing something.
|
272
272
|
#
|
273
|
-
# The
|
273
|
+
# The datadog-ruby_core_source gem uses `with_cppflags` during makefile generation to inject extra headers into the
|
274
274
|
# path. But because `with_cppflags` replaces `$CPPFLAGS`, well, the default `$CPPFLAGS` are not included in the
|
275
275
|
# makefile.
|
276
276
|
#
|
@@ -281,12 +281,12 @@ else
|
|
281
281
|
# `VM_CHECK_MODE=1` when building Ruby will trigger this issue (because somethings in structures the profiler reads
|
282
282
|
# are ifdef'd out using this setting).
|
283
283
|
#
|
284
|
-
# To workaround this issue, we override `with_cppflags` for
|
285
|
-
|
284
|
+
# To workaround this issue, we override `with_cppflags` for datadog-ruby_core_source to still include `$CPPFLAGS`.
|
285
|
+
Datadog::RubyCoreSource.define_singleton_method(:with_cppflags) do |newflags, &block|
|
286
286
|
super("#{newflags} #{$CPPFLAGS}", &block)
|
287
287
|
end
|
288
288
|
|
289
|
-
|
289
|
+
Datadog::RubyCoreSource
|
290
290
|
.create_makefile_with_core(
|
291
291
|
proc do
|
292
292
|
headers_available =
|
@@ -5,6 +5,7 @@
|
|
5
5
|
#include <errno.h>
|
6
6
|
#include "collectors_stack.h"
|
7
7
|
#include "libdatadog_helpers.h"
|
8
|
+
#include "time_helpers.h"
|
8
9
|
|
9
10
|
#if (defined(HAVE_WORKING_RB_GC_FORCE_RECYCLE) && ! defined(NO_SEEN_OBJ_ID_FLAG))
|
10
11
|
#define CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
|
@@ -16,6 +17,16 @@
|
|
16
17
|
// relevant for heap profiles as the great majority should be trivially reclaimed
|
17
18
|
// during the next GC.
|
18
19
|
#define ITERATION_MIN_AGE 1
|
20
|
+
// Copied from https://github.com/ruby/ruby/blob/15135030e5808d527325feaaaf04caeb1b44f8b5/gc/default.c#L725C1-L725C27
|
21
|
+
// to align with Ruby's GC definition of what constitutes an old object which are only
|
22
|
+
// supposed to be reclaimed in major GCs.
|
23
|
+
#define OLD_AGE 3
|
24
|
+
// Wait at least 2 seconds before asking heap recorder to explicitly update itself. Heap recorder
|
25
|
+
// data will only materialize at profile serialization time but updating often helps keep our
|
26
|
+
// heap tracking data small since every GC should get rid of a bunch of temporary objects. The
|
27
|
+
// more we clean up before profile flush, the less work we'll have to do all-at-once when preparing
|
28
|
+
// to flush heap data and holding the GVL which should hopefully help with reducing latency impact.
|
29
|
+
#define MIN_TIME_BETWEEN_HEAP_RECORDER_UPDATES_NS SECONDS_AS_NS(2)
|
19
30
|
|
20
31
|
// A compact representation of a stacktrace frame for a heap allocation.
|
21
32
|
typedef struct {
|
@@ -144,11 +155,18 @@ struct heap_recorder {
|
|
144
155
|
// mutation of the data so iteration can occur without acquiring a lock.
|
145
156
|
// NOTE: Contrary to object_records, this table has no ownership of its data.
|
146
157
|
st_table *object_records_snapshot;
|
147
|
-
//
|
158
|
+
// Are we currently updating or not?
|
159
|
+
bool updating;
|
160
|
+
// The GC gen/epoch/count in which we are updating (or last updated if not currently updating).
|
148
161
|
//
|
149
|
-
// This enables us to calculate the age of
|
150
|
-
//
|
151
|
-
size_t
|
162
|
+
// This enables us to calculate the age of objects considered in the update by comparing it
|
163
|
+
// against an object's alloc_gen.
|
164
|
+
size_t update_gen;
|
165
|
+
// Whether the current update (or last update if not currently updating) is including old
|
166
|
+
// objects or not.
|
167
|
+
bool update_include_old;
|
168
|
+
// When did we do the last update of heap recorder?
|
169
|
+
long last_update_ns;
|
152
170
|
|
153
171
|
// Data for a heap recording that was started but not yet ended
|
154
172
|
recording active_recording;
|
@@ -165,6 +183,21 @@ struct heap_recorder {
|
|
165
183
|
size_t objects_skipped;
|
166
184
|
size_t objects_frozen;
|
167
185
|
} stats_last_update;
|
186
|
+
|
187
|
+
struct stats_lifetime {
|
188
|
+
unsigned long updates_successful;
|
189
|
+
unsigned long updates_skipped_concurrent;
|
190
|
+
unsigned long updates_skipped_gcgen;
|
191
|
+
unsigned long updates_skipped_time;
|
192
|
+
|
193
|
+
double ewma_young_objects_alive;
|
194
|
+
double ewma_young_objects_dead;
|
195
|
+
double ewma_young_objects_skipped; // Note: Here "young" refers to the young update; objects skipped includes non-young objects
|
196
|
+
|
197
|
+
double ewma_objects_alive;
|
198
|
+
double ewma_objects_dead;
|
199
|
+
double ewma_objects_skipped;
|
200
|
+
} stats_lifetime;
|
168
201
|
};
|
169
202
|
|
170
203
|
struct end_heap_allocation_args {
|
@@ -183,6 +216,8 @@ static int st_object_records_debug(st_data_t key, st_data_t value, st_data_t ext
|
|
183
216
|
static int update_object_record_entry(st_data_t*, st_data_t*, st_data_t, int);
|
184
217
|
static void commit_recording(heap_recorder*, heap_record*, recording);
|
185
218
|
static VALUE end_heap_allocation_recording(VALUE end_heap_allocation_args);
|
219
|
+
static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update);
|
220
|
+
static inline double ewma_stat(double previous, double current);
|
186
221
|
|
187
222
|
// ==========================
|
188
223
|
// Heap Recorder External API
|
@@ -280,6 +315,9 @@ void heap_recorder_after_fork(heap_recorder *heap_recorder) {
|
|
280
315
|
if (heap_recorder->object_records_snapshot != NULL) {
|
281
316
|
heap_recorder_finish_iteration(heap_recorder);
|
282
317
|
}
|
318
|
+
|
319
|
+
// Clear lifetime stats since this is essentially a new heap recorder
|
320
|
+
heap_recorder->stats_lifetime = (struct stats_lifetime) {0};
|
283
321
|
}
|
284
322
|
|
285
323
|
void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj, unsigned int weight, ddog_CharSlice *alloc_class) {
|
@@ -394,23 +432,94 @@ static VALUE end_heap_allocation_recording(VALUE end_heap_allocation_args) {
|
|
394
432
|
return Qnil;
|
395
433
|
}
|
396
434
|
|
397
|
-
void
|
435
|
+
void heap_recorder_update_young_objects(heap_recorder *heap_recorder) {
|
398
436
|
if (heap_recorder == NULL) {
|
399
437
|
return;
|
400
438
|
}
|
401
439
|
|
402
|
-
heap_recorder
|
440
|
+
heap_recorder_update(heap_recorder, /* full_update: */ false);
|
441
|
+
}
|
442
|
+
|
443
|
+
static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update) {
|
444
|
+
if (heap_recorder->updating) {
|
445
|
+
if (full_update) rb_raise(rb_eRuntimeError, "BUG: full_update should not be triggered during another update");
|
446
|
+
|
447
|
+
// If we try to update while another update is still running, short-circuit.
|
448
|
+
// NOTE: This runs while holding the GVL. But since updates may be triggered from GC activity, there's still
|
449
|
+
// a chance for updates to be attempted concurrently if scheduling gods so determine.
|
450
|
+
heap_recorder->stats_lifetime.updates_skipped_concurrent++;
|
451
|
+
return;
|
452
|
+
}
|
403
453
|
|
404
454
|
if (heap_recorder->object_records_snapshot != NULL) {
|
405
|
-
//
|
406
|
-
|
455
|
+
// While serialization is happening, it runs without the GVL and uses the object_records_snapshot.
|
456
|
+
// Although we iterate on a snapshot of object_records, these records point to other data that has not been
|
457
|
+
// snapshotted for efficiency reasons (e.g. heap_records). Since updating may invalidate
|
458
|
+
// some of that non-snapshotted data, let's refrain from doing updates during iteration. This also enforces the
|
459
|
+
// semantic that iteration will operate as a point-in-time snapshot.
|
460
|
+
return;
|
407
461
|
}
|
408
462
|
|
463
|
+
size_t current_gc_gen = rb_gc_count();
|
464
|
+
long now_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
|
465
|
+
|
466
|
+
if (!full_update) {
|
467
|
+
if (current_gc_gen == heap_recorder->update_gen) {
|
468
|
+
// Are we still in the same GC gen as last update? If so, skip updating since things should not have
|
469
|
+
// changed significantly since last time.
|
470
|
+
// NOTE: This is mostly a performance decision. I suppose some objects may be cleaned up in intermediate
|
471
|
+
// GC steps and sizes may change. But because we have to iterate through all our tracked
|
472
|
+
// object records to do an update, let's wait until all steps for a particular GC generation
|
473
|
+
// have finished to do so. We may revisit this once we have a better liveness checking mechanism.
|
474
|
+
heap_recorder->stats_lifetime.updates_skipped_gcgen++;
|
475
|
+
return;
|
476
|
+
}
|
477
|
+
|
478
|
+
if (now_ns > 0 && (now_ns - heap_recorder->last_update_ns) < MIN_TIME_BETWEEN_HEAP_RECORDER_UPDATES_NS) {
|
479
|
+
// We did an update not too long ago. Let's skip this one to avoid over-taxing the system.
|
480
|
+
heap_recorder->stats_lifetime.updates_skipped_time++;
|
481
|
+
return;
|
482
|
+
}
|
483
|
+
}
|
484
|
+
|
485
|
+
heap_recorder->updating = true;
|
409
486
|
// Reset last update stats, we'll be building them from scratch during the st_foreach call below
|
410
|
-
heap_recorder->stats_last_update = (struct stats_last_update) {};
|
487
|
+
heap_recorder->stats_last_update = (struct stats_last_update) {0};
|
488
|
+
|
489
|
+
heap_recorder->update_gen = current_gc_gen;
|
490
|
+
heap_recorder->update_include_old = full_update;
|
411
491
|
|
412
492
|
st_foreach(heap_recorder->object_records, st_object_record_update, (st_data_t) heap_recorder);
|
413
493
|
|
494
|
+
heap_recorder->last_update_ns = now_ns;
|
495
|
+
heap_recorder->stats_lifetime.updates_successful++;
|
496
|
+
|
497
|
+
// Lifetime stats updating
|
498
|
+
if (!full_update) {
|
499
|
+
heap_recorder->stats_lifetime.ewma_young_objects_alive = ewma_stat(heap_recorder->stats_lifetime.ewma_young_objects_alive, heap_recorder->stats_last_update.objects_alive);
|
500
|
+
heap_recorder->stats_lifetime.ewma_young_objects_dead = ewma_stat(heap_recorder->stats_lifetime.ewma_young_objects_dead, heap_recorder->stats_last_update.objects_dead);
|
501
|
+
heap_recorder->stats_lifetime.ewma_young_objects_skipped = ewma_stat(heap_recorder->stats_lifetime.ewma_young_objects_skipped, heap_recorder->stats_last_update.objects_skipped);
|
502
|
+
} else {
|
503
|
+
heap_recorder->stats_lifetime.ewma_objects_alive = ewma_stat(heap_recorder->stats_lifetime.ewma_objects_alive, heap_recorder->stats_last_update.objects_alive);
|
504
|
+
heap_recorder->stats_lifetime.ewma_objects_dead = ewma_stat(heap_recorder->stats_lifetime.ewma_objects_dead, heap_recorder->stats_last_update.objects_dead);
|
505
|
+
heap_recorder->stats_lifetime.ewma_objects_skipped = ewma_stat(heap_recorder->stats_lifetime.ewma_objects_skipped, heap_recorder->stats_last_update.objects_skipped);
|
506
|
+
}
|
507
|
+
|
508
|
+
heap_recorder->updating = false;
|
509
|
+
}
|
510
|
+
|
511
|
+
void heap_recorder_prepare_iteration(heap_recorder *heap_recorder) {
|
512
|
+
if (heap_recorder == NULL) {
|
513
|
+
return;
|
514
|
+
}
|
515
|
+
|
516
|
+
if (heap_recorder->object_records_snapshot != NULL) {
|
517
|
+
// we could trivially handle this but we raise to highlight and catch unexpected usages.
|
518
|
+
rb_raise(rb_eRuntimeError, "New heap recorder iteration prepared without the previous one having been finished.");
|
519
|
+
}
|
520
|
+
|
521
|
+
heap_recorder_update(heap_recorder, /* full_update: */ true);
|
522
|
+
|
414
523
|
heap_recorder->object_records_snapshot = st_copy(heap_recorder->object_records);
|
415
524
|
if (heap_recorder->object_records_snapshot == NULL) {
|
416
525
|
rb_raise(rb_eRuntimeError, "Failed to create heap snapshot.");
|
@@ -474,6 +583,19 @@ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder) {
|
|
474
583
|
ID2SYM(rb_intern("last_update_objects_dead")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_dead),
|
475
584
|
ID2SYM(rb_intern("last_update_objects_skipped")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_skipped),
|
476
585
|
ID2SYM(rb_intern("last_update_objects_frozen")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_frozen),
|
586
|
+
|
587
|
+
// Lifetime stats
|
588
|
+
ID2SYM(rb_intern("lifetime_updates_successful")), /* => */ LONG2NUM(heap_recorder->stats_lifetime.updates_successful),
|
589
|
+
ID2SYM(rb_intern("lifetime_updates_skipped_concurrent")), /* => */ LONG2NUM(heap_recorder->stats_lifetime.updates_skipped_concurrent),
|
590
|
+
ID2SYM(rb_intern("lifetime_updates_skipped_gcgen")), /* => */ LONG2NUM(heap_recorder->stats_lifetime.updates_skipped_gcgen),
|
591
|
+
ID2SYM(rb_intern("lifetime_updates_skipped_time")), /* => */ LONG2NUM(heap_recorder->stats_lifetime.updates_skipped_time),
|
592
|
+
ID2SYM(rb_intern("lifetime_ewma_young_objects_alive")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_young_objects_alive),
|
593
|
+
ID2SYM(rb_intern("lifetime_ewma_young_objects_dead")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_young_objects_dead),
|
594
|
+
// Note: Here "young" refers to the young update; objects skipped includes non-young objects
|
595
|
+
ID2SYM(rb_intern("lifetime_ewma_young_objects_skipped")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_young_objects_skipped),
|
596
|
+
ID2SYM(rb_intern("lifetime_ewma_objects_alive")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_objects_alive),
|
597
|
+
ID2SYM(rb_intern("lifetime_ewma_objects_dead")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_objects_dead),
|
598
|
+
ID2SYM(rb_intern("lifetime_ewma_objects_skipped")), /* => */ DBL2NUM(heap_recorder->stats_lifetime.ewma_objects_skipped),
|
477
599
|
};
|
478
600
|
VALUE hash = rb_hash_new();
|
479
601
|
for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(hash, arguments[i], arguments[i+1]);
|
@@ -503,11 +625,14 @@ void heap_recorder_testonly_assert_hash_matches(ddog_prof_Slice_Location locatio
|
|
503
625
|
|
504
626
|
VALUE heap_recorder_testonly_debug(heap_recorder *heap_recorder) {
|
505
627
|
if (heap_recorder == NULL) {
|
506
|
-
|
628
|
+
rb_raise(rb_eArgError, "heap_recorder is NULL");
|
507
629
|
}
|
508
630
|
|
509
631
|
VALUE debug_str = rb_str_new2("object records:\n");
|
510
632
|
st_foreach(heap_recorder->object_records, st_object_records_debug, (st_data_t) debug_str);
|
633
|
+
|
634
|
+
rb_str_catf(debug_str, "state snapshot: %"PRIsVALUE"\n------\n", heap_recorder_state_snapshot(heap_recorder));
|
635
|
+
|
511
636
|
return debug_str;
|
512
637
|
}
|
513
638
|
|
@@ -526,13 +651,6 @@ static int st_object_record_entry_free(DDTRACE_UNUSED st_data_t key, st_data_t v
|
|
526
651
|
return ST_DELETE;
|
527
652
|
}
|
528
653
|
|
529
|
-
// Check to see if an object should not be included in a heap recorder iteration.
|
530
|
-
// This centralizes the checking logic to ensure it's equally applied between
|
531
|
-
// preparation and iteration codepaths.
|
532
|
-
static inline bool should_exclude_from_iteration(object_record *obj_record) {
|
533
|
-
return obj_record->object_data.gen_age < ITERATION_MIN_AGE;
|
534
|
-
}
|
535
|
-
|
536
654
|
static int st_object_record_update(st_data_t key, st_data_t value, st_data_t extra_arg) {
|
537
655
|
long obj_id = (long) key;
|
538
656
|
object_record *record = (object_record*) value;
|
@@ -540,16 +658,20 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
|
|
540
658
|
|
541
659
|
VALUE ref;
|
542
660
|
|
543
|
-
size_t
|
661
|
+
size_t update_gen = recorder->update_gen;
|
544
662
|
size_t alloc_gen = record->object_data.alloc_gen;
|
545
663
|
// Guard against potential overflows given unsigned types here.
|
546
|
-
record->object_data.gen_age = alloc_gen <
|
664
|
+
record->object_data.gen_age = alloc_gen < update_gen ? update_gen - alloc_gen : 0;
|
665
|
+
|
666
|
+
if (record->object_data.gen_age == 0) {
|
667
|
+
// Objects that belong to the current GC gen have not had a chance to be cleaned up yet
|
668
|
+
// and won't show up in the iteration anyway so no point in checking their liveness/sizes.
|
669
|
+
recorder->stats_last_update.objects_skipped++;
|
670
|
+
return ST_CONTINUE;
|
671
|
+
}
|
547
672
|
|
548
|
-
if (
|
549
|
-
//
|
550
|
-
// no point checking for liveness or updating its size, so exit early.
|
551
|
-
// NOTE: This means that there should be an equivalent check during actual
|
552
|
-
// iteration otherwise we'd iterate/expose stale object data.
|
673
|
+
if (!recorder->update_include_old && record->object_data.gen_age >= OLD_AGE) {
|
674
|
+
// The current update is not including old objects but this record is for an old object, skip its update.
|
553
675
|
recorder->stats_last_update.objects_skipped++;
|
554
676
|
return ST_CONTINUE;
|
555
677
|
}
|
@@ -598,7 +720,11 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
|
|
598
720
|
|
599
721
|
#endif
|
600
722
|
|
601
|
-
if (
|
723
|
+
if (
|
724
|
+
recorder->size_enabled &&
|
725
|
+
recorder->update_include_old && // We only update sizes when doing a full update
|
726
|
+
!record->object_data.is_frozen
|
727
|
+
) {
|
602
728
|
// if we were asked to update sizes and this object was not already seen as being frozen,
|
603
729
|
// update size again.
|
604
730
|
record->object_data.size = ruby_obj_memsize_of(ref);
|
@@ -622,10 +748,8 @@ static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t val
|
|
622
748
|
|
623
749
|
const heap_recorder *recorder = context->heap_recorder;
|
624
750
|
|
625
|
-
if (
|
751
|
+
if (record->object_data.gen_age < ITERATION_MIN_AGE) {
|
626
752
|
// Skip objects that should not be included in iteration
|
627
|
-
// NOTE: This matches the short-circuiting condition in st_object_record_update
|
628
|
-
// and prevents iteration over stale objects.
|
629
753
|
return ST_CONTINUE;
|
630
754
|
}
|
631
755
|
|
@@ -1087,3 +1211,25 @@ st_index_t heap_record_key_hash_st(st_data_t key) {
|
|
1087
1211
|
return ddog_location_slice_hash(*record_key->location_slice, FNV1_32A_INIT);
|
1088
1212
|
}
|
1089
1213
|
}
|
1214
|
+
|
1215
|
+
static inline double ewma_stat(double previous, double current) {
|
1216
|
+
double alpha = 0.3;
|
1217
|
+
return (1 - alpha) * previous + alpha * current;
|
1218
|
+
}
|
1219
|
+
|
1220
|
+
VALUE heap_recorder_testonly_is_object_recorded(heap_recorder *heap_recorder, VALUE obj_id) {
|
1221
|
+
if (heap_recorder == NULL) {
|
1222
|
+
rb_raise(rb_eArgError, "heap_recorder is NULL");
|
1223
|
+
}
|
1224
|
+
|
1225
|
+
// Check if object records contains an object with this object_id
|
1226
|
+
return st_is_member(heap_recorder->object_records, FIX2LONG(obj_id)) ? Qtrue : Qfalse;
|
1227
|
+
}
|
1228
|
+
|
1229
|
+
void heap_recorder_testonly_reset_last_update(heap_recorder *heap_recorder) {
|
1230
|
+
if (heap_recorder == NULL) {
|
1231
|
+
rb_raise(rb_eArgError, "heap_recorder is NULL");
|
1232
|
+
}
|
1233
|
+
|
1234
|
+
heap_recorder->last_update_ns = 0;
|
1235
|
+
}
|
@@ -118,6 +118,11 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
|
|
118
118
|
__attribute__((warn_unused_result))
|
119
119
|
int end_heap_allocation_recording_with_rb_protect(heap_recorder *heap_recorder, ddog_prof_Slice_Location locations);
|
120
120
|
|
121
|
+
// Update the heap recorder, **checking young objects only**. The idea here is to align with GC: most young objects never
|
122
|
+
// survive enough GC generations, and thus periodically running this method reduces memory usage (we get rid of
|
123
|
+
// these objects quicker) and hopefully reduces tail latency (because there's less objects at serialization time to check).
|
124
|
+
void heap_recorder_update_young_objects(heap_recorder *heap_recorder);
|
125
|
+
|
121
126
|
// Update the heap recorder to reflect the latest state of the VM and prepare internal structures
|
122
127
|
// for efficient iteration.
|
123
128
|
//
|
@@ -166,3 +171,9 @@ void heap_recorder_testonly_assert_hash_matches(ddog_prof_Slice_Location locatio
|
|
166
171
|
// Returns a Ruby string with a representation of internal data helpful to
|
167
172
|
// troubleshoot issues such as unexpected test failures.
|
168
173
|
VALUE heap_recorder_testonly_debug(heap_recorder *heap_recorder);
|
174
|
+
|
175
|
+
// Check if a given object_id is being tracked or not
|
176
|
+
VALUE heap_recorder_testonly_is_object_recorded(heap_recorder *heap_recorder, VALUE obj_id);
|
177
|
+
|
178
|
+
// Used to ensure that a GC actually triggers an update of the objects
|
179
|
+
void heap_recorder_testonly_reset_last_update(heap_recorder *heap_recorder);
|
@@ -9,7 +9,7 @@ module Datadog
|
|
9
9
|
# Can be set to force rubygems to fail gem installation when profiling extension could not be built
|
10
10
|
ENV_FAIL_INSTALL_IF_MISSING_EXTENSION = "DD_PROFILING_FAIL_INSTALL_IF_MISSING_EXTENSION"
|
11
11
|
|
12
|
-
# The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on
|
12
|
+
# The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on datadog-ruby_core_source
|
13
13
|
CAN_USE_MJIT_HEADER = RUBY_VERSION.start_with?("2.6", "2.7", "3.0.", "3.1.", "3.2.")
|
14
14
|
|
15
15
|
def self.fail_install_if_missing_extension?
|
@@ -13,7 +13,7 @@
|
|
13
13
|
#include RUBY_MJIT_HEADER
|
14
14
|
#else
|
15
15
|
// The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on
|
16
|
-
// the
|
16
|
+
// the datadog-ruby_core_source gem to get access to private VM headers.
|
17
17
|
|
18
18
|
// We can't do anything about warnings in VM headers, so we just use this technique to suppress them.
|
19
19
|
// See https://nelkinda.com/blog/suppress-warnings-in-gcc-and-clang/#d11e364 for details.
|
@@ -219,16 +219,19 @@ static bool ruby_is_obj_with_class(VALUE obj) {
|
|
219
219
|
return false;
|
220
220
|
}
|
221
221
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
}
|
222
|
+
// These two functions are not present in the VM headers, but are public symbols that can be invoked.
|
223
|
+
int rb_objspace_internal_object_p(VALUE obj);
|
224
|
+
const char *rb_obj_info(VALUE obj);
|
226
225
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
226
|
+
VALUE ruby_safe_inspect(VALUE obj) {
|
227
|
+
if (!ruby_is_obj_with_class(obj)) return rb_str_new_cstr("(Not an object)");
|
228
|
+
if (rb_objspace_internal_object_p(obj)) return rb_sprintf("(VM Internal, %s)", rb_obj_info(obj));
|
229
|
+
// @ivoanjo: I saw crashes on Ruby 3.1.4 when trying to #inspect matchdata objects. I'm not entirely sure why this
|
230
|
+
// is needed, but since we only use this method for debug purposes I put in this alternative and decided not to
|
231
|
+
// dig deeper.
|
232
|
+
if (rb_type(obj) == RUBY_T_MATCH) return rb_sprintf("(MatchData, %s)", rb_obj_info(obj));
|
233
|
+
if (rb_respond_to(obj, inspect_id)) return rb_sprintf("%+"PRIsVALUE, obj);
|
234
|
+
if (rb_respond_to(obj, to_s_id)) return rb_sprintf("%"PRIsVALUE, obj);
|
235
|
+
|
236
|
+
return rb_str_new_cstr("(Not inspectable)");
|
234
237
|
}
|
@@ -187,6 +187,7 @@ typedef struct profile_slot {
|
|
187
187
|
struct stack_recorder_state {
|
188
188
|
// Heap recorder instance
|
189
189
|
heap_recorder *heap_recorder;
|
190
|
+
bool heap_clean_after_gc_enabled;
|
190
191
|
|
191
192
|
pthread_mutex_t mutex_slot_one;
|
192
193
|
profile_slot profile_slot_one;
|
@@ -236,16 +237,7 @@ static VALUE _native_new(VALUE klass);
|
|
236
237
|
static void initialize_slot_concurrency_control(struct stack_recorder_state *state);
|
237
238
|
static void initialize_profiles(struct stack_recorder_state *state, ddog_prof_Slice_ValueType sample_types);
|
238
239
|
static void stack_recorder_typed_data_free(void *data);
|
239
|
-
static VALUE _native_initialize(
|
240
|
-
DDTRACE_UNUSED VALUE _self,
|
241
|
-
VALUE recorder_instance,
|
242
|
-
VALUE cpu_time_enabled,
|
243
|
-
VALUE alloc_samples_enabled,
|
244
|
-
VALUE heap_samples_enabled,
|
245
|
-
VALUE heap_size_enabled,
|
246
|
-
VALUE heap_sample_every,
|
247
|
-
VALUE timeline_enabled
|
248
|
-
);
|
240
|
+
static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
|
249
241
|
static VALUE _native_serialize(VALUE self, VALUE recorder_instance);
|
250
242
|
static VALUE ruby_time_from(ddog_Timespec ddprof_time);
|
251
243
|
static void *call_serialize_without_gvl(void *call_args);
|
@@ -270,7 +262,9 @@ static VALUE _native_gc_force_recycle(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
|
270
262
|
static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj);
|
271
263
|
static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance);
|
272
264
|
static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns, long heap_iteration_prep_time_ns, long heap_profile_build_time_ns);
|
273
|
-
|
265
|
+
static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE object_id);
|
266
|
+
static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
267
|
+
static VALUE _native_recorder_after_gc_step(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
|
274
268
|
|
275
269
|
void stack_recorder_init(VALUE profiling_module) {
|
276
270
|
VALUE stack_recorder_class = rb_define_class_under(profiling_module, "StackRecorder", rb_cObject);
|
@@ -287,7 +281,7 @@ void stack_recorder_init(VALUE profiling_module) {
|
|
287
281
|
// https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
|
288
282
|
rb_define_alloc_func(stack_recorder_class, _native_new);
|
289
283
|
|
290
|
-
rb_define_singleton_method(stack_recorder_class, "_native_initialize", _native_initialize,
|
284
|
+
rb_define_singleton_method(stack_recorder_class, "_native_initialize", _native_initialize, -1);
|
291
285
|
rb_define_singleton_method(stack_recorder_class, "_native_serialize", _native_serialize, 1);
|
292
286
|
rb_define_singleton_method(stack_recorder_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
|
293
287
|
rb_define_singleton_method(stack_recorder_class, "_native_stats", _native_stats, 1);
|
@@ -307,6 +301,9 @@ void stack_recorder_init(VALUE profiling_module) {
|
|
307
301
|
_native_gc_force_recycle, 1);
|
308
302
|
rb_define_singleton_method(testing_module, "_native_has_seen_id_flag",
|
309
303
|
_native_has_seen_id_flag, 1);
|
304
|
+
rb_define_singleton_method(testing_module, "_native_is_object_recorded?", _native_is_object_recorded, 2);
|
305
|
+
rb_define_singleton_method(testing_module, "_native_heap_recorder_reset_last_update", _native_heap_recorder_reset_last_update, 1);
|
306
|
+
rb_define_singleton_method(testing_module, "_native_recorder_after_gc_step", _native_recorder_after_gc_step, 1);
|
310
307
|
|
311
308
|
ok_symbol = ID2SYM(rb_intern_const("ok"));
|
312
309
|
error_symbol = ID2SYM(rb_intern_const("error"));
|
@@ -330,6 +327,8 @@ static VALUE _native_new(VALUE klass) {
|
|
330
327
|
// Note: Any exceptions raised from this note until the TypedData_Wrap_Struct call will lead to the state memory
|
331
328
|
// being leaked.
|
332
329
|
|
330
|
+
state->heap_clean_after_gc_enabled = false;
|
331
|
+
|
333
332
|
ddog_prof_Slice_ValueType sample_types = {.ptr = all_value_types, .len = ALL_VALUE_TYPES_COUNT};
|
334
333
|
|
335
334
|
initialize_slot_concurrency_control(state);
|
@@ -411,26 +410,33 @@ static void stack_recorder_typed_data_free(void *state_ptr) {
|
|
411
410
|
ruby_xfree(state);
|
412
411
|
}
|
413
412
|
|
414
|
-
static VALUE _native_initialize(
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
VALUE
|
420
|
-
VALUE
|
421
|
-
VALUE
|
422
|
-
VALUE
|
423
|
-
)
|
413
|
+
static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
|
414
|
+
VALUE options;
|
415
|
+
rb_scan_args(argc, argv, "0:", &options);
|
416
|
+
if (options == Qnil) options = rb_hash_new();
|
417
|
+
|
418
|
+
VALUE recorder_instance = rb_hash_fetch(options, ID2SYM(rb_intern("self_instance")));
|
419
|
+
VALUE cpu_time_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("cpu_time_enabled")));
|
420
|
+
VALUE alloc_samples_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("alloc_samples_enabled")));
|
421
|
+
VALUE heap_samples_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_samples_enabled")));
|
422
|
+
VALUE heap_size_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_size_enabled")));
|
423
|
+
VALUE heap_sample_every = rb_hash_fetch(options, ID2SYM(rb_intern("heap_sample_every")));
|
424
|
+
VALUE timeline_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("timeline_enabled")));
|
425
|
+
VALUE heap_clean_after_gc_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("heap_clean_after_gc_enabled")));
|
426
|
+
|
424
427
|
ENFORCE_BOOLEAN(cpu_time_enabled);
|
425
428
|
ENFORCE_BOOLEAN(alloc_samples_enabled);
|
426
429
|
ENFORCE_BOOLEAN(heap_samples_enabled);
|
427
430
|
ENFORCE_BOOLEAN(heap_size_enabled);
|
428
431
|
ENFORCE_TYPE(heap_sample_every, T_FIXNUM);
|
429
432
|
ENFORCE_BOOLEAN(timeline_enabled);
|
433
|
+
ENFORCE_BOOLEAN(heap_clean_after_gc_enabled);
|
430
434
|
|
431
435
|
struct stack_recorder_state *state;
|
432
436
|
TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
|
433
437
|
|
438
|
+
state->heap_clean_after_gc_enabled = (heap_clean_after_gc_enabled == Qtrue);
|
439
|
+
|
434
440
|
heap_recorder_set_sample_rate(state->heap_recorder, NUM2INT(heap_sample_every));
|
435
441
|
|
436
442
|
uint8_t requested_values_count = ALL_VALUE_TYPES_COUNT -
|
@@ -675,6 +681,13 @@ void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_
|
|
675
681
|
}
|
676
682
|
}
|
677
683
|
|
684
|
+
void recorder_after_gc_step(VALUE recorder_instance) {
|
685
|
+
struct stack_recorder_state *state;
|
686
|
+
TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
|
687
|
+
|
688
|
+
if (state->heap_clean_after_gc_enabled) heap_recorder_update_young_objects(state->heap_recorder);
|
689
|
+
}
|
690
|
+
|
678
691
|
#define MAX_LEN_HEAP_ITERATION_ERROR_MSG 256
|
679
692
|
|
680
693
|
// Heap recorder iteration context allows us access to stack recorder state and profile being serialized
|
@@ -1057,3 +1070,26 @@ static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns,
|
|
1057
1070
|
for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(stats_as_hash, arguments[i], arguments[i+1]);
|
1058
1071
|
return stats_as_hash;
|
1059
1072
|
}
|
1073
|
+
|
1074
|
+
static VALUE _native_is_object_recorded(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE obj_id) {
|
1075
|
+
ENFORCE_TYPE(obj_id, T_FIXNUM);
|
1076
|
+
|
1077
|
+
struct stack_recorder_state *state;
|
1078
|
+
TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
|
1079
|
+
|
1080
|
+
return heap_recorder_testonly_is_object_recorded(state->heap_recorder, obj_id);
|
1081
|
+
}
|
1082
|
+
|
1083
|
+
static VALUE _native_heap_recorder_reset_last_update(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
|
1084
|
+
struct stack_recorder_state *state;
|
1085
|
+
TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
|
1086
|
+
|
1087
|
+
heap_recorder_testonly_reset_last_update(state->heap_recorder);
|
1088
|
+
|
1089
|
+
return Qtrue;
|
1090
|
+
}
|
1091
|
+
|
1092
|
+
static VALUE _native_recorder_after_gc_step(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance) {
|
1093
|
+
recorder_after_gc_step(recorder_instance);
|
1094
|
+
return Qtrue;
|
1095
|
+
}
|
@@ -27,4 +27,5 @@ typedef struct sample_labels {
|
|
27
27
|
void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations, sample_values values, sample_labels labels);
|
28
28
|
void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_CharSlice endpoint);
|
29
29
|
void track_object(VALUE recorder_instance, VALUE new_object, unsigned int sample_weight, ddog_CharSlice *alloc_class);
|
30
|
+
void recorder_after_gc_step(VALUE recorder_instance);
|
30
31
|
VALUE enforce_recorder_instance(VALUE object);
|
@@ -8,7 +8,7 @@ module Datadog
|
|
8
8
|
module LibdatadogExtconfHelpers
|
9
9
|
# Used to make sure the correct gem version gets loaded, as extconf.rb does not get run with "bundle exec" and thus
|
10
10
|
# may see multiple libdatadog versions. See https://github.com/DataDog/dd-trace-rb/pull/2531 for the horror story.
|
11
|
-
LIBDATADOG_VERSION = '~>
|
11
|
+
LIBDATADOG_VERSION = '~> 13.1.0.1.0'
|
12
12
|
|
13
13
|
# Used as an workaround for a limitation with how dynamic linking works in environments where the datadog gem and
|
14
14
|
# libdatadog are moved after the extension gets compiled.
|
@@ -197,6 +197,14 @@ module Datadog
|
|
197
197
|
o.type :bool, nilable: true
|
198
198
|
o.env 'DD_APPSEC_SCA_ENABLED'
|
199
199
|
end
|
200
|
+
|
201
|
+
settings :standalone do
|
202
|
+
option :enabled do |o|
|
203
|
+
o.type :bool
|
204
|
+
o.env 'DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED'
|
205
|
+
o.default false
|
206
|
+
end
|
207
|
+
end
|
200
208
|
end
|
201
209
|
end
|
202
210
|
end
|
@@ -38,11 +38,7 @@ module Datadog
|
|
38
38
|
actions: result.actions
|
39
39
|
}
|
40
40
|
|
41
|
-
|
42
|
-
scope.service_entry_span.set_tag('appsec.blocked', 'true') if result.actions.include?('block')
|
43
|
-
scope.service_entry_span.set_tag('appsec.event', 'true')
|
44
|
-
end
|
45
|
-
|
41
|
+
Datadog::AppSec::Event.tag_and_keep!(scope, result)
|
46
42
|
scope.processor_context.events << event
|
47
43
|
end
|
48
44
|
|