datadog 2.18.0 → 2.19.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +51 -10
  4. data/ext/datadog_profiling_native_extension/collectors_stack.c +58 -49
  5. data/ext/datadog_profiling_native_extension/collectors_stack.h +2 -1
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +5 -6
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +1 -1
  8. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +37 -26
  9. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +0 -1
  10. data/ext/datadog_profiling_native_extension/ruby_helpers.h +1 -1
  11. data/lib/datadog/appsec/api_security/route_extractor.rb +7 -1
  12. data/lib/datadog/appsec/instrumentation/gateway/argument.rb +1 -1
  13. data/lib/datadog/core/configuration/settings.rb +20 -0
  14. data/lib/datadog/core/telemetry/component.rb +8 -4
  15. data/lib/datadog/core/telemetry/event/app_started.rb +21 -3
  16. data/lib/datadog/di/instrumenter.rb +11 -18
  17. data/lib/datadog/di/probe_notification_builder.rb +21 -16
  18. data/lib/datadog/di/serializer.rb +6 -2
  19. data/lib/datadog/di.rb +0 -6
  20. data/lib/datadog/kit/appsec/events/v2.rb +195 -0
  21. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -0
  22. data/lib/datadog/profiling/collectors/info.rb +41 -0
  23. data/lib/datadog/profiling/component.rb +1 -0
  24. data/lib/datadog/profiling/exporter.rb +9 -3
  25. data/lib/datadog/profiling/sequence_tracker.rb +44 -0
  26. data/lib/datadog/profiling/tag_builder.rb +2 -0
  27. data/lib/datadog/profiling.rb +1 -0
  28. data/lib/datadog/single_step_instrument.rb +9 -0
  29. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +7 -1
  30. data/lib/datadog/tracing/contrib/active_support/configuration/settings.rb +13 -0
  31. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +16 -6
  32. data/lib/datadog/tracing/contrib/rails/patcher.rb +4 -1
  33. data/lib/datadog/tracing/contrib/rails/runner.rb +61 -40
  34. data/lib/datadog/tracing/diagnostics/environment_logger.rb +3 -1
  35. data/lib/datadog/tracing/span_event.rb +1 -1
  36. data/lib/datadog/tracing/span_operation.rb +22 -0
  37. data/lib/datadog/version.rb +1 -1
  38. data/lib/datadog.rb +7 -0
  39. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a05fd61c1cac65f510b1e6d180ce2bc0552d63218f4fb837284ab9e509b071cb
4
- data.tar.gz: 29b4e928bb96d57d3fa4820e91ec030ea21973868cfdb906ebce5304c0955f22
3
+ metadata.gz: 9c533682b9a96989e1ad8d4eb96339af4bffdb5fd9cbfe447bd0a034bc387c03
4
+ data.tar.gz: 5d7808aa6b7fd5f9c68e453fef4fff8c5345c62fb26a7f7839d630ec0da2fe9f
5
5
  SHA512:
6
- metadata.gz: 9590ee74f91ecf5cf304627790069b6347f63ae050acfc6d79ad29d9155b5e8dde3243a47049eed5c023ad2b1dd6dc78ff4a0813d16d942d83df2db981b5511c
7
- data.tar.gz: 22892adf3a7520629a4907738aa31125f00a97cdf263fb2edc85568f86096031de8038e78b6933e3a859d0f52aff3b67955e00109bbdc18a2e7e2d3f66602497
6
+ metadata.gz: 6b35d7ef1ce2f9e565727037f9e5b329d019202ff20c01a15bb5096cf14253b3ab9812d4fc84fc23e265ec2e7e0096e34acc6a845ad510b215ef7df4093107e7
7
+ data.tar.gz: 72fff247b51de201e4373638627fa419a3a679979f880dfa44c73d1c9a7c062991746045d41cbe5fa47d824b025fb9cad2f34c0139e034df1b85ddb4a07bccd9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.19.0] - 2025-07-24
6
+
7
+ ### Added
8
+
9
+ * AppSec: Added Business Logic Events SDK v2. ([#4802][])
10
+ * Tracing: Add `record_exception` API to capture and attach error information to spans via span events. ([#4771][])
11
+ * Tracing: Add `:cache_store` option to ActiveSupport integration to allow tracing only specified cache backends. ([#4693][])
12
+ * SSI: Rework SSI from the ground up. ([#4366][])
13
+
14
+ ### Changed
15
+
16
+ * Profiling: Switch profiler stack truncation strategy and improve sampling performance ([#4819][])
17
+ * Profiling: Report GC tuning environment variables with profiles ([#4813][])
18
+ * Profiling: Tag profiles with sequence number ([#4794][])
19
+ * Profiling: Enable sample from inside signal handler by default on modern Rubies ([#4786][], [#4785][])
20
+
21
+ ### Fixed
22
+
23
+ * Core: Fix emitting duplicate warnings on agent configuration mismatch ([#4814][])
24
+ * Appsec: Fix an error in AppSec route extractor for not-found routes in Rails 8 ([#4793][])
25
+ * Profiling: Add workaround for Ruby VM bug ([#4787][])
26
+ * Profiling: Fix checking for dladdr in profiling ([#4783][])
27
+ * Profiling: Fix potential profiler compilation issue. ([#4783][])
28
+ * Tracing: The mysql integration now only sets the `db.name` tag if there is a valid value ([#4776][])
29
+ * Tracing: The Rails Runner instrumentation should now create Rails Runner spans. ([#4681][])
30
+ * Tracing: Fix sampling rules and sample rate reporting in environment logger. ([#4772][])
31
+
32
+ ### Removed
33
+
5
34
  ## [2.18.0] - 2025-07-03
6
35
 
7
36
  ### Added
@@ -3268,7 +3297,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
3268
3297
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
3269
3298
 
3270
3299
 
3271
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.18.0...master
3300
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.19.0...master
3301
+ [2.19.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.18.0...v2.19.0
3272
3302
  [2.18.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.17.0...v2.18.0
3273
3303
  [2.17.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.16.0...v2.17.0
3274
3304
  [2.16.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.15.0...v2.16.0
@@ -4770,6 +4800,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
4770
4800
  [#4353]: https://github.com/DataDog/dd-trace-rb/issues/4353
4771
4801
  [#4360]: https://github.com/DataDog/dd-trace-rb/issues/4360
4772
4802
  [#4363]: https://github.com/DataDog/dd-trace-rb/issues/4363
4803
+ [#4366]: https://github.com/DataDog/dd-trace-rb/issues/4366
4773
4804
  [#4391]: https://github.com/DataDog/dd-trace-rb/issues/4391
4774
4805
  [#4398]: https://github.com/DataDog/dd-trace-rb/issues/4398
4775
4806
  [#4399]: https://github.com/DataDog/dd-trace-rb/issues/4399
@@ -4827,7 +4858,9 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
4827
4858
  [#4673]: https://github.com/DataDog/dd-trace-rb/issues/4673
4828
4859
  [#4678]: https://github.com/DataDog/dd-trace-rb/issues/4678
4829
4860
  [#4679]: https://github.com/DataDog/dd-trace-rb/issues/4679
4861
+ [#4681]: https://github.com/DataDog/dd-trace-rb/issues/4681
4830
4862
  [#4688]: https://github.com/DataDog/dd-trace-rb/issues/4688
4863
+ [#4693]: https://github.com/DataDog/dd-trace-rb/issues/4693
4831
4864
  [#4697]: https://github.com/DataDog/dd-trace-rb/issues/4697
4832
4865
  [#4699]: https://github.com/DataDog/dd-trace-rb/issues/4699
4833
4866
  [#4718]: https://github.com/DataDog/dd-trace-rb/issues/4718
@@ -4838,6 +4871,19 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
4838
4871
  [#4745]: https://github.com/DataDog/dd-trace-rb/issues/4745
4839
4872
  [#4756]: https://github.com/DataDog/dd-trace-rb/issues/4756
4840
4873
  [#4757]: https://github.com/DataDog/dd-trace-rb/issues/4757
4874
+ [#4771]: https://github.com/DataDog/dd-trace-rb/issues/4771
4875
+ [#4772]: https://github.com/DataDog/dd-trace-rb/issues/4772
4876
+ [#4776]: https://github.com/DataDog/dd-trace-rb/issues/4776
4877
+ [#4783]: https://github.com/DataDog/dd-trace-rb/issues/4783
4878
+ [#4785]: https://github.com/DataDog/dd-trace-rb/issues/4785
4879
+ [#4786]: https://github.com/DataDog/dd-trace-rb/issues/4786
4880
+ [#4787]: https://github.com/DataDog/dd-trace-rb/issues/4787
4881
+ [#4793]: https://github.com/DataDog/dd-trace-rb/issues/4793
4882
+ [#4794]: https://github.com/DataDog/dd-trace-rb/issues/4794
4883
+ [#4802]: https://github.com/DataDog/dd-trace-rb/issues/4802
4884
+ [#4813]: https://github.com/DataDog/dd-trace-rb/issues/4813
4885
+ [#4814]: https://github.com/DataDog/dd-trace-rb/issues/4814
4886
+ [#4819]: https://github.com/DataDog/dd-trace-rb/issues/4819
4841
4887
  [@AdrianLC]: https://github.com/AdrianLC
4842
4888
  [@Azure7111]: https://github.com/Azure7111
4843
4889
  [@BabyGroot]: https://github.com/BabyGroot
@@ -102,6 +102,7 @@ typedef struct {
102
102
  bool allocation_counting_enabled;
103
103
  bool gvl_profiling_enabled;
104
104
  bool skip_idle_samples_for_testing;
105
+ bool sighandler_sampling_enabled;
105
106
  VALUE self_instance;
106
107
  VALUE thread_context_collector_instance;
107
108
  VALUE idle_sampling_helper_instance;
@@ -142,8 +143,10 @@ typedef struct {
142
143
  unsigned int trigger_simulated_signal_delivery_attempts;
143
144
  // How many times we actually simulated signal delivery
144
145
  unsigned int simulated_signal_delivery;
145
- // How many times we actually called rb_postponed_job_register_one from a signal handler
146
+ // How many times we actually called rb_postponed_job_register_one from the signal handler
146
147
  unsigned int signal_handler_enqueued_sample;
148
+ // How many times we prepared a sample (sampled directly) from the signal handler
149
+ unsigned int signal_handler_prepared_sample;
147
150
  // How many times the signal handler was called from the wrong thread
148
151
  unsigned int signal_handler_wrong_thread;
149
152
  // How many times we actually tried to interrupt a thread for sampling
@@ -232,6 +235,8 @@ static void after_gvl_running_from_postponed_job(DDTRACE_UNUSED void *_unused);
232
235
  #endif
233
236
  static VALUE rescued_after_gvl_running_from_postponed_job(VALUE self_instance);
234
237
  static VALUE _native_gvl_profiling_hook_active(DDTRACE_UNUSED VALUE self, VALUE instance);
238
+ static inline void during_sample_enter(cpu_and_wall_time_worker_state* state);
239
+ static inline void during_sample_exit(cpu_and_wall_time_worker_state* state);
235
240
 
236
241
  // We're using `on_newobj_event` function with `rb_add_event_hook2`, which requires in its public signature a function
237
242
  // with signature `rb_event_hook_func_t` which doesn't match `on_newobj_event`.
@@ -356,6 +361,7 @@ static VALUE _native_new(VALUE klass) {
356
361
  state->allocation_counting_enabled = false;
357
362
  state->gvl_profiling_enabled = false;
358
363
  state->skip_idle_samples_for_testing = false;
364
+ state->sighandler_sampling_enabled = false;
359
365
  state->thread_context_collector_instance = Qnil;
360
366
  state->idle_sampling_helper_instance = Qnil;
361
367
  state->owner_thread = Qnil;
@@ -366,7 +372,7 @@ static VALUE _native_new(VALUE klass) {
366
372
  state->failure_exception = Qnil;
367
373
  state->stop_thread = Qnil;
368
374
 
369
- state->during_sample = false;
375
+ during_sample_exit(state);
370
376
 
371
377
  #ifndef NO_GVL_INSTRUMENTATION
372
378
  state->gvl_profiling_hook = NULL;
@@ -398,6 +404,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
398
404
  VALUE allocation_counting_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("allocation_counting_enabled")));
399
405
  VALUE gvl_profiling_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("gvl_profiling_enabled")));
400
406
  VALUE skip_idle_samples_for_testing = rb_hash_fetch(options, ID2SYM(rb_intern("skip_idle_samples_for_testing")));
407
+ VALUE sighandler_sampling_enabled = rb_hash_fetch(options, ID2SYM(rb_intern("sighandler_sampling_enabled")));
401
408
 
402
409
  ENFORCE_BOOLEAN(gc_profiling_enabled);
403
410
  ENFORCE_BOOLEAN(no_signals_workaround_enabled);
@@ -407,6 +414,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
407
414
  ENFORCE_BOOLEAN(allocation_counting_enabled);
408
415
  ENFORCE_BOOLEAN(gvl_profiling_enabled);
409
416
  ENFORCE_BOOLEAN(skip_idle_samples_for_testing)
417
+ ENFORCE_BOOLEAN(sighandler_sampling_enabled)
410
418
 
411
419
  cpu_and_wall_time_worker_state *state;
412
420
  TypedData_Get_Struct(self_instance, cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
@@ -418,6 +426,7 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
418
426
  state->allocation_counting_enabled = (allocation_counting_enabled == Qtrue);
419
427
  state->gvl_profiling_enabled = (gvl_profiling_enabled == Qtrue);
420
428
  state->skip_idle_samples_for_testing = (skip_idle_samples_for_testing == Qtrue);
429
+ state->sighandler_sampling_enabled = (sighandler_sampling_enabled == Qtrue);
421
430
 
422
431
  double total_overhead_target_percentage = NUM2DBL(dynamic_sampling_rate_overhead_target_percentage);
423
432
  if (!state->allocation_profiling_enabled) {
@@ -590,6 +599,19 @@ static void handle_sampling_signal(DDTRACE_UNUSED int _signal, DDTRACE_UNUSED si
590
599
 
591
600
  state->stats.signal_handler_enqueued_sample++;
592
601
 
602
+ bool sample_from_signal_handler =
603
+ state->sighandler_sampling_enabled &&
604
+ // Don't sample if we're already in the middle of processing a sample
605
+ !state->during_sample;
606
+
607
+ if (sample_from_signal_handler) {
608
+ // Buffer current stack trace. Note that this will not actually record the sample, for that we still need to wait
609
+ // until the postponed job below gets run.
610
+ bool prepared = thread_context_collector_prepare_sample_inside_signal_handler(state->thread_context_collector_instance);
611
+
612
+ if (prepared) state->stats.signal_handler_prepared_sample++;
613
+ }
614
+
593
615
  #ifndef NO_POSTPONED_TRIGGER // Ruby 3.3+
594
616
  rb_postponed_job_trigger(sample_from_postponed_job_handle);
595
617
  #else
@@ -701,12 +723,12 @@ static void sample_from_postponed_job(DDTRACE_UNUSED void *_unused) {
701
723
  return; // We're not on the main Ractor; we currently don't support profiling non-main Ractors
702
724
  }
703
725
 
704
- state->during_sample = true;
726
+ during_sample_enter(state);
705
727
 
706
728
  // Rescue against any exceptions that happen during sampling
707
729
  safely_call(rescued_sample_from_postponed_job, state->self_instance, state->self_instance);
708
730
 
709
- state->during_sample = false;
731
+ during_sample_exit(state);
710
732
  }
711
733
 
712
734
  static VALUE rescued_sample_from_postponed_job(VALUE self_instance) {
@@ -912,11 +934,11 @@ static void after_gc_from_postponed_job(DDTRACE_UNUSED void *_unused) {
912
934
  return; // We're not on the main Ractor; we currently don't support profiling non-main Ractors
913
935
  }
914
936
 
915
- state->during_sample = true;
937
+ during_sample_enter(state);
916
938
 
917
939
  safely_call(thread_context_collector_sample_after_gc, state->thread_context_collector_instance, state->self_instance);
918
940
 
919
- state->during_sample = false;
941
+ during_sample_exit(state);
920
942
  }
921
943
 
922
944
  // Equivalent to Ruby begin/rescue call, where we call a C function and jump to the exception handler if an
@@ -994,6 +1016,7 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
994
1016
  ID2SYM(rb_intern("trigger_simulated_signal_delivery_attempts")), /* => */ UINT2NUM(state->stats.trigger_simulated_signal_delivery_attempts),
995
1017
  ID2SYM(rb_intern("simulated_signal_delivery")), /* => */ UINT2NUM(state->stats.simulated_signal_delivery),
996
1018
  ID2SYM(rb_intern("signal_handler_enqueued_sample")), /* => */ UINT2NUM(state->stats.signal_handler_enqueued_sample),
1019
+ ID2SYM(rb_intern("signal_handler_prepared_sample")), /* => */ UINT2NUM(state->stats.signal_handler_prepared_sample),
997
1020
  ID2SYM(rb_intern("signal_handler_wrong_thread")), /* => */ UINT2NUM(state->stats.signal_handler_wrong_thread),
998
1021
  ID2SYM(rb_intern("interrupt_thread_attempts")), /* => */ UINT2NUM(state->stats.interrupt_thread_attempts),
999
1022
 
@@ -1177,7 +1200,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1177
1200
  &state->allocation_sampler, HANDLE_CLOCK_FAILURE(monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE))
1178
1201
  );
1179
1202
 
1180
- state->during_sample = true;
1203
+ during_sample_enter(state);
1181
1204
 
1182
1205
  // Rescue against any exceptions that happen during sampling
1183
1206
  safely_call(rescued_sample_allocation, Qnil, state->self_instance);
@@ -1198,7 +1221,7 @@ static void on_newobj_event(DDTRACE_UNUSED VALUE unused1, DDTRACE_UNUSED void *u
1198
1221
 
1199
1222
  state->stats.allocation_sampled++;
1200
1223
 
1201
- state->during_sample = false;
1224
+ during_sample_exit(state);
1202
1225
  }
1203
1226
 
1204
1227
  static void disable_tracepoints(cpu_and_wall_time_worker_state *state) {
@@ -1339,12 +1362,12 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1339
1362
  // This can potentially happen if the CpuAndWallTimeWorker was stopped while the postponed job was waiting to be executed; nothing to do
1340
1363
  if (state == NULL) return;
1341
1364
 
1342
- state->during_sample = true;
1365
+ during_sample_enter(state);
1343
1366
 
1344
1367
  // Rescue against any exceptions that happen during sampling
1345
1368
  safely_call(rescued_after_gvl_running_from_postponed_job, state->self_instance, state->self_instance);
1346
1369
 
1347
- state->during_sample = false;
1370
+ during_sample_exit(state);
1348
1371
  }
1349
1372
 
1350
1373
  static VALUE rescued_after_gvl_running_from_postponed_job(VALUE self_instance) {
@@ -1380,3 +1403,21 @@ static VALUE _native_resume_signals(DDTRACE_UNUSED VALUE self) {
1380
1403
  return Qfalse;
1381
1404
  }
1382
1405
  #endif
1406
+
1407
+ static inline void during_sample_enter(cpu_and_wall_time_worker_state* state) {
1408
+ // Tell the compiler it's not allowed to reorder the `during_sample` flag with anything that happens after.
1409
+ //
1410
+ // In a few cases, we may be checking this flag from a signal handler, so we need to make sure the compiler didn't
1411
+ // get clever and reordered things in such a way that makes us miss the flag update.
1412
+ //
1413
+ // See https://github.com/ruby/ruby/pull/11036 for a similar change made to the Ruby VM with more context.
1414
+ state->during_sample = true;
1415
+ atomic_signal_fence(memory_order_seq_cst);
1416
+ }
1417
+
1418
+ static inline void during_sample_exit(cpu_and_wall_time_worker_state* state) {
1419
+ // See `during_sample_enter` for more context; in this case we set the fence before to make sure anything that
1420
+ // happens before the fence is not reordered with the flag update.
1421
+ atomic_signal_fence(memory_order_seq_cst);
1422
+ state->during_sample = false;
1423
+ }
@@ -1,16 +1,16 @@
1
1
  #include <ruby.h>
2
2
  #include <ruby/debug.h>
3
3
  #include <ruby/st.h>
4
+ #include <stdatomic.h>
4
5
 
5
6
  #include "extconf.h" // This is needed for the HAVE_DLADDR and friends below
6
7
 
7
- // For dladdr/dladdr1
8
- #if defined(HAVE_DLADDR1) || defined(HAVE_DLADDR)
8
+ #if (defined(HAVE_DLADDR1) && HAVE_DLADDR1) || (defined(HAVE_DLADDR) && HAVE_DLADDR)
9
9
  #ifndef _GNU_SOURCE
10
10
  #define _GNU_SOURCE
11
11
  #endif
12
12
  #include <dlfcn.h>
13
- #ifdef HAVE_DLADDR1
13
+ #if defined(HAVE_DLADDR1) && HAVE_DLADDR1
14
14
  #include <link.h>
15
15
  #endif
16
16
  #endif
@@ -39,7 +39,7 @@ static void set_file_info_for_cfunc(
39
39
  st_table *native_filenames_cache
40
40
  );
41
41
  static const char *get_or_compute_native_filename(void *function, st_table *native_filenames_cache);
42
- static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer* buffer, char *frames_omitted_message, int frames_omitted_message_size);
42
+ static void add_truncated_frames_placeholder(sampling_buffer* buffer);
43
43
  static void record_placeholder_stack_in_native_code(VALUE recorder_instance, sample_values values, sample_labels labels);
44
44
  static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_CharSlice *filename_slice);
45
45
 
@@ -63,24 +63,24 @@ void collectors_stack_init(VALUE profiling_module) {
63
63
 
64
64
  rb_define_singleton_method(testing_module, "_native_sample", _native_sample, -1);
65
65
 
66
- #if defined(HAVE_DLADDR1) || defined(HAVE_DLADDR)
67
- // To be able to detect when a frame is coming from Ruby, we record here its filename as returned by dladdr.
68
- // We expect this same pointer to be returned by dladdr for all frames coming from Ruby.
69
- //
70
- // Small note: Creating/deleting the cache is a bit awkward here, but it seems like a bigger footgun to allow
71
- // `get_or_compute_native_filename` to run without a cache, since we never expect that to happen during sampling. So it seems
72
- // like a reasonable trade-off to force callers to always figure that out.
73
- st_table *temporary_cache = st_init_numtable();
74
- const char *native_filename = get_or_compute_native_filename(rb_ary_new, temporary_cache);
75
- if (native_filename != NULL && native_filename[0] != '\0') {
76
- ruby_native_filename = native_filename;
77
- }
78
- st_free_table(temporary_cache);
66
+ #if (defined(HAVE_DLADDR1) && HAVE_DLADDR1) || (defined(HAVE_DLADDR) && HAVE_DLADDR)
67
+ // To be able to detect when a frame is coming from Ruby, we record here its filename as returned by dladdr.
68
+ // We expect this same pointer to be returned by dladdr for all frames coming from Ruby.
69
+ //
70
+ // Small note: Creating/deleting the cache is a bit awkward here, but it seems like a bigger footgun to allow
71
+ // `get_or_compute_native_filename` to run without a cache, since we never expect that to happen during sampling. So it seems
72
+ // like a reasonable trade-off to force callers to always figure that out.
73
+ st_table *temporary_cache = st_init_numtable();
74
+ const char *native_filename = get_or_compute_native_filename(rb_ary_new, temporary_cache);
75
+ if (native_filename != NULL && native_filename[0] != '\0') {
76
+ ruby_native_filename = native_filename;
77
+ }
78
+ st_free_table(temporary_cache);
79
79
  #endif
80
80
  }
81
81
 
82
82
  static VALUE _native_filenames_available(DDTRACE_UNUSED VALUE self) {
83
- #if defined(HAVE_DLADDR1) || defined(HAVE_DLADDR)
83
+ #if (defined(HAVE_DLADDR1) && HAVE_DLADDR1) || (defined(HAVE_DLADDR) && HAVE_DLADDR)
84
84
  return ruby_native_filename != NULL ? Qtrue : Qfalse;
85
85
  #else
86
86
  return Qfalse;
@@ -271,7 +271,8 @@ void sample_thread(
271
271
  // The convention in Kernel#caller_locations is to instead use the path and line number of the first Ruby frame
272
272
  // on the stack that is below (e.g. directly or indirectly has called) the native method.
273
273
  // Thus, we keep that frame here to able to replicate that behavior.
274
- // (This is why we also iterate the sampling buffers backwards below -- so that it's easier to keep the last_ruby_frame_filename)
274
+ // (This is why we also iterate the sampling buffers backwards from what libdatadog uses below -- so that it's easier
275
+ // to keep the last_ruby_frame_filename)
275
276
  ddog_CharSlice last_ruby_frame_filename = DDOG_CHARSLICE_C("");
276
277
  int last_ruby_line = 0;
277
278
 
@@ -290,10 +291,12 @@ void sample_thread(
290
291
  if (labels.is_gvl_waiting_state) rb_raise(rb_eRuntimeError, "BUG: Unexpected combination of cpu-time with is_gvl_waiting");
291
292
  }
292
293
 
293
- for (int i = captured_frames - 1; i >= 0; i--) {
294
+ int top_of_stack_position = captured_frames - 1;
295
+
296
+ for (int i = 0; i <= top_of_stack_position; i++) {
294
297
  ddog_CharSlice name_slice, filename_slice;
295
298
  int line;
296
- bool top_of_the_stack = i == 0;
299
+ bool top_of_the_stack = i == top_of_stack_position;
297
300
 
298
301
  if (buffer->stack_buffer[i].is_ruby_frame) {
299
302
  VALUE name = rb_iseq_base_label(buffer->stack_buffer[i].as.ruby_frame.iseq);
@@ -324,7 +327,6 @@ void sample_thread(
324
327
 
325
328
  maybe_trim_template_random_ids(&name_slice, &filename_slice);
326
329
 
327
-
328
330
  // When there's only wall-time in a sample, this means that the thread was not active in the sampled period.
329
331
  if (top_of_the_stack && only_wall_time) {
330
332
  // Did the caller already provide the state?
@@ -368,21 +370,19 @@ void sample_thread(
368
370
  }
369
371
  }
370
372
 
371
- buffer->locations[i] = (ddog_prof_Location) {
373
+ int libdatadog_stores_stacks_flipped_from_rb_profile_frames_index = top_of_stack_position - i;
374
+
375
+ buffer->locations[libdatadog_stores_stacks_flipped_from_rb_profile_frames_index] = (ddog_prof_Location) {
372
376
  .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C(""), .build_id_id = {}},
373
377
  .function = (ddog_prof_Function) {.name = name_slice, .filename = filename_slice},
374
378
  .line = line,
375
379
  };
376
380
  }
377
381
 
378
- // Used below; since we want to stack-allocate this, we must do it here rather than in maybe_add_placeholder_frames_omitted
379
- const int frames_omitted_message_size = sizeof(MAX_FRAMES_LIMIT_AS_STRING " frames omitted");
380
- char frames_omitted_message[frames_omitted_message_size];
381
-
382
382
  // If we filled up the buffer, some frames may have been omitted. In that case, we'll add a placeholder frame
383
383
  // with that info.
384
384
  if (captured_frames == (long) buffer->max_frames) {
385
- maybe_add_placeholder_frames_omitted(thread, buffer, frames_omitted_message, frames_omitted_message_size);
385
+ add_truncated_frames_placeholder(buffer);
386
386
  }
387
387
 
388
388
  record_sample(
@@ -393,7 +393,7 @@ void sample_thread(
393
393
  );
394
394
  }
395
395
 
396
- #if defined(HAVE_DLADDR1) || defined(HAVE_DLADDR)
396
+ #if (defined(HAVE_DLADDR1) && HAVE_DLADDR1) || (defined(HAVE_DLADDR) && HAVE_DLADDR)
397
397
  static void set_file_info_for_cfunc(
398
398
  ddog_CharSlice *filename_slice,
399
399
  int *line,
@@ -441,12 +441,12 @@ void sample_thread(
441
441
 
442
442
  Dl_info info;
443
443
  const char *native_filename = NULL;
444
- #ifdef HAVE_DLADDR1
444
+ #if defined(HAVE_DLADDR1) && HAVE_DLADDR1
445
445
  struct link_map *extra_info = NULL;
446
446
  if (dladdr1(function, &info, (void **) &extra_info, RTLD_DL_LINKMAP) != 0 && extra_info != NULL) {
447
447
  native_filename = extra_info->l_name != NULL ? extra_info->l_name : info.dli_fname;
448
448
  }
449
- #elif defined(HAVE_DLADDR)
449
+ #elif defined(HAVE_DLADDR) && HAVE_DLADDR
450
450
  if (dladdr(function, &info) != 0) {
451
451
  native_filename = info.dli_fname;
452
452
  }
@@ -521,24 +521,12 @@ static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_Char
521
521
  name_slice->len = pos;
522
522
  }
523
523
 
524
- static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer* buffer, char *frames_omitted_message, int frames_omitted_message_size) {
525
- ptrdiff_t frames_omitted = stack_depth_for(thread) - buffer->max_frames;
526
-
527
- if (frames_omitted == 0) return; // Perfect fit!
528
-
529
- // The placeholder frame takes over a space, so if 10 frames were left out and we consume one other space for the
530
- // placeholder, then 11 frames are omitted in total
531
- frames_omitted++;
532
-
533
- snprintf(frames_omitted_message, frames_omitted_message_size, "%td frames omitted", frames_omitted);
534
-
535
- // Important note: `frames_omitted_message` MUST have a lifetime that is at least as long as the call to
536
- // `record_sample`. So be careful where it gets allocated. (We do have tests for this, at least!)
537
- ddog_CharSlice function_name = DDOG_CHARSLICE_C("");
538
- ddog_CharSlice function_filename = {.ptr = frames_omitted_message, .len = strlen(frames_omitted_message)};
539
- buffer->locations[buffer->max_frames - 1] = (ddog_prof_Location) {
524
+ static void add_truncated_frames_placeholder(sampling_buffer* buffer) {
525
+ // Important note: The strings below are static so we don't need to worry about their lifetime. If we ever want to change
526
+ // this to non-static strings, don't forget to check that lifetimes are properly respected.
527
+ buffer->locations[0] = (ddog_prof_Location) {
540
528
  .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C(""), .build_id_id = {}},
541
- .function = (ddog_prof_Function) {.name = function_name, .filename = function_filename},
529
+ .function = {.name = DDOG_CHARSLICE_C("Truncated Frames"), .filename = DDOG_CHARSLICE_C(""), .filename_id = {}},
542
530
  .line = 0,
543
531
  };
544
532
  }
@@ -597,9 +585,14 @@ void record_placeholder_stack(
597
585
  );
598
586
  }
599
587
 
600
- void prepare_sample_thread(VALUE thread, sampling_buffer *buffer) {
588
+ bool prepare_sample_thread(VALUE thread, sampling_buffer *buffer) {
589
+ // Since this can get called from inside a signal handler, we don't want to touch the buffer if
590
+ // the thread was actually in the middle of marking it.
591
+ if (buffer->is_marking) return false;
592
+
601
593
  buffer->pending_sample = true;
602
594
  buffer->pending_sample_result = ddtrace_rb_profile_frames(thread, 0, buffer->max_frames, buffer->stack_buffer);
595
+ return true;
603
596
  }
604
597
 
605
598
  uint16_t sampling_buffer_check_max_frames(int max_frames) {
@@ -615,6 +608,7 @@ void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames, dd
615
608
  buffer->locations = locations;
616
609
  buffer->stack_buffer = ruby_xcalloc(max_frames, sizeof(frame_info));
617
610
  buffer->pending_sample = false;
611
+ buffer->is_marking = false;
618
612
  buffer->pending_sample_result = 0;
619
613
  }
620
614
 
@@ -630,6 +624,7 @@ void sampling_buffer_free(sampling_buffer *buffer) {
630
624
  buffer->locations = NULL;
631
625
  buffer->stack_buffer = NULL;
632
626
  buffer->pending_sample = false;
627
+ buffer->is_marking = false;
633
628
  buffer->pending_sample_result = 0;
634
629
  }
635
630
 
@@ -638,9 +633,23 @@ void sampling_buffer_mark(sampling_buffer *buffer) {
638
633
  rb_bug("sampling_buffer_mark called with no pending sample. `sampling_buffer_needs_marking` should be used before calling mark.");
639
634
  }
640
635
 
636
+ buffer->is_marking = true;
637
+ // Tell the compiler it's not allowed to reorder the `is_marking` flag with the iteration below.
638
+ //
639
+ // Specifically, in the middle of `sampling_buffer_mark` a signal handler may execute and call
640
+ // `prepare_sample_thread` to add a new sample to the buffer. This flag is here to prevent that BUT we need to
641
+ // make sure the signal handler actually sees the flag being set.
642
+ //
643
+ // See https://github.com/ruby/ruby/pull/11036 for a similar change made to the Ruby VM with more context.
644
+ atomic_signal_fence(memory_order_seq_cst);
645
+
641
646
  for (int i = 0; i < buffer->pending_sample_result; i++) {
642
647
  if (buffer->stack_buffer[i].is_ruby_frame) {
643
648
  rb_gc_mark(buffer->stack_buffer[i].as.ruby_frame.iseq);
644
649
  }
645
650
  }
651
+
652
+ // Make sure iteration completes before `is_marking` is unset...
653
+ atomic_signal_fence(memory_order_seq_cst);
654
+ buffer->is_marking = false;
646
655
  }
@@ -14,6 +14,7 @@ typedef struct {
14
14
  ddog_prof_Location *locations;
15
15
  frame_info *stack_buffer;
16
16
  bool pending_sample;
17
+ bool is_marking; // Used to avoid recording a sample when marking
17
18
  int pending_sample_result;
18
19
  } sampling_buffer;
19
20
 
@@ -32,7 +33,7 @@ void record_placeholder_stack(
32
33
  sample_labels labels,
33
34
  ddog_CharSlice placeholder_stack
34
35
  );
35
- void prepare_sample_thread(VALUE thread, sampling_buffer *buffer);
36
+ bool prepare_sample_thread(VALUE thread, sampling_buffer *buffer);
36
37
 
37
38
  uint16_t sampling_buffer_check_max_frames(int max_frames);
38
39
  void sampling_buffer_initialize(sampling_buffer *buffer, uint16_t max_frames, ddog_prof_Location *locations);
@@ -1469,17 +1469,17 @@ static VALUE thread_list(thread_context_collector_state *state) {
1469
1469
  // expected to be called from a signal handler and to be async-signal-safe.
1470
1470
  //
1471
1471
  // Also, no allocation (Ruby or malloc) can happen.
1472
- void thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance) {
1472
+ bool thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance) {
1473
1473
  thread_context_collector_state *state;
1474
- if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return;
1474
+ if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
1475
1475
  // This should never fail if the above check passes
1476
1476
  TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1477
1477
 
1478
1478
  VALUE current_thread = rb_thread_current();
1479
1479
  per_thread_context *thread_context = get_context_for(current_thread, state);
1480
- if (thread_context == NULL) return;
1480
+ if (thread_context == NULL) return false;
1481
1481
 
1482
- prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1482
+ return prepare_sample_thread(current_thread, &thread_context->sampling_buffer);
1483
1483
  }
1484
1484
 
1485
1485
  void thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object) {
@@ -2217,6 +2217,5 @@ static VALUE _native_system_epoch_time_now_ns(DDTRACE_UNUSED VALUE self, VALUE c
2217
2217
  }
2218
2218
 
2219
2219
  static VALUE _native_prepare_sample_inside_signal_handler(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
2220
- thread_context_collector_prepare_sample_inside_signal_handler(collector_instance);
2221
- return Qtrue;
2220
+ return thread_context_collector_prepare_sample_inside_signal_handler(collector_instance) ? Qtrue : Qfalse;
2222
2221
  }
@@ -10,7 +10,7 @@ void thread_context_collector_sample(
10
10
  long current_monotonic_wall_time_ns,
11
11
  VALUE profiler_overhead_stack_thread
12
12
  );
13
- void thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance);
13
+ __attribute__((warn_unused_result)) bool thread_context_collector_prepare_sample_inside_signal_handler(VALUE self_instance);
14
14
  void thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object);
15
15
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples);
16
16
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance);