datadog 2.4.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +3 -3
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +57 -18
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +93 -106
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +8 -2
  7. data/ext/datadog_profiling_native_extension/extconf.rb +8 -8
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +174 -28
  9. data/ext/datadog_profiling_native_extension/heap_recorder.h +11 -0
  10. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +1 -1
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -1
  12. data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -11
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +58 -22
  14. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  15. data/ext/libdatadog_api/crashtracker.c +3 -5
  16. data/ext/libdatadog_extconf_helpers.rb +1 -1
  17. data/lib/datadog/appsec/configuration/settings.rb +8 -0
  18. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -5
  19. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +7 -20
  20. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -15
  21. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +6 -18
  22. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +7 -20
  23. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +5 -18
  24. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +3 -1
  25. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +3 -5
  26. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +5 -18
  27. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +6 -10
  28. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +7 -20
  29. data/lib/datadog/appsec/event.rb +24 -0
  30. data/lib/datadog/appsec/ext.rb +4 -0
  31. data/lib/datadog/appsec/monitor/gateway/watcher.rb +3 -5
  32. data/lib/datadog/appsec/monitor/reactive/set_user.rb +7 -20
  33. data/lib/datadog/appsec/processor/context.rb +107 -0
  34. data/lib/datadog/appsec/processor.rb +7 -71
  35. data/lib/datadog/appsec/scope.rb +1 -4
  36. data/lib/datadog/appsec/utils/trace_operation.rb +15 -0
  37. data/lib/datadog/appsec/utils.rb +2 -0
  38. data/lib/datadog/appsec.rb +1 -0
  39. data/lib/datadog/core/configuration/agent_settings_resolver.rb +26 -25
  40. data/lib/datadog/core/configuration/settings.rb +12 -0
  41. data/lib/datadog/core/configuration.rb +1 -3
  42. data/lib/datadog/core/crashtracking/component.rb +8 -5
  43. data/lib/datadog/core/environment/yjit.rb +5 -0
  44. data/lib/datadog/core/remote/transport/http.rb +5 -0
  45. data/lib/datadog/core/remote/worker.rb +1 -1
  46. data/lib/datadog/core/runtime/ext.rb +1 -0
  47. data/lib/datadog/core/runtime/metrics.rb +4 -0
  48. data/lib/datadog/core/semaphore.rb +35 -0
  49. data/lib/datadog/core/telemetry/logging.rb +10 -10
  50. data/lib/datadog/core/transport/ext.rb +1 -0
  51. data/lib/datadog/core/workers/async.rb +1 -1
  52. data/lib/datadog/di/code_tracker.rb +11 -13
  53. data/lib/datadog/di/instrumenter.rb +301 -0
  54. data/lib/datadog/di/probe.rb +29 -0
  55. data/lib/datadog/di/probe_builder.rb +7 -1
  56. data/lib/datadog/di/probe_notification_builder.rb +207 -0
  57. data/lib/datadog/di/probe_notifier_worker.rb +244 -0
  58. data/lib/datadog/di/serializer.rb +23 -1
  59. data/lib/datadog/di/transport.rb +67 -0
  60. data/lib/datadog/di/utils.rb +39 -0
  61. data/lib/datadog/di.rb +43 -0
  62. data/lib/datadog/profiling/collectors/thread_context.rb +9 -11
  63. data/lib/datadog/profiling/component.rb +1 -0
  64. data/lib/datadog/profiling/stack_recorder.rb +37 -9
  65. data/lib/datadog/tracing/component.rb +13 -0
  66. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -0
  67. data/lib/datadog/tracing/contrib/excon/middleware.rb +3 -0
  68. data/lib/datadog/tracing/contrib/faraday/middleware.rb +3 -0
  69. data/lib/datadog/tracing/contrib/grape/endpoint.rb +5 -2
  70. data/lib/datadog/tracing/contrib/http/circuit_breaker.rb +9 -0
  71. data/lib/datadog/tracing/contrib/http/instrumentation.rb +4 -0
  72. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +4 -0
  73. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +4 -0
  74. data/lib/datadog/tracing/contrib/rails/runner.rb +1 -1
  75. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +3 -0
  76. data/lib/datadog/tracing/sampling/rule_sampler.rb +6 -4
  77. data/lib/datadog/tracing/tracer.rb +15 -10
  78. data/lib/datadog/tracing/transport/http.rb +4 -0
  79. data/lib/datadog/tracing/workers.rb +1 -1
  80. data/lib/datadog/tracing/writer.rb +26 -28
  81. data/lib/datadog/version.rb +1 -1
  82. 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 debase-ruby_core_source gem to get access to private VM headers.
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/ruby-debug/debase-ruby_core_source for details
261
+ # see https://github.com/DataDog/datadog-ruby_core_source for details
262
262
 
263
263
  create_header
264
264
 
265
- require "debase/ruby_core_source"
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 debase-ruby_core_source uses. This helper temporarily
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 debase-ruby_core_source gem uses `with_cppflags` during makefile generation to inject extra headers into 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 debase-ruby_core_source to still include `$CPPFLAGS`.
285
- Debase::RubyCoreSource.define_singleton_method(:with_cppflags) do |newflags, &block|
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
- Debase::RubyCoreSource
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
- // The GC gen/epoch/count in which we prepared the current iteration.
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 iterated objects in the above snapshot by
150
- // comparing it against an object's alloc_gen.
151
- size_t iteration_gen;
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 heap_recorder_prepare_iteration(heap_recorder *heap_recorder) {
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->iteration_gen = rb_gc_count();
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
- // we could trivially handle this but we raise to highlight and catch unexpected usages.
406
- rb_raise(rb_eRuntimeError, "New heap recorder iteration prepared without the previous one having been finished.");
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
- return rb_str_new2("NULL heap_recorder");
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 iteration_gen = recorder->iteration_gen;
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 < iteration_gen ? iteration_gen - alloc_gen : 0;
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 (should_exclude_from_iteration(record)) {
549
- // If an object won't be included in the current iteration, there's
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 (recorder->size_enabled && !record->object_data.is_frozen) {
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 (should_exclude_from_iteration(record)) {
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 debase-ruby_core_source
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 debase-ruby_core_source gem to get access to private VM headers.
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
- VALUE ruby_safe_inspect(VALUE obj) {
223
- if (!ruby_is_obj_with_class(obj)) {
224
- return rb_str_new_cstr("(Not an object)");
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
- if (rb_respond_to(obj, inspect_id)) {
228
- return rb_sprintf("%+"PRIsVALUE, obj);
229
- } else if (rb_respond_to(obj, to_s_id)) {
230
- return rb_sprintf("%"PRIsVALUE, obj);
231
- } else {
232
- return rb_str_new_cstr("(Not inspectable)");
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, 7);
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
- DDTRACE_UNUSED VALUE _self,
416
- VALUE recorder_instance,
417
- VALUE cpu_time_enabled,
418
- VALUE alloc_samples_enabled,
419
- VALUE heap_samples_enabled,
420
- VALUE heap_size_enabled,
421
- VALUE heap_sample_every,
422
- VALUE timeline_enabled
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);
@@ -67,12 +67,10 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
67
67
  // The Ruby crash handler also seems to get confused when this option is enabled and
68
68
  // "Process.kill('SEGV', Process.pid)" gets run.
69
69
  .create_alt_stack = false,
70
+ .use_alt_stack = true, // NOTE: This is a no-op in libdatadog 14.0; should be fixed in a future version
70
71
  .endpoint = endpoint,
71
72
  .resolve_frames = DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_SYMBOLS_IN_RECEIVER,
72
- .timeout_secs = FIX2INT(upload_timeout_seconds),
73
- // Waits for crash tracker to finish reporting the issue before letting the Ruby process die; see
74
- // https://github.com/DataDog/libdatadog/pull/477 for details
75
- .wait_for_receiver = true,
73
+ .timeout_ms = FIX2INT(upload_timeout_seconds) * 1000,
76
74
  };
77
75
 
78
76
  ddog_crasht_Metadata metadata = {
@@ -97,7 +95,7 @@ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUS
97
95
 
98
96
  ddog_crasht_Result result =
99
97
  action == start_action ?
100
- ddog_crasht_init_with_receiver(config, receiver_config, metadata) :
98
+ ddog_crasht_init(config, receiver_config, metadata) :
101
99
  ddog_crasht_update_on_fork(config, receiver_config, metadata);
102
100
 
103
101
  // Clean up before potentially raising any exceptions
@@ -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 = '~> 12.0.0.1.0'
11
+ LIBDATADOG_VERSION = '~> 14.0.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
- if scope.service_entry_span
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