datadog 2.0.0.beta1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +181 -1
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +1 -1
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +40 -32
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +23 -12
  6. data/ext/datadog_profiling_native_extension/crashtracker.c +108 -0
  7. data/ext/datadog_profiling_native_extension/extconf.rb +9 -23
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +81 -4
  9. data/ext/datadog_profiling_native_extension/heap_recorder.h +12 -1
  10. data/ext/datadog_profiling_native_extension/http_transport.c +1 -94
  11. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +86 -0
  12. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +4 -0
  13. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +2 -12
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -86
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.h +3 -5
  17. data/ext/datadog_profiling_native_extension/stack_recorder.c +161 -62
  18. data/lib/datadog/appsec/contrib/devise/tracking.rb +8 -0
  19. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +43 -13
  20. data/lib/datadog/appsec/event.rb +2 -2
  21. data/lib/datadog/core/configuration/components.rb +2 -1
  22. data/lib/datadog/core/configuration/option.rb +7 -5
  23. data/lib/datadog/core/configuration/settings.rb +34 -79
  24. data/lib/datadog/core/configuration.rb +20 -4
  25. data/lib/datadog/core/environment/platform.rb +7 -1
  26. data/lib/datadog/core/remote/client/capabilities.rb +2 -1
  27. data/lib/datadog/core/remote/client.rb +1 -5
  28. data/lib/datadog/core/remote/configuration/repository.rb +1 -1
  29. data/lib/datadog/core/remote/dispatcher.rb +3 -3
  30. data/lib/datadog/core/remote/transport/http/config.rb +5 -5
  31. data/lib/datadog/core/telemetry/client.rb +18 -10
  32. data/lib/datadog/core/telemetry/emitter.rb +9 -13
  33. data/lib/datadog/core/telemetry/event.rb +247 -57
  34. data/lib/datadog/core/telemetry/ext.rb +1 -0
  35. data/lib/datadog/core/telemetry/heartbeat.rb +1 -3
  36. data/lib/datadog/core/telemetry/http/ext.rb +4 -1
  37. data/lib/datadog/core/telemetry/http/response.rb +4 -0
  38. data/lib/datadog/core/telemetry/http/transport.rb +9 -4
  39. data/lib/datadog/core/telemetry/request.rb +59 -0
  40. data/lib/datadog/core/utils/base64.rb +22 -0
  41. data/lib/datadog/opentelemetry/sdk/span_processor.rb +19 -2
  42. data/lib/datadog/opentelemetry/sdk/trace/span.rb +3 -17
  43. data/lib/datadog/profiling/collectors/code_provenance.rb +10 -4
  44. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +25 -0
  45. data/lib/datadog/profiling/component.rb +49 -17
  46. data/lib/datadog/profiling/crashtracker.rb +91 -0
  47. data/lib/datadog/profiling/exporter.rb +6 -3
  48. data/lib/datadog/profiling/http_transport.rb +7 -11
  49. data/lib/datadog/profiling/load_native_extension.rb +14 -1
  50. data/lib/datadog/profiling/profiler.rb +9 -2
  51. data/lib/datadog/profiling/stack_recorder.rb +6 -2
  52. data/lib/datadog/profiling.rb +12 -0
  53. data/lib/datadog/tracing/component.rb +5 -1
  54. data/lib/datadog/tracing/configuration/dynamic.rb +39 -1
  55. data/lib/datadog/tracing/configuration/settings.rb +1 -0
  56. data/lib/datadog/tracing/contrib/action_pack/integration.rb +1 -1
  57. data/lib/datadog/tracing/contrib/action_view/integration.rb +1 -1
  58. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +1 -0
  59. data/lib/datadog/tracing/contrib/active_record/integration.rb +11 -1
  60. data/lib/datadog/tracing/contrib/active_support/integration.rb +1 -1
  61. data/lib/datadog/tracing/contrib/configuration/resolver.rb +43 -0
  62. data/lib/datadog/tracing/contrib/grape/endpoint.rb +43 -5
  63. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +1 -1
  64. data/lib/datadog/tracing/correlation.rb +3 -4
  65. data/lib/datadog/tracing/remote.rb +5 -1
  66. data/lib/datadog/tracing/sampling/ext.rb +5 -1
  67. data/lib/datadog/tracing/sampling/matcher.rb +75 -26
  68. data/lib/datadog/tracing/sampling/rule.rb +27 -4
  69. data/lib/datadog/tracing/sampling/rule_sampler.rb +19 -1
  70. data/lib/datadog/tracing/sampling/span/matcher.rb +13 -41
  71. data/lib/datadog/tracing/span.rb +7 -2
  72. data/lib/datadog/tracing/span_link.rb +92 -0
  73. data/lib/datadog/tracing/span_operation.rb +6 -4
  74. data/lib/datadog/tracing/trace_operation.rb +12 -0
  75. data/lib/datadog/tracing/tracer.rb +4 -3
  76. data/lib/datadog/tracing/transport/serializable_trace.rb +3 -1
  77. data/lib/datadog/tracing/utils.rb +16 -0
  78. data/lib/datadog/version.rb +1 -1
  79. metadata +10 -31
  80. data/lib/datadog/core/telemetry/collector.rb +0 -248
  81. data/lib/datadog/core/telemetry/v1/app_event.rb +0 -59
  82. data/lib/datadog/core/telemetry/v1/application.rb +0 -94
  83. data/lib/datadog/core/telemetry/v1/configuration.rb +0 -27
  84. data/lib/datadog/core/telemetry/v1/dependency.rb +0 -45
  85. data/lib/datadog/core/telemetry/v1/host.rb +0 -59
  86. data/lib/datadog/core/telemetry/v1/install_signature.rb +0 -38
  87. data/lib/datadog/core/telemetry/v1/integration.rb +0 -66
  88. data/lib/datadog/core/telemetry/v1/product.rb +0 -36
  89. data/lib/datadog/core/telemetry/v1/telemetry_request.rb +0 -108
  90. data/lib/datadog/core/telemetry/v2/app_client_configuration_change.rb +0 -41
  91. data/lib/datadog/core/telemetry/v2/request.rb +0 -29
@@ -0,0 +1,108 @@
1
+ #include <ruby.h>
2
+ #include <datadog/common.h>
3
+ #include <libdatadog_helpers.h>
4
+
5
+ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
6
+ static VALUE _native_stop(DDTRACE_UNUSED VALUE _self);
7
+
8
+ // Used to report Ruby VM crashes.
9
+ // Once initialized, segfaults will be reported automatically using libdatadog.
10
+
11
+ void crashtracker_init(VALUE profiling_module) {
12
+ VALUE crashtracker_class = rb_define_class_under(profiling_module, "Crashtracker", rb_cObject);
13
+
14
+ rb_define_singleton_method(crashtracker_class, "_native_start_or_update_on_fork", _native_start_or_update_on_fork, -1);
15
+ rb_define_singleton_method(crashtracker_class, "_native_stop", _native_stop, 0);
16
+ }
17
+
18
+ static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
19
+ VALUE options;
20
+ rb_scan_args(argc, argv, "0:", &options);
21
+
22
+ VALUE exporter_configuration = rb_hash_fetch(options, ID2SYM(rb_intern("exporter_configuration")));
23
+ VALUE path_to_crashtracking_receiver_binary = rb_hash_fetch(options, ID2SYM(rb_intern("path_to_crashtracking_receiver_binary")));
24
+ VALUE ld_library_path = rb_hash_fetch(options, ID2SYM(rb_intern("ld_library_path")));
25
+ VALUE tags_as_array = rb_hash_fetch(options, ID2SYM(rb_intern("tags_as_array")));
26
+ VALUE action = rb_hash_fetch(options, ID2SYM(rb_intern("action")));
27
+ VALUE upload_timeout_seconds = rb_hash_fetch(options, ID2SYM(rb_intern("upload_timeout_seconds")));
28
+
29
+ VALUE start_action = ID2SYM(rb_intern("start"));
30
+ VALUE update_on_fork_action = ID2SYM(rb_intern("update_on_fork"));
31
+
32
+ ENFORCE_TYPE(exporter_configuration, T_ARRAY);
33
+ ENFORCE_TYPE(tags_as_array, T_ARRAY);
34
+ ENFORCE_TYPE(path_to_crashtracking_receiver_binary, T_STRING);
35
+ ENFORCE_TYPE(ld_library_path, T_STRING);
36
+ ENFORCE_TYPE(action, T_SYMBOL);
37
+ ENFORCE_TYPE(upload_timeout_seconds, T_FIXNUM);
38
+
39
+ if (action != start_action && action != update_on_fork_action) rb_raise(rb_eArgError, "Unexpected action: %+"PRIsVALUE, action);
40
+
41
+ VALUE version = ddtrace_version();
42
+ ddog_prof_Endpoint endpoint = endpoint_from(exporter_configuration);
43
+
44
+ // Tags are heap-allocated, so after here we can't raise exceptions otherwise we'll leak this memory
45
+ // Start of exception-free zone to prevent leaks {{
46
+ ddog_Vec_Tag tags = convert_tags(tags_as_array);
47
+
48
+ ddog_prof_CrashtrackerConfiguration config = {
49
+ .additional_files = {},
50
+ // The Ruby VM already uses an alt stack to detect stack overflows so the crash handler must not overwrite it.
51
+ //
52
+ // @ivoanjo: Specifically, with `create_alt_stack = true` I saw a segfault, such as Ruby 2.6's bug with
53
+ // "Process.detach(fork { exit! }).instance_variable_get(:@foo)" being turned into a
54
+ // "-e:1:in `instance_variable_get': stack level too deep (SystemStackError)" by Ruby.
55
+ //
56
+ // The Ruby crash handler also seems to get confused when this option is enabled and
57
+ // "Process.kill('SEGV', Process.pid)" gets run.
58
+ .create_alt_stack = false,
59
+ .endpoint = endpoint,
60
+ .resolve_frames = DDOG_PROF_STACKTRACE_COLLECTION_ENABLED,
61
+ .timeout_secs = FIX2INT(upload_timeout_seconds),
62
+ };
63
+
64
+ ddog_prof_CrashtrackerMetadata metadata = {
65
+ .profiling_library_name = DDOG_CHARSLICE_C("dd-trace-rb"),
66
+ .profiling_library_version = char_slice_from_ruby_string(version),
67
+ .family = DDOG_CHARSLICE_C("ruby"),
68
+ .tags = &tags,
69
+ };
70
+
71
+ ddog_prof_EnvVar ld_library_path_env = {
72
+ .key = DDOG_CHARSLICE_C("LD_LIBRARY_PATH"),
73
+ .val = char_slice_from_ruby_string(ld_library_path),
74
+ };
75
+
76
+ ddog_prof_CrashtrackerReceiverConfig receiver_config = {
77
+ .args = {},
78
+ .env = {.ptr = &ld_library_path_env, .len = 1},
79
+ .path_to_receiver_binary = char_slice_from_ruby_string(path_to_crashtracking_receiver_binary),
80
+ .optional_stderr_filename = {},
81
+ .optional_stdout_filename = {},
82
+ };
83
+
84
+ ddog_prof_CrashtrackerResult result =
85
+ action == start_action ?
86
+ ddog_prof_Crashtracker_init(config, receiver_config, metadata) :
87
+ ddog_prof_Crashtracker_update_on_fork(config, receiver_config, metadata);
88
+
89
+ // Clean up before potentially raising any exceptions
90
+ ddog_Vec_Tag_drop(tags);
91
+ // }} End of exception-free zone to prevent leaks
92
+
93
+ if (result.tag == DDOG_PROF_CRASHTRACKER_RESULT_ERR) {
94
+ rb_raise(rb_eRuntimeError, "Failed to start/update the crash tracker: %"PRIsVALUE, get_error_details_and_drop(&result.err));
95
+ }
96
+
97
+ return Qtrue;
98
+ }
99
+
100
+ static VALUE _native_stop(DDTRACE_UNUSED VALUE _self) {
101
+ ddog_prof_CrashtrackerResult result = ddog_prof_Crashtracker_shutdown();
102
+
103
+ if (result.tag == DDOG_PROF_CRASHTRACKER_RESULT_ERR) {
104
+ rb_raise(rb_eRuntimeError, "Failed to stop the crash tracker: %"PRIsVALUE, get_error_details_and_drop(&result.err));
105
+ }
106
+
107
+ return Qtrue;
108
+ }
@@ -126,7 +126,7 @@ if RUBY_PLATFORM.include?('linux')
126
126
  # have_library 'pthread'
127
127
  # have_func 'pthread_getcpuclockid'
128
128
  # ```
129
- # but a) it broke the build on Windows, b) on older Ruby versions (2.2 and below) and c) It's slower to build
129
+ # but it's slower to build
130
130
  # so instead we just assume that we have the function we need on Linux, and nowhere else
131
131
  $defs << '-DHAVE_PTHREAD_GETCPUCLOCKID'
132
132
  end
@@ -163,6 +163,11 @@ $defs << '-DNO_THREAD_TID' if RUBY_VERSION < '3.1'
163
163
  # On older Rubies, there was no jit_return member on the rb_control_frame_t struct
164
164
  $defs << '-DNO_JIT_RETURN' if RUBY_VERSION < '3.1'
165
165
 
166
+ # On older Rubies, rb_gc_force_recycle allowed to free objects in a way that
167
+ # would be invisible to free tracepoints, finalizers and without cleaning
168
+ # obj_to_id_tbl mappings.
169
+ $defs << '-DHAVE_WORKING_RB_GC_FORCE_RECYCLE' if RUBY_VERSION < '3.1'
170
+
166
171
  # On older Rubies, we need to use a backported version of this function. See private_vm_api_access.h for details.
167
172
  $defs << '-DUSE_BACKPORTED_RB_PROFILE_FRAME_METHOD_NAME' if RUBY_VERSION < '3'
168
173
 
@@ -175,34 +180,15 @@ $defs << '-DNO_IMEMO_NAME' if RUBY_VERSION < '3'
175
180
  # On older Rubies, objects would not move
176
181
  $defs << '-DNO_T_MOVED' if RUBY_VERSION < '2.7'
177
182
 
183
+ # On older Rubies, there was no RUBY_SEEN_OBJ_ID flag
184
+ $defs << '-DNO_SEEN_OBJ_ID_FLAG' if RUBY_VERSION < '2.7'
185
+
178
186
  # On older Rubies, rb_global_vm_lock_struct did not include the owner field
179
187
  $defs << '-DNO_GVL_OWNER' if RUBY_VERSION < '2.6'
180
188
 
181
189
  # On older Rubies, there was no thread->invoke_arg
182
190
  $defs << '-DNO_THREAD_INVOKE_ARG' if RUBY_VERSION < '2.6'
183
191
 
184
- # On older Rubies, we need to use rb_thread_t instead of rb_execution_context_t
185
- $defs << '-DUSE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT' if RUBY_VERSION < '2.5'
186
-
187
- # On older Rubies, extensions can't use GET_VM()
188
- $defs << '-DNO_GET_VM' if RUBY_VERSION < '2.5'
189
-
190
- # On older Rubies...
191
- if RUBY_VERSION < '2.4'
192
- # ...we need to use RUBY_VM_NORMAL_ISEQ_P instead of VM_FRAME_RUBYFRAME_P
193
- $defs << '-DUSE_ISEQ_P_INSTEAD_OF_RUBYFRAME_P'
194
- # ...we use a legacy copy of rb_vm_frame_method_entry
195
- $defs << '-DUSE_LEGACY_RB_VM_FRAME_METHOD_ENTRY'
196
- end
197
-
198
- # On older Rubies, rb_gc_force_recycle allowed to free objects in a way that
199
- # would be invisible to free tracepoints, finalizers and without cleaning
200
- # obj_to_id_tbl mappings.
201
- $defs << '-DHAVE_WORKING_RB_GC_FORCE_RECYCLE' if RUBY_VERSION < '3.1'
202
-
203
- # On older Rubies, there was no RUBY_SEEN_OBJ_ID flag
204
- $defs << '-DNO_SEEN_OBJ_ID_FLAG' if RUBY_VERSION < '2.7'
205
-
206
192
  # If we got here, libdatadog is available and loaded
207
193
  ENV['PKG_CONFIG_PATH'] = "#{ENV['PKG_CONFIG_PATH']}:#{Libdatadog.pkgconfig_folder}"
208
194
  Logging.message("[datadog] PKG_CONFIG_PATH set to #{ENV['PKG_CONFIG_PATH'].inspect}\n")
@@ -10,6 +10,13 @@
10
10
  #define CAN_APPLY_GC_FORCE_RECYCLE_BUG_WORKAROUND
11
11
  #endif
12
12
 
13
+ // Minimum age (in GC generations) of heap objects we want to include in heap
14
+ // recorder iterations. Object with age 0 represent objects that have yet to undergo
15
+ // a GC and, thus, may just be noise/trash at instant of iteration and are usually not
16
+ // relevant for heap profiles as the great majority should be trivially reclaimed
17
+ // during the next GC.
18
+ #define ITERATION_MIN_AGE 1
19
+
13
20
  // A compact representation of a stacktrace frame for a heap allocation.
14
21
  typedef struct {
15
22
  char *name;
@@ -137,6 +144,11 @@ struct heap_recorder {
137
144
  // mutation of the data so iteration can occur without acquiring a lock.
138
145
  // NOTE: Contrary to object_records, this table has no ownership of its data.
139
146
  st_table *object_records_snapshot;
147
+ // The GC gen/epoch/count in which we prepared the current iteration.
148
+ //
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;
140
152
 
141
153
  // Data for a heap recording that was started but not yet ended
142
154
  recording active_recording;
@@ -146,6 +158,13 @@ struct heap_recorder {
146
158
 
147
159
  // Sampling state
148
160
  uint num_recordings_skipped;
161
+
162
+ struct stats_last_update {
163
+ size_t objects_alive;
164
+ size_t objects_dead;
165
+ size_t objects_skipped;
166
+ size_t objects_frozen;
167
+ } stats_last_update;
149
168
  };
150
169
  static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location);
151
170
  static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*);
@@ -353,11 +372,16 @@ void heap_recorder_prepare_iteration(heap_recorder *heap_recorder) {
353
372
  return;
354
373
  }
355
374
 
375
+ heap_recorder->iteration_gen = rb_gc_count();
376
+
356
377
  if (heap_recorder->object_records_snapshot != NULL) {
357
378
  // we could trivially handle this but we raise to highlight and catch unexpected usages.
358
379
  rb_raise(rb_eRuntimeError, "New heap recorder iteration prepared without the previous one having been finished.");
359
380
  }
360
381
 
382
+ // Reset last update stats, we'll be building them from scratch during the st_foreach call below
383
+ heap_recorder->stats_last_update = (struct stats_last_update) {};
384
+
361
385
  st_foreach(heap_recorder->object_records, st_object_record_update, (st_data_t) heap_recorder);
362
386
 
363
387
  heap_recorder->object_records_snapshot = st_copy(heap_recorder->object_records);
@@ -413,6 +437,22 @@ bool heap_recorder_for_each_live_object(
413
437
  return true;
414
438
  }
415
439
 
440
+ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder) {
441
+ VALUE arguments[] = {
442
+ ID2SYM(rb_intern("num_object_records")), /* => */ LONG2NUM(heap_recorder->object_records->num_entries),
443
+ ID2SYM(rb_intern("num_heap_records")), /* => */ LONG2NUM(heap_recorder->heap_records->num_entries),
444
+
445
+ // Stats as of last update
446
+ ID2SYM(rb_intern("last_update_objects_alive")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_alive),
447
+ ID2SYM(rb_intern("last_update_objects_dead")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_dead),
448
+ ID2SYM(rb_intern("last_update_objects_skipped")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_skipped),
449
+ ID2SYM(rb_intern("last_update_objects_frozen")), /* => */ LONG2NUM(heap_recorder->stats_last_update.objects_frozen),
450
+ };
451
+ VALUE hash = rb_hash_new();
452
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(hash, arguments[i], arguments[i+1]);
453
+ return hash;
454
+ }
455
+
416
456
  void heap_recorder_testonly_assert_hash_matches(ddog_prof_Slice_Location locations) {
417
457
  heap_stack *stack = heap_stack_new(locations);
418
458
  heap_record_key stack_based_key = (heap_record_key) {
@@ -459,6 +499,13 @@ static int st_object_record_entry_free(DDTRACE_UNUSED st_data_t key, st_data_t v
459
499
  return ST_DELETE;
460
500
  }
461
501
 
502
+ // Check to see if an object should not be included in a heap recorder iteration.
503
+ // This centralizes the checking logic to ensure it's equally applied between
504
+ // preparation and iteration codepaths.
505
+ static inline bool should_exclude_from_iteration(object_record *obj_record) {
506
+ return obj_record->object_data.gen_age < ITERATION_MIN_AGE;
507
+ }
508
+
462
509
  static int st_object_record_update(st_data_t key, st_data_t value, st_data_t extra_arg) {
463
510
  long obj_id = (long) key;
464
511
  object_record *record = (object_record*) value;
@@ -466,9 +513,24 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
466
513
 
467
514
  VALUE ref;
468
515
 
516
+ size_t iteration_gen = recorder->iteration_gen;
517
+ size_t alloc_gen = record->object_data.alloc_gen;
518
+ // Guard against potential overflows given unsigned types here.
519
+ record->object_data.gen_age = alloc_gen < iteration_gen ? iteration_gen - alloc_gen : 0;
520
+
521
+ if (should_exclude_from_iteration(record)) {
522
+ // If an object won't be included in the current iteration, there's
523
+ // no point checking for liveness or updating its size, so exit early.
524
+ // NOTE: This means that there should be an equivalent check during actual
525
+ // iteration otherwise we'd iterate/expose stale object data.
526
+ recorder->stats_last_update.objects_skipped++;
527
+ return ST_CONTINUE;
528
+ }
529
+
469
530
  if (!ruby_ref_from_id(LONG2NUM(obj_id), &ref)) {
470
531
  // Id no longer associated with a valid ref. Need to delete this object record!
471
532
  on_committed_object_record_cleanup(recorder, record);
533
+ recorder->stats_last_update.objects_dead++;
472
534
  return ST_DELETE;
473
535
  }
474
536
 
@@ -503,6 +565,7 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
503
565
  RB_FL_SET(ref, RUBY_FL_SEEN_OBJ_ID);
504
566
 
505
567
  on_committed_object_record_cleanup(recorder, record);
568
+ recorder->stats_last_update.objects_dead++;
506
569
  return ST_DELETE;
507
570
  }
508
571
 
@@ -516,6 +579,11 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
516
579
  record->object_data.is_frozen = RB_OBJ_FROZEN(ref);
517
580
  }
518
581
 
582
+ recorder->stats_last_update.objects_alive++;
583
+ if (record->object_data.is_frozen) {
584
+ recorder->stats_last_update.objects_frozen++;
585
+ }
586
+
519
587
  return ST_CONTINUE;
520
588
  }
521
589
 
@@ -525,8 +593,16 @@ static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t val
525
593
  const heap_stack *stack = record->heap_record->stack;
526
594
  iteration_context *context = (iteration_context*) extra;
527
595
 
528
- ddog_prof_Location *locations = context->heap_recorder->reusable_locations;
596
+ const heap_recorder *recorder = context->heap_recorder;
597
+
598
+ if (should_exclude_from_iteration(record)) {
599
+ // Skip objects that should not be included in iteration
600
+ // NOTE: This matches the short-circuiting condition in st_object_record_update
601
+ // and prevents iteration over stale objects.
602
+ return ST_CONTINUE;
603
+ }
529
604
 
605
+ ddog_prof_Location *locations = recorder->reusable_locations;
530
606
  for (uint16_t i = 0; i < stack->frames_len; i++) {
531
607
  const heap_frame *frame = &stack->frames[i];
532
608
  ddog_prof_Location *location = &locations[i];
@@ -725,9 +801,10 @@ void object_record_free(object_record *record) {
725
801
 
726
802
  VALUE object_record_inspect(object_record *record) {
727
803
  heap_frame top_frame = record->heap_record->stack->frames[0];
728
- VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%s:%d alloc_gen=%zu ",
729
- record->obj_id, record->object_data.weight, record->object_data.size, top_frame.filename,
730
- (int) top_frame.line, record->object_data.alloc_gen);
804
+ live_object_data object_data = record->object_data;
805
+ VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%s:%d alloc_gen=%zu gen_age=%zu frozen=%d ",
806
+ record->obj_id, object_data.weight, object_data.size, top_frame.filename,
807
+ (int) top_frame.line, object_data.alloc_gen, object_data.gen_age, object_data.is_frozen);
731
808
 
732
809
  const char *class = record->object_data.class;
733
810
  if (class != NULL) {
@@ -27,7 +27,9 @@ typedef struct live_object_data {
27
27
  // could be seen as being representative of 50 objects.
28
28
  unsigned int weight;
29
29
 
30
- // Size of this object on last flush/update.
30
+ // Size of this object in memory.
31
+ // NOTE: This only gets updated during heap_recorder_prepare_iteration and only
32
+ // for those objects that meet the minimum iteration age requirements.
31
33
  size_t size;
32
34
 
33
35
  // The class of the object that we're tracking.
@@ -39,6 +41,10 @@ typedef struct live_object_data {
39
41
  // This enables us to calculate the age of this object in terms of GC executions.
40
42
  size_t alloc_gen;
41
43
 
44
+ // The age of this object in terms of GC generations.
45
+ // NOTE: This only gets updated during heap_recorder_prepare_iteration
46
+ size_t gen_age;
47
+
42
48
  // Whether this object was previously seen as being frozen. If this is the case,
43
49
  // we'll skip any further size updates since frozen objects are supposed to be
44
50
  // immutable.
@@ -144,6 +150,11 @@ bool heap_recorder_for_each_live_object(
144
150
  bool (*for_each_callback)(heap_recorder_iteration_data data, void* extra_arg),
145
151
  void *for_each_callback_extra_arg);
146
152
 
153
+ // Return a Ruby hash containing a snapshot of this recorder's interesting state at calling time.
154
+ // WARN: This allocates in the Ruby VM and therefore should not be called without the
155
+ // VM lock or during GC.
156
+ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder);
157
+
147
158
  // v--- TEST-ONLY APIs ---v
148
159
 
149
160
  // Assert internal hashing logic is valid for the provided locations and its
@@ -11,11 +11,6 @@
11
11
  static VALUE ok_symbol = Qnil; // :ok in Ruby
12
12
  static VALUE error_symbol = Qnil; // :error in Ruby
13
13
 
14
- static ID agentless_id; // id of :agentless in Ruby
15
- static ID agent_id; // id of :agent in Ruby
16
-
17
- static ID log_failure_to_process_tag_id; // id of :log_failure_to_process_tag in Ruby
18
-
19
14
  static VALUE library_version_string = Qnil;
20
15
 
21
16
  struct call_exporter_without_gvl_arguments {
@@ -30,9 +25,6 @@ inline static ddog_ByteSlice byte_slice_from_ruby_string(VALUE string);
30
25
  static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration);
31
26
  static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration, VALUE tags_as_array);
32
27
  static VALUE handle_exporter_failure(ddog_prof_Exporter_NewResult exporter_result);
33
- static ddog_Endpoint endpoint_from(VALUE exporter_configuration);
34
- static ddog_Vec_Tag convert_tags(VALUE tags_as_array);
35
- static void safely_log_failure_to_process_tag(ddog_Vec_Tag tags, VALUE err_details);
36
28
  static VALUE _native_do_export(
37
29
  VALUE self,
38
30
  VALUE exporter_configuration,
@@ -60,9 +52,6 @@ void http_transport_init(VALUE profiling_module) {
60
52
 
61
53
  ok_symbol = ID2SYM(rb_intern_const("ok"));
62
54
  error_symbol = ID2SYM(rb_intern_const("error"));
63
- agentless_id = rb_intern_const("agentless");
64
- agent_id = rb_intern_const("agent");
65
- log_failure_to_process_tag_id = rb_intern_const("log_failure_to_process_tag");
66
55
 
67
56
  library_version_string = ddtrace_version();
68
57
  rb_global_variable(&library_version_string);
@@ -94,7 +83,7 @@ static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration
94
83
 
95
84
  // This needs to be called BEFORE convert_tags since it can raise an exception and thus cause the ddog_Vec_Tag
96
85
  // to be leaked.
97
- ddog_Endpoint endpoint = endpoint_from(exporter_configuration);
86
+ ddog_prof_Endpoint endpoint = endpoint_from(exporter_configuration);
98
87
 
99
88
  ddog_Vec_Tag tags = convert_tags(tags_as_array);
100
89
 
@@ -116,88 +105,6 @@ static VALUE handle_exporter_failure(ddog_prof_Exporter_NewResult exporter_resul
116
105
  rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&exporter_result.err));
117
106
  }
118
107
 
119
- static ddog_Endpoint endpoint_from(VALUE exporter_configuration) {
120
- ENFORCE_TYPE(exporter_configuration, T_ARRAY);
121
-
122
- ID working_mode = SYM2ID(rb_ary_entry(exporter_configuration, 0)); // SYM2ID verifies its input so we can do this safely
123
-
124
- if (working_mode != agentless_id && working_mode != agent_id) {
125
- rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
126
- }
127
-
128
- if (working_mode == agentless_id) {
129
- VALUE site = rb_ary_entry(exporter_configuration, 1);
130
- VALUE api_key = rb_ary_entry(exporter_configuration, 2);
131
- ENFORCE_TYPE(site, T_STRING);
132
- ENFORCE_TYPE(api_key, T_STRING);
133
-
134
- return ddog_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
135
- } else { // agent_id
136
- VALUE base_url = rb_ary_entry(exporter_configuration, 1);
137
- ENFORCE_TYPE(base_url, T_STRING);
138
-
139
- return ddog_Endpoint_agent(char_slice_from_ruby_string(base_url));
140
- }
141
- }
142
-
143
- __attribute__((warn_unused_result))
144
- static ddog_Vec_Tag convert_tags(VALUE tags_as_array) {
145
- ENFORCE_TYPE(tags_as_array, T_ARRAY);
146
-
147
- long tags_count = RARRAY_LEN(tags_as_array);
148
- ddog_Vec_Tag tags = ddog_Vec_Tag_new();
149
-
150
- for (long i = 0; i < tags_count; i++) {
151
- VALUE name_value_pair = rb_ary_entry(tags_as_array, i);
152
-
153
- if (!RB_TYPE_P(name_value_pair, T_ARRAY)) {
154
- ddog_Vec_Tag_drop(tags);
155
- ENFORCE_TYPE(name_value_pair, T_ARRAY);
156
- }
157
-
158
- // Note: We can index the array without checking its size first because rb_ary_entry returns Qnil if out of bounds
159
- VALUE tag_name = rb_ary_entry(name_value_pair, 0);
160
- VALUE tag_value = rb_ary_entry(name_value_pair, 1);
161
-
162
- if (!(RB_TYPE_P(tag_name, T_STRING) && RB_TYPE_P(tag_value, T_STRING))) {
163
- ddog_Vec_Tag_drop(tags);
164
- ENFORCE_TYPE(tag_name, T_STRING);
165
- ENFORCE_TYPE(tag_value, T_STRING);
166
- }
167
-
168
- ddog_Vec_Tag_PushResult push_result =
169
- ddog_Vec_Tag_push(&tags, char_slice_from_ruby_string(tag_name), char_slice_from_ruby_string(tag_value));
170
-
171
- if (push_result.tag == DDOG_VEC_TAG_PUSH_RESULT_ERR) {
172
- // libdatadog validates tags and may catch invalid tags that ddtrace didn't actually catch.
173
- // We warn users about such tags, and then just ignore them.
174
- safely_log_failure_to_process_tag(tags, get_error_details_and_drop(&push_result.err));
175
- }
176
- }
177
-
178
- return tags;
179
- }
180
-
181
- static VALUE log_failure_to_process_tag(VALUE err_details) {
182
- VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
183
- VALUE profiling_module = rb_const_get(datadog_module, rb_intern("Profiling"));
184
- VALUE http_transport_class = rb_const_get(profiling_module, rb_intern("HttpTransport"));
185
-
186
- return rb_funcall(http_transport_class, log_failure_to_process_tag_id, 1, err_details);
187
- }
188
-
189
- // Since we are calling into Ruby code, it may raise an exception. This method ensure that dynamically-allocated tags
190
- // get cleaned before propagating the exception.
191
- static void safely_log_failure_to_process_tag(ddog_Vec_Tag tags, VALUE err_details) {
192
- int exception_state;
193
- rb_protect(log_failure_to_process_tag, err_details, &exception_state);
194
-
195
- if (exception_state) { // An exception was raised
196
- ddog_Vec_Tag_drop(tags); // clean up
197
- rb_jump_tag(exception_state); // "Re-raise" exception
198
- }
199
- }
200
-
201
108
  // Note: This function handles a bunch of libdatadog dynamically-allocated objects, so it MUST not use any Ruby APIs
202
109
  // which can raise exceptions, otherwise the objects will be leaked.
203
110
  static VALUE perform_export(
@@ -2,6 +2,8 @@
2
2
 
3
3
  #include <ruby.h>
4
4
 
5
+ static VALUE log_failure_to_process_tag(VALUE err_details);
6
+
5
7
  const char *ruby_value_type_to_string(enum ruby_value_type type) {
6
8
  return ruby_value_type_to_char_slice(type).ptr;
7
9
  }
@@ -60,3 +62,87 @@ size_t read_ddogerr_string_and_drop(ddog_Error *error, char *string, size_t capa
60
62
  ddog_Error_drop(error);
61
63
  return error_msg_size;
62
64
  }
65
+
66
+ __attribute__((warn_unused_result))
67
+ ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration) {
68
+ ENFORCE_TYPE(exporter_configuration, T_ARRAY);
69
+
70
+ VALUE exporter_working_mode = rb_ary_entry(exporter_configuration, 0);
71
+ ENFORCE_TYPE(exporter_working_mode, T_SYMBOL);
72
+ ID working_mode = SYM2ID(exporter_working_mode);
73
+
74
+ ID agentless_id = rb_intern("agentless");
75
+ ID agent_id = rb_intern("agent");
76
+
77
+ if (working_mode != agentless_id && working_mode != agent_id) {
78
+ rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
79
+ }
80
+
81
+ if (working_mode == agentless_id) {
82
+ VALUE site = rb_ary_entry(exporter_configuration, 1);
83
+ VALUE api_key = rb_ary_entry(exporter_configuration, 2);
84
+ ENFORCE_TYPE(site, T_STRING);
85
+ ENFORCE_TYPE(api_key, T_STRING);
86
+
87
+ return ddog_prof_Endpoint_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
88
+ } else { // agent_id
89
+ VALUE base_url = rb_ary_entry(exporter_configuration, 1);
90
+ ENFORCE_TYPE(base_url, T_STRING);
91
+
92
+ return ddog_prof_Endpoint_agent(char_slice_from_ruby_string(base_url));
93
+ }
94
+ }
95
+
96
+ __attribute__((warn_unused_result))
97
+ ddog_Vec_Tag convert_tags(VALUE tags_as_array) {
98
+ ENFORCE_TYPE(tags_as_array, T_ARRAY);
99
+
100
+ long tags_count = RARRAY_LEN(tags_as_array);
101
+ ddog_Vec_Tag tags = ddog_Vec_Tag_new();
102
+
103
+ for (long i = 0; i < tags_count; i++) {
104
+ VALUE name_value_pair = rb_ary_entry(tags_as_array, i);
105
+
106
+ if (!RB_TYPE_P(name_value_pair, T_ARRAY)) {
107
+ ddog_Vec_Tag_drop(tags);
108
+ ENFORCE_TYPE(name_value_pair, T_ARRAY);
109
+ }
110
+
111
+ // Note: We can index the array without checking its size first because rb_ary_entry returns Qnil if out of bounds
112
+ VALUE tag_name = rb_ary_entry(name_value_pair, 0);
113
+ VALUE tag_value = rb_ary_entry(name_value_pair, 1);
114
+
115
+ if (!(RB_TYPE_P(tag_name, T_STRING) && RB_TYPE_P(tag_value, T_STRING))) {
116
+ ddog_Vec_Tag_drop(tags);
117
+ ENFORCE_TYPE(tag_name, T_STRING);
118
+ ENFORCE_TYPE(tag_value, T_STRING);
119
+ }
120
+
121
+ ddog_Vec_Tag_PushResult push_result =
122
+ ddog_Vec_Tag_push(&tags, char_slice_from_ruby_string(tag_name), char_slice_from_ruby_string(tag_value));
123
+
124
+ if (push_result.tag == DDOG_VEC_TAG_PUSH_RESULT_ERR) {
125
+ // libdatadog validates tags and may catch invalid tags that ddtrace didn't actually catch.
126
+ // We warn users about such tags, and then just ignore them.
127
+
128
+ int exception_state;
129
+ rb_protect(log_failure_to_process_tag, get_error_details_and_drop(&push_result.err), &exception_state);
130
+
131
+ // Since we are calling into Ruby code, it may raise an exception. Ensure that dynamically-allocated tags
132
+ // get cleaned before propagating the exception.
133
+ if (exception_state) {
134
+ ddog_Vec_Tag_drop(tags);
135
+ rb_jump_tag(exception_state); // "Re-raise" exception
136
+ }
137
+ }
138
+ }
139
+
140
+ return tags;
141
+ }
142
+
143
+ static VALUE log_failure_to_process_tag(VALUE err_details) {
144
+ VALUE datadog_module = rb_const_get(rb_cObject, rb_intern("Datadog"));
145
+ VALUE logger = rb_funcall(datadog_module, rb_intern("logger"), 0);
146
+
147
+ return rb_funcall(logger, rb_intern("warn"), 1, rb_sprintf("Failed to add tag to profiling request: %"PRIsVALUE, err_details));
148
+ }
@@ -40,3 +40,7 @@ ddog_CharSlice ruby_value_type_to_char_slice(enum ruby_value_type type);
40
40
  inline static char* string_from_char_slice(ddog_CharSlice slice) {
41
41
  return ruby_strndup(slice.ptr, slice.len);
42
42
  }
43
+
44
+ ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration);
45
+
46
+ ddog_Vec_Tag convert_tags(VALUE tags_as_array);
@@ -15,7 +15,7 @@ module Datadog
15
15
  # The MJIT header was introduced on 2.6 and removed on 3.3; for other Rubies we rely on debase-ruby_core_source
16
16
  CAN_USE_MJIT_HEADER = RUBY_VERSION.start_with?('2.6', '2.7', '3.0.', '3.1.', '3.2.')
17
17
 
18
- LIBDATADOG_VERSION = '~> 6.0.0.2.0'
18
+ LIBDATADOG_VERSION = '~> 9.0.0.1.0'
19
19
 
20
20
  def self.fail_install_if_missing_extension?
21
21
  ENV[ENV_FAIL_INSTALL_IF_MISSING_EXTENSION].to_s.strip.downcase == 'true'
@@ -86,7 +86,6 @@ module Datadog
86
86
  on_macos? ||
87
87
  on_unknown_os? ||
88
88
  on_unsupported_cpu_arch? ||
89
- on_unsupported_ruby_version? ||
90
89
  expected_to_use_mjit_but_mjit_is_disabled? ||
91
90
  libdatadog_not_available? ||
92
91
  libdatadog_not_usable?
@@ -166,7 +165,7 @@ module Datadog
166
165
 
167
166
  # Validation for this check is done in extconf.rb because it relies on mkmf
168
167
  PKG_CONFIG_IS_MISSING = explain_issue(
169
- #+-----------------------------------------------------------------------------+
168
+ # ----------------------------------------------------------------------------+
170
169
  'the `pkg-config` system tool is missing.',
171
170
  'This issue can usually be fixed by installing one of the following:',
172
171
  'the `pkg-config` package on Homebrew and Debian/Ubuntu-based Linux;',
@@ -260,15 +259,6 @@ module Datadog
260
259
  architecture_not_supported unless RUBY_PLATFORM.start_with?('x86_64', 'aarch64', 'arm64')
261
260
  end
262
261
 
263
- private_class_method def self.on_unsupported_ruby_version?
264
- ruby_version_not_supported = explain_issue(
265
- 'the profiler only supports Ruby 2.3 or newer.',
266
- suggested: UPGRADE_RUBY,
267
- )
268
-
269
- ruby_version_not_supported if RUBY_VERSION.start_with?('2.1.', '2.2.')
270
- end
271
-
272
262
  # On some Rubies, we require the mjit header to be present. If Ruby was installed without MJIT support, we also skip
273
263
  # building the extension.
274
264
  private_class_method def self.expected_to_use_mjit_but_mjit_is_disabled?