datadog 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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