datadog 2.2.0 → 2.3.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -2
  3. data/ext/datadog_profiling_loader/extconf.rb +15 -15
  4. data/ext/datadog_profiling_native_extension/clock_id.h +1 -0
  5. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -2
  6. data/ext/datadog_profiling_native_extension/clock_id_noop.c +1 -2
  7. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +113 -43
  8. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +49 -26
  9. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +34 -4
  10. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +4 -0
  11. data/ext/datadog_profiling_native_extension/collectors_stack.c +49 -37
  12. data/ext/datadog_profiling_native_extension/collectors_stack.h +2 -2
  13. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +81 -19
  14. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +1 -0
  15. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +110 -0
  16. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +57 -0
  17. data/ext/datadog_profiling_native_extension/extconf.rb +65 -60
  18. data/ext/datadog_profiling_native_extension/heap_recorder.c +34 -6
  19. data/ext/datadog_profiling_native_extension/heap_recorder.h +3 -1
  20. data/ext/datadog_profiling_native_extension/helpers.h +6 -17
  21. data/ext/datadog_profiling_native_extension/http_transport.c +3 -3
  22. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +0 -86
  23. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +2 -23
  24. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +61 -172
  25. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +64 -138
  26. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +17 -11
  27. data/ext/datadog_profiling_native_extension/profiling.c +0 -2
  28. data/ext/datadog_profiling_native_extension/ruby_helpers.c +0 -33
  29. data/ext/datadog_profiling_native_extension/ruby_helpers.h +1 -26
  30. data/ext/datadog_profiling_native_extension/setup_signal_handler.h +1 -0
  31. data/ext/datadog_profiling_native_extension/stack_recorder.c +14 -2
  32. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -0
  33. data/ext/datadog_profiling_native_extension/time_helpers.c +0 -15
  34. data/ext/datadog_profiling_native_extension/time_helpers.h +36 -6
  35. data/ext/{datadog_profiling_native_extension → libdatadog_api}/crashtracker.c +19 -6
  36. data/ext/libdatadog_api/datadog_ruby_common.c +110 -0
  37. data/ext/libdatadog_api/datadog_ruby_common.h +57 -0
  38. data/ext/libdatadog_api/extconf.rb +108 -0
  39. data/ext/libdatadog_api/macos_development.md +26 -0
  40. data/ext/libdatadog_extconf_helpers.rb +130 -0
  41. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +49 -0
  42. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +73 -0
  43. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +68 -0
  44. data/lib/datadog/appsec/contrib/graphql/integration.rb +41 -0
  45. data/lib/datadog/appsec/contrib/graphql/patcher.rb +37 -0
  46. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +59 -0
  47. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +1 -1
  48. data/lib/datadog/appsec/processor/actions.rb +1 -1
  49. data/lib/datadog/appsec/response.rb +15 -1
  50. data/lib/datadog/appsec.rb +1 -0
  51. data/lib/datadog/core/configuration/components.rb +14 -12
  52. data/lib/datadog/core/configuration/settings.rb +54 -7
  53. data/lib/datadog/core/crashtracking/agent_base_url.rb +21 -0
  54. data/lib/datadog/core/crashtracking/component.rb +111 -0
  55. data/lib/datadog/core/crashtracking/tag_builder.rb +39 -0
  56. data/lib/datadog/core/diagnostics/environment_logger.rb +8 -11
  57. data/lib/datadog/core/telemetry/component.rb +49 -2
  58. data/lib/datadog/core/telemetry/emitter.rb +9 -11
  59. data/lib/datadog/core/telemetry/event.rb +32 -1
  60. data/lib/datadog/core/telemetry/ext.rb +1 -0
  61. data/lib/datadog/core/telemetry/http/adapters/net.rb +10 -12
  62. data/lib/datadog/core/telemetry/http/ext.rb +3 -0
  63. data/lib/datadog/core/telemetry/http/transport.rb +38 -9
  64. data/lib/datadog/core/telemetry/logging.rb +35 -0
  65. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +102 -0
  66. data/lib/datadog/kit/appsec/events.rb +2 -4
  67. data/lib/datadog/opentelemetry/sdk/span_processor.rb +10 -0
  68. data/lib/datadog/opentelemetry/sdk/trace/span.rb +23 -0
  69. data/lib/datadog/profiling/collectors/code_provenance.rb +7 -7
  70. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +17 -17
  71. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +11 -13
  72. data/lib/datadog/profiling/collectors/info.rb +3 -3
  73. data/lib/datadog/profiling/collectors/thread_context.rb +4 -2
  74. data/lib/datadog/profiling/component.rb +69 -91
  75. data/lib/datadog/profiling/exporter.rb +3 -3
  76. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  77. data/lib/datadog/profiling/ext.rb +21 -21
  78. data/lib/datadog/profiling/flush.rb +1 -1
  79. data/lib/datadog/profiling/http_transport.rb +8 -6
  80. data/lib/datadog/profiling/load_native_extension.rb +5 -5
  81. data/lib/datadog/profiling/preload.rb +1 -1
  82. data/lib/datadog/profiling/profiler.rb +5 -8
  83. data/lib/datadog/profiling/scheduler.rb +31 -25
  84. data/lib/datadog/profiling/tag_builder.rb +2 -2
  85. data/lib/datadog/profiling/tasks/exec.rb +5 -5
  86. data/lib/datadog/profiling/tasks/setup.rb +16 -35
  87. data/lib/datadog/profiling.rb +4 -5
  88. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  89. data/lib/datadog/tracing/contrib/ext.rb +14 -0
  90. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +1 -1
  91. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +4 -1
  92. data/lib/datadog/tracing/contrib/lograge/patcher.rb +16 -0
  93. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +5 -0
  94. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +17 -13
  95. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +5 -0
  96. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +4 -1
  97. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +28 -0
  98. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  99. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +22 -10
  100. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +5 -0
  101. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +4 -1
  102. data/lib/datadog/tracing/diagnostics/environment_logger.rb +14 -16
  103. data/lib/datadog/tracing/metadata/errors.rb +9 -1
  104. data/lib/datadog/tracing/metadata/ext.rb +4 -0
  105. data/lib/datadog/tracing/pipeline/span_filter.rb +2 -2
  106. data/lib/datadog/tracing/span.rb +9 -2
  107. data/lib/datadog/tracing/span_event.rb +41 -0
  108. data/lib/datadog/tracing/span_operation.rb +6 -2
  109. data/lib/datadog/tracing/transport/serializable_trace.rb +3 -0
  110. data/lib/datadog/version.rb +1 -1
  111. metadata +28 -10
  112. data/lib/datadog/profiling/crashtracker.rb +0 -91
  113. data/lib/datadog/profiling/ext/forking.rb +0 -98
@@ -20,6 +20,9 @@
20
20
 
21
21
  #define EMA_SMOOTHING_FACTOR 0.6
22
22
 
23
+ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now_ns);
24
+ static inline bool should_readjust(discrete_dynamic_sampler *sampler, coarse_instant now);
25
+
23
26
  void discrete_dynamic_sampler_init(discrete_dynamic_sampler *sampler, const char *debug_name, long now_ns) {
24
27
  sampler->debug_name = debug_name;
25
28
  discrete_dynamic_sampler_set_overhead_target_percentage(sampler, BASE_OVERHEAD_PCT, now_ns);
@@ -54,27 +57,25 @@ void discrete_dynamic_sampler_set_overhead_target_percentage(discrete_dynamic_sa
54
57
  return discrete_dynamic_sampler_reset(sampler, now_ns);
55
58
  }
56
59
 
57
- static void maybe_readjust(discrete_dynamic_sampler *sampler, long now);
58
-
59
- bool discrete_dynamic_sampler_should_sample(discrete_dynamic_sampler *sampler, long now_ns) {
60
+ // NOTE: See header for an explanation of when this should get used
61
+ __attribute__((warn_unused_result))
62
+ bool discrete_dynamic_sampler_should_sample(discrete_dynamic_sampler *sampler) {
60
63
  // For efficiency reasons we don't do true random sampling but rather systematic
61
64
  // sampling following a sample interval/skip. This can be biased and hide patterns
62
- // but the dynamic interval and rather indeterministic pattern of allocations in
65
+ // but the dynamic interval and rather nondeterministic pattern of allocations in
63
66
  // most real applications should help reduce the bias impact.
64
67
  sampler->events_since_last_sample++;
65
68
  sampler->events_since_last_readjustment++;
66
- bool should_sample = sampler->sampling_interval > 0 && sampler->events_since_last_sample >= sampler->sampling_interval;
67
69
 
68
- if (should_sample) {
69
- sampler->sample_start_time_ns = now_ns;
70
- } else {
71
- // check if we should readjust our sampler after this event, even if we didn't sample it
72
- maybe_readjust(sampler, now_ns);
73
- }
70
+ return sampler->sampling_interval > 0 && sampler->events_since_last_sample >= sampler->sampling_interval;
71
+ }
74
72
 
75
- return should_sample;
73
+ // NOTE: See header for an explanation of when this should get used
74
+ void discrete_dynamic_sampler_before_sample(discrete_dynamic_sampler *sampler, long now_ns) {
75
+ sampler->sample_start_time_ns = now_ns;
76
76
  }
77
77
 
78
+ // NOTE: See header for an explanation of when this should get used
78
79
  long discrete_dynamic_sampler_after_sample(discrete_dynamic_sampler *sampler, long now_ns) {
79
80
  long last_sampling_time_ns = sampler->sample_start_time_ns == 0 ? 0 : long_max_of(0, now_ns - sampler->sample_start_time_ns);
80
81
  sampler->samples_since_last_readjustment++;
@@ -95,6 +96,11 @@ size_t discrete_dynamic_sampler_events_since_last_sample(discrete_dynamic_sample
95
96
  return sampler->events_since_last_sample;
96
97
  }
97
98
 
99
+ // NOTE: See header for an explanation of when this should get used
100
+ bool discrete_dynamic_sampler_skipped_sample(discrete_dynamic_sampler *sampler, coarse_instant now) {
101
+ return should_readjust(sampler, now);
102
+ }
103
+
98
104
  static double ewma_adj_window(double latest_value, double avg, long current_window_time_ns, bool is_first) {
99
105
  if (is_first) {
100
106
  return latest_value;
@@ -109,19 +115,27 @@ static double ewma_adj_window(double latest_value, double avg, long current_wind
109
115
  return (1-alpha) * avg + alpha * latest_value;
110
116
  }
111
117
 
112
- static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
113
- long this_window_time_ns = sampler->last_readjust_time_ns == 0 ? ADJUSTMENT_WINDOW_NS : now - sampler->last_readjust_time_ns;
118
+ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now_ns) {
119
+ if (should_readjust(sampler, to_coarse_instant(now_ns))) discrete_dynamic_sampler_readjust(sampler, now_ns);
120
+ }
121
+
122
+ static inline bool should_readjust(discrete_dynamic_sampler *sampler, coarse_instant now) {
123
+ long this_window_time_ns =
124
+ sampler->last_readjust_time_ns == 0 ? ADJUSTMENT_WINDOW_NS : now.timestamp_ns - sampler->last_readjust_time_ns;
114
125
 
115
126
  bool should_readjust_based_on_time = this_window_time_ns >= ADJUSTMENT_WINDOW_NS;
116
127
  bool should_readjust_based_on_samples = sampler->samples_since_last_readjustment >= ADJUSTMENT_WINDOW_SAMPLES;
117
128
 
118
- if (!should_readjust_based_on_time && !should_readjust_based_on_samples) {
119
- // not enough time or samples have passed to perform a readjustment
120
- return;
121
- }
129
+ return should_readjust_based_on_time || should_readjust_based_on_samples;
130
+ }
131
+
132
+ // NOTE: This method ASSUMES should_readjust == true
133
+ // NOTE: See header for an explanation of when this should get used
134
+ void discrete_dynamic_sampler_readjust(discrete_dynamic_sampler *sampler, long now_ns) {
135
+ long this_window_time_ns = sampler->last_readjust_time_ns == 0 ? ADJUSTMENT_WINDOW_NS : now_ns - sampler->last_readjust_time_ns;
122
136
 
123
- if (this_window_time_ns == 0) {
124
- // should not be possible given previous condition but lets protect against div by 0 below.
137
+ if (this_window_time_ns <= 0) {
138
+ // should not be possible given should_readjust but lets protect against div by 0 below.
125
139
  return;
126
140
  }
127
141
 
@@ -143,7 +157,7 @@ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
143
157
  // Lets update our average sampling time per event
144
158
  long avg_sampling_time_in_window_ns = sampler->samples_since_last_readjustment == 0 ? 0 : sampler->sampling_time_since_last_readjustment_ns / sampler->samples_since_last_readjustment;
145
159
  if (avg_sampling_time_in_window_ns > sampler->max_sampling_time_ns) {
146
- // If the average sampling time in the previous window was deemed unnacceptable, clamp it to the
160
+ // If the average sampling time in the previous window was deemed unacceptable, clamp it to the
147
161
  // maximum acceptable value and register this operation in our counter.
148
162
  // NOTE: This is important so that events like suspensions or system overloads do not lead us to
149
163
  // learn arbitrarily big sampling times which may then result in us not sampling anything
@@ -255,9 +269,7 @@ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
255
269
  double num_this_windows_in_60s = 60 * 1e9 / this_window_time_ns;
256
270
  double real_total_sampling_time_in_60s = sampler->sampling_time_since_last_readjustment_ns * num_this_windows_in_60s / 1e9;
257
271
 
258
- const char* readjustment_reason = should_readjust_based_on_time ? "time" : "samples";
259
-
260
- fprintf(stderr, "[dds.%s] readjusting due to %s...\n", sampler->debug_name, readjustment_reason);
272
+ fprintf(stderr, "[dds.%s] readjusting...\n", sampler->debug_name);
261
273
  fprintf(stderr, "events_since_last_readjustment=%ld\n", sampler->events_since_last_readjustment);
262
274
  fprintf(stderr, "samples_since_last_readjustment=%ld\n", sampler->samples_since_last_readjustment);
263
275
  fprintf(stderr, "this_window_time=%ld\n", this_window_time_ns);
@@ -286,7 +298,7 @@ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
286
298
  sampler->events_since_last_readjustment = 0;
287
299
  sampler->samples_since_last_readjustment = 0;
288
300
  sampler->sampling_time_since_last_readjustment_ns = 0;
289
- sampler->last_readjust_time_ns = now;
301
+ sampler->last_readjust_time_ns = now_ns;
290
302
  sampler->has_completed_full_adjustment_window = true;
291
303
  }
292
304
 
@@ -359,6 +371,10 @@ static VALUE _native_new(VALUE klass) {
359
371
  }
360
372
  discrete_dynamic_sampler_init(&state->sampler, "test sampler", now_ns);
361
373
 
374
+ // Note: As of this writing, no new Ruby objects get created and stored in the state. If that ever changes, remember
375
+ // to keep them on the stack and mark them with RB_GC_GUARD -- otherwise it's possible for a GC to run and
376
+ // since the instance representing the state does not yet exist, such objects will not get marked.
377
+
362
378
  return TypedData_Wrap_Struct(klass, &sampler_typed_data, state);
363
379
  }
364
380
 
@@ -402,7 +418,14 @@ VALUE _native_should_sample(VALUE self, VALUE now_ns) {
402
418
  sampler_state *state;
403
419
  TypedData_Get_Struct(self, sampler_state, &sampler_typed_data, state);
404
420
 
405
- return discrete_dynamic_sampler_should_sample(&state->sampler, NUM2LONG(now_ns)) ? Qtrue : Qfalse;
421
+ if (discrete_dynamic_sampler_should_sample(&state->sampler)) {
422
+ discrete_dynamic_sampler_before_sample(&state->sampler, NUM2LONG(now_ns));
423
+ return Qtrue;
424
+ } else {
425
+ bool needs_readjust = discrete_dynamic_sampler_skipped_sample(&state->sampler, to_coarse_instant(NUM2LONG(now_ns)));
426
+ if (needs_readjust) discrete_dynamic_sampler_readjust(&state->sampler, NUM2LONG(now_ns));
427
+ return Qfalse;
428
+ }
406
429
  }
407
430
 
408
431
  VALUE _native_after_sample(VALUE self, VALUE now_ns) {
@@ -5,6 +5,8 @@
5
5
 
6
6
  #include <ruby.h>
7
7
 
8
+ #include "time_helpers.h"
9
+
8
10
  // A sampler that will sample discrete events based on the overhead of their
9
11
  // sampling.
10
12
  //
@@ -62,7 +64,6 @@ typedef struct discrete_dynamic_sampler {
62
64
  unsigned long sampling_time_clamps;
63
65
  } discrete_dynamic_sampler;
64
66
 
65
-
66
67
  // Init a new sampler with sane defaults.
67
68
  void discrete_dynamic_sampler_init(discrete_dynamic_sampler *sampler, const char *debug_name, long now_ns);
68
69
 
@@ -80,9 +81,38 @@ void discrete_dynamic_sampler_set_overhead_target_percentage(discrete_dynamic_sa
80
81
  // @return True if the event associated with this decision should be sampled, false
81
82
  // otherwise.
82
83
  //
83
- // NOTE: If true is returned we implicitly assume the start of a sampling operation
84
- // and it is expected that a follow-up after_sample call is issued.
85
- bool discrete_dynamic_sampler_should_sample(discrete_dynamic_sampler *sampler, long now_ns);
84
+ // IMPORTANT: A call to this method MUST be followed by a call to either
85
+ // `discrete_dynamic_sampler_before_sample` if return is `true` or
86
+ // `discrete_dynamic_sampler_skipped_sample` if return is `false`.
87
+ //
88
+ // In the past, this method took care of before_sample/skipped_sample/readjust as well.
89
+ // We've had to split it up because we wanted to both use coarse time and regular monotonic time depending on the
90
+ // situation, but also wanted time to come as an argument from the outside.
91
+ //
92
+ // TL;DR here's how they should be used as Ruby code:
93
+ // if discrete_dynamic_sampler_should_sample
94
+ // discrete_dynamic_sampler_before_sample(monotonic_wall_time_now_ns())
95
+ // else
96
+ // needs_readjust = discrete_dynamic_sampler_skipped_sample(monotonic_coarse_wall_time_now_ns())
97
+ // discrete_dynamic_sampler_readjust(monotonic_wall_time_now_ns()) if needs_readjust
98
+ // end
99
+ __attribute__((warn_unused_result))
100
+ bool discrete_dynamic_sampler_should_sample(discrete_dynamic_sampler *sampler);
101
+
102
+ // Signal the start of a sampling operation.
103
+ // MUST be called after `discrete_dynamic_sampler_should_sample` returns `true`.
104
+ void discrete_dynamic_sampler_before_sample(discrete_dynamic_sampler *sampler, long now_ns);
105
+
106
+ // Signals that sampling did not happen
107
+ // MUST be called after `discrete_dynamic_sampler_skipped_sample` returns `false`.
108
+ //
109
+ // @return True if the sampler needs to be readjusted.
110
+ //
111
+ // IMPORTANT: A call to this method MUST be followed by a call to `discrete_dynamic_sampler_readjust` if return is `true`.
112
+ __attribute__((warn_unused_result))
113
+ bool discrete_dynamic_sampler_skipped_sample(discrete_dynamic_sampler *sampler, coarse_instant now);
114
+
115
+ void discrete_dynamic_sampler_readjust(discrete_dynamic_sampler *sampler, long now_ns);
86
116
 
87
117
  // Signal the end of a sampling operation.
88
118
  //
@@ -83,6 +83,10 @@ static VALUE _native_new(VALUE klass) {
83
83
 
84
84
  reset_state(state);
85
85
 
86
+ // Note: As of this writing, no new Ruby objects get created and stored in the state. If that ever changes, remember
87
+ // to keep them on the stack and mark them with RB_GC_GUARD -- otherwise it's possible for a GC to run and
88
+ // since the instance representing the state does not yet exist, such objects will not get marked.
89
+
86
90
  return TypedData_Wrap_Struct(klass, &idle_sampling_helper_typed_data, state);
87
91
  }
88
92
 
@@ -15,11 +15,9 @@ static VALUE missing_string = Qnil;
15
15
 
16
16
  // Used as scratch space during sampling
17
17
  struct sampling_buffer {
18
- unsigned int max_frames;
19
- VALUE *stack_buffer;
20
- int *lines_buffer;
21
- bool *is_ruby_frame;
18
+ uint16_t max_frames;
22
19
  ddog_prof_Location *locations;
20
+ frame_info *stack_buffer;
23
21
  }; // Note: typedef'd in the header to sampling_buffer
24
22
 
25
23
  static VALUE _native_sample(
@@ -33,9 +31,14 @@ static VALUE _native_sample(
33
31
  VALUE in_gc
34
32
  );
35
33
  static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer* buffer, char *frames_omitted_message, int frames_omitted_message_size);
36
- static void record_placeholder_stack_in_native_code(sampling_buffer* buffer, VALUE recorder_instance, sample_values values, sample_labels labels);
34
+ static void record_placeholder_stack_in_native_code(VALUE recorder_instance, sample_values values, sample_labels labels);
37
35
  static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_CharSlice *filename_slice);
38
36
 
37
+ // These two functions are exposed as symbols by the VM but are not in any header.
38
+ // Their signatures actually take a `const rb_iseq_t *iseq` but it gets casted back and forth between VALUE.
39
+ extern VALUE rb_iseq_path(const VALUE);
40
+ extern VALUE rb_iseq_base_label(const VALUE);
41
+
39
42
  void collectors_stack_init(VALUE profiling_module) {
40
43
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
41
44
  VALUE collectors_stack_class = rb_define_class_under(collectors_module, "Stack", rb_cObject);
@@ -65,6 +68,8 @@ static VALUE _native_sample(
65
68
  ENFORCE_TYPE(numeric_labels_array, T_ARRAY);
66
69
 
67
70
  VALUE zero = INT2NUM(0);
71
+ VALUE heap_sample = rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("heap_sample"), Qfalse);
72
+ ENFORCE_BOOLEAN(heap_sample);
68
73
  sample_values values = {
69
74
  .cpu_time_ns = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("cpu-time"), zero)),
70
75
  .cpu_or_wall_samples = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("cpu-samples"), zero)),
@@ -72,6 +77,7 @@ static VALUE _native_sample(
72
77
  .alloc_samples = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("alloc-samples"), zero)),
73
78
  .alloc_samples_unscaled = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("alloc-samples-unscaled"), zero)),
74
79
  .timeline_wall_time_ns = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("timeline"), zero)),
80
+ .heap_sample = heap_sample == Qtrue,
75
81
  };
76
82
 
77
83
  long labels_count = RARRAY_LEN(labels_array) + RARRAY_LEN(numeric_labels_array);
@@ -102,13 +108,13 @@ static VALUE _native_sample(
102
108
  int max_frames_requested = NUM2INT(max_frames);
103
109
  if (max_frames_requested < 0) rb_raise(rb_eArgError, "Invalid max_frames: value must not be negative");
104
110
 
105
- sampling_buffer *buffer = sampling_buffer_new(max_frames_requested);
111
+ ddog_prof_Location *locations = ruby_xcalloc(max_frames_requested, sizeof(ddog_prof_Location));
112
+ sampling_buffer *buffer = sampling_buffer_new(max_frames_requested, locations);
106
113
 
107
114
  ddog_prof_Slice_Label slice_labels = {.ptr = labels, .len = labels_count};
108
115
 
109
116
  if (in_gc == Qtrue) {
110
117
  record_placeholder_stack(
111
- buffer,
112
118
  recorder_instance,
113
119
  values,
114
120
  (sample_labels) {.labels = slice_labels, .state_label = state_label},
@@ -124,6 +130,7 @@ static VALUE _native_sample(
124
130
  );
125
131
  }
126
132
 
133
+ ruby_xfree(locations);
127
134
  sampling_buffer_free(buffer);
128
135
 
129
136
  return Qtrue;
@@ -151,22 +158,28 @@ void sample_thread(
151
158
  thread,
152
159
  0 /* stack starting depth */,
153
160
  buffer->max_frames,
154
- buffer->stack_buffer,
155
- buffer->lines_buffer,
156
- buffer->is_ruby_frame
161
+ buffer->stack_buffer
157
162
  );
158
163
 
159
164
  if (captured_frames == PLACEHOLDER_STACK_IN_NATIVE_CODE) {
160
- record_placeholder_stack_in_native_code(buffer, recorder_instance, values, labels);
165
+ record_placeholder_stack_in_native_code(recorder_instance, values, labels);
161
166
  return;
162
167
  }
163
168
 
169
+ // if (captured_frames > 0) {
170
+ // int cache_hits = 0;
171
+ // for (int i = 0; i < captured_frames; i++) {
172
+ // if (buffer->stack_buffer[i].same_frame) cache_hits++;
173
+ // }
174
+ // fprintf(stderr, "Sampling cache hits: %f\n", ((double) cache_hits / captured_frames) * 100);
175
+ // }
176
+
164
177
  // Ruby does not give us path and line number for methods implemented using native code.
165
178
  // The convention in Kernel#caller_locations is to instead use the path and line number of the first Ruby frame
166
179
  // on the stack that is below (e.g. directly or indirectly has called) the native method.
167
180
  // Thus, we keep that frame here to able to replicate that behavior.
168
- // (This is why we also iterate the sampling buffers backwards below -- so that it's easier to keep the last_ruby_frame)
169
- VALUE last_ruby_frame = Qnil;
181
+ // (This is why we also iterate the sampling buffers backwards below -- so that it's easier to keep the last_ruby_frame_filename)
182
+ VALUE last_ruby_frame_filename = Qnil;
170
183
  int last_ruby_line = 0;
171
184
 
172
185
  ddog_prof_Label *state_label = labels.state_label;
@@ -182,16 +195,16 @@ void sample_thread(
182
195
  VALUE name, filename;
183
196
  int line;
184
197
 
185
- if (buffer->is_ruby_frame[i]) {
186
- last_ruby_frame = buffer->stack_buffer[i];
187
- last_ruby_line = buffer->lines_buffer[i];
198
+ if (buffer->stack_buffer[i].is_ruby_frame) {
199
+ name = rb_iseq_base_label(buffer->stack_buffer[i].as.ruby_frame.iseq);
200
+ filename = rb_iseq_path(buffer->stack_buffer[i].as.ruby_frame.iseq);
201
+ line = buffer->stack_buffer[i].as.ruby_frame.line;
188
202
 
189
- name = rb_profile_frame_base_label(buffer->stack_buffer[i]);
190
- filename = rb_profile_frame_path(buffer->stack_buffer[i]);
191
- line = buffer->lines_buffer[i];
203
+ last_ruby_frame_filename = filename;
204
+ last_ruby_line = line;
192
205
  } else {
193
- name = ddtrace_rb_profile_frame_method_name(buffer->stack_buffer[i]);
194
- filename = NIL_P(last_ruby_frame) ? Qnil : rb_profile_frame_path(last_ruby_frame);
206
+ name = rb_id2str(buffer->stack_buffer[i].as.native_frame.method_id);
207
+ filename = last_ruby_frame_filename;
195
208
  line = last_ruby_line;
196
209
  }
197
210
 
@@ -211,7 +224,7 @@ void sample_thread(
211
224
  // approximation, and in the future we hope to replace this with a more accurate approach (such as using the
212
225
  // GVL instrumentation API.)
213
226
  if (top_of_the_stack && only_wall_time) {
214
- if (!buffer->is_ruby_frame[i]) {
227
+ if (!buffer->stack_buffer[i].is_ruby_frame) {
215
228
  // We know that known versions of Ruby implement these using native code; thus if we find a method with the
216
229
  // same name that is not native code, we ignore it, as it's probably a user method that coincidentally
217
230
  // has the same name. Thus, even though "matching just by method name" is kinda weak,
@@ -352,13 +365,11 @@ static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer*
352
365
  // with one containing a placeholder frame, so that these threads are properly represented in the UX.
353
366
 
354
367
  static void record_placeholder_stack_in_native_code(
355
- sampling_buffer* buffer,
356
368
  VALUE recorder_instance,
357
369
  sample_values values,
358
370
  sample_labels labels
359
371
  ) {
360
372
  record_placeholder_stack(
361
- buffer,
362
373
  recorder_instance,
363
374
  values,
364
375
  labels,
@@ -367,36 +378,39 @@ static void record_placeholder_stack_in_native_code(
367
378
  }
368
379
 
369
380
  void record_placeholder_stack(
370
- sampling_buffer* buffer,
371
381
  VALUE recorder_instance,
372
382
  sample_values values,
373
383
  sample_labels labels,
374
384
  ddog_CharSlice placeholder_stack
375
385
  ) {
376
- ddog_prof_Function placeholder = {.name = DDOG_CHARSLICE_C(""), .filename = placeholder_stack};
377
- buffer->locations[0] = (ddog_prof_Location) {.function = placeholder, .line = 0};
386
+ ddog_prof_Location placeholder_location = {
387
+ .function = {.name = DDOG_CHARSLICE_C(""), .filename = placeholder_stack},
388
+ .line = 0,
389
+ };
378
390
 
379
391
  record_sample(
380
392
  recorder_instance,
381
- (ddog_prof_Slice_Location) {.ptr = buffer->locations, .len = 1},
393
+ (ddog_prof_Slice_Location) {.ptr = &placeholder_location, .len = 1},
382
394
  values,
383
395
  labels
384
396
  );
385
397
  }
386
398
 
387
- sampling_buffer *sampling_buffer_new(unsigned int max_frames) {
399
+ uint16_t sampling_buffer_check_max_frames(int max_frames) {
388
400
  if (max_frames < 5) rb_raise(rb_eArgError, "Invalid max_frames: value must be >= 5");
389
401
  if (max_frames > MAX_FRAMES_LIMIT) rb_raise(rb_eArgError, "Invalid max_frames: value must be <= " MAX_FRAMES_LIMIT_AS_STRING);
402
+ return max_frames;
403
+ }
404
+
405
+ sampling_buffer *sampling_buffer_new(uint16_t max_frames, ddog_prof_Location *locations) {
406
+ sampling_buffer_check_max_frames(max_frames);
390
407
 
391
408
  // Note: never returns NULL; if out of memory, it calls the Ruby out-of-memory handlers
392
409
  sampling_buffer* buffer = ruby_xcalloc(1, sizeof(sampling_buffer));
393
410
 
394
411
  buffer->max_frames = max_frames;
395
-
396
- buffer->stack_buffer = ruby_xcalloc(max_frames, sizeof(VALUE));
397
- buffer->lines_buffer = ruby_xcalloc(max_frames, sizeof(int));
398
- buffer->is_ruby_frame = ruby_xcalloc(max_frames, sizeof(bool));
399
- buffer->locations = ruby_xcalloc(max_frames, sizeof(ddog_prof_Location));
412
+ buffer->locations = locations;
413
+ buffer->stack_buffer = ruby_xcalloc(max_frames, sizeof(frame_info));
400
414
 
401
415
  return buffer;
402
416
  }
@@ -404,10 +418,8 @@ sampling_buffer *sampling_buffer_new(unsigned int max_frames) {
404
418
  void sampling_buffer_free(sampling_buffer *buffer) {
405
419
  if (buffer == NULL) rb_raise(rb_eArgError, "sampling_buffer_free called with NULL buffer");
406
420
 
421
+ // buffer->locations are owned by whoever called sampling_buffer_new, not us
407
422
  ruby_xfree(buffer->stack_buffer);
408
- ruby_xfree(buffer->lines_buffer);
409
- ruby_xfree(buffer->is_ruby_frame);
410
- ruby_xfree(buffer->locations);
411
423
 
412
424
  ruby_xfree(buffer);
413
425
  }
@@ -17,11 +17,11 @@ void sample_thread(
17
17
  sample_labels labels
18
18
  );
19
19
  void record_placeholder_stack(
20
- sampling_buffer* buffer,
21
20
  VALUE recorder_instance,
22
21
  sample_values values,
23
22
  sample_labels labels,
24
23
  ddog_CharSlice placeholder_stack
25
24
  );
26
- sampling_buffer *sampling_buffer_new(unsigned int max_frames);
25
+ uint16_t sampling_buffer_check_max_frames(int max_frames);
26
+ sampling_buffer *sampling_buffer_new(uint16_t max_frames, ddog_prof_Location *locations);
27
27
  void sampling_buffer_free(sampling_buffer *buffer);