datadog 2.2.0 → 2.4.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -2
  3. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +9 -1
  4. data/ext/datadog_profiling_loader/extconf.rb +14 -26
  5. data/ext/datadog_profiling_native_extension/clock_id.h +1 -0
  6. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +1 -2
  7. data/ext/datadog_profiling_native_extension/clock_id_noop.c +1 -2
  8. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +257 -69
  9. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +53 -28
  10. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +34 -4
  11. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +4 -0
  12. data/ext/datadog_profiling_native_extension/collectors_stack.c +136 -81
  13. data/ext/datadog_profiling_native_extension/collectors_stack.h +2 -2
  14. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +661 -48
  15. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +10 -1
  16. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +83 -0
  17. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +53 -0
  18. data/ext/datadog_profiling_native_extension/extconf.rb +91 -69
  19. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +50 -0
  20. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +75 -0
  21. data/ext/datadog_profiling_native_extension/heap_recorder.c +54 -12
  22. data/ext/datadog_profiling_native_extension/heap_recorder.h +3 -1
  23. data/ext/datadog_profiling_native_extension/helpers.h +6 -17
  24. data/ext/datadog_profiling_native_extension/http_transport.c +41 -9
  25. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +0 -86
  26. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +2 -23
  27. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +61 -172
  28. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +116 -139
  29. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +20 -11
  30. data/ext/datadog_profiling_native_extension/profiling.c +1 -3
  31. data/ext/datadog_profiling_native_extension/ruby_helpers.c +0 -33
  32. data/ext/datadog_profiling_native_extension/ruby_helpers.h +1 -26
  33. data/ext/datadog_profiling_native_extension/setup_signal_handler.h +1 -0
  34. data/ext/datadog_profiling_native_extension/stack_recorder.c +14 -2
  35. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -0
  36. data/ext/datadog_profiling_native_extension/time_helpers.c +0 -15
  37. data/ext/datadog_profiling_native_extension/time_helpers.h +36 -6
  38. data/ext/{datadog_profiling_native_extension → libdatadog_api}/crashtracker.c +37 -22
  39. data/ext/libdatadog_api/datadog_ruby_common.c +83 -0
  40. data/ext/libdatadog_api/datadog_ruby_common.h +53 -0
  41. data/ext/libdatadog_api/extconf.rb +108 -0
  42. data/ext/libdatadog_api/macos_development.md +26 -0
  43. data/ext/libdatadog_extconf_helpers.rb +130 -0
  44. data/lib/datadog/appsec/assets/waf_rules/recommended.json +2184 -108
  45. data/lib/datadog/appsec/assets/waf_rules/strict.json +1430 -2
  46. data/lib/datadog/appsec/component.rb +29 -8
  47. data/lib/datadog/appsec/configuration/settings.rb +2 -2
  48. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +1 -0
  49. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +21 -0
  50. data/lib/datadog/appsec/contrib/devise/patcher.rb +12 -2
  51. data/lib/datadog/appsec/contrib/graphql/appsec_trace.rb +35 -0
  52. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +109 -0
  53. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +71 -0
  54. data/lib/datadog/appsec/contrib/graphql/integration.rb +54 -0
  55. data/lib/datadog/appsec/contrib/graphql/patcher.rb +37 -0
  56. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +59 -0
  57. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +3 -6
  58. data/lib/datadog/appsec/event.rb +1 -1
  59. data/lib/datadog/appsec/processor/actions.rb +1 -1
  60. data/lib/datadog/appsec/processor/rule_loader.rb +3 -1
  61. data/lib/datadog/appsec/processor/rule_merger.rb +33 -15
  62. data/lib/datadog/appsec/processor.rb +36 -37
  63. data/lib/datadog/appsec/rate_limiter.rb +25 -40
  64. data/lib/datadog/appsec/remote.rb +7 -3
  65. data/lib/datadog/appsec/response.rb +15 -1
  66. data/lib/datadog/appsec.rb +3 -2
  67. data/lib/datadog/core/configuration/components.rb +18 -15
  68. data/lib/datadog/core/configuration/settings.rb +135 -9
  69. data/lib/datadog/core/crashtracking/agent_base_url.rb +21 -0
  70. data/lib/datadog/core/crashtracking/component.rb +111 -0
  71. data/lib/datadog/core/crashtracking/tag_builder.rb +39 -0
  72. data/lib/datadog/core/diagnostics/environment_logger.rb +8 -11
  73. data/lib/datadog/core/environment/execution.rb +5 -5
  74. data/lib/datadog/core/metrics/client.rb +7 -0
  75. data/lib/datadog/core/rate_limiter.rb +183 -0
  76. data/lib/datadog/core/remote/client/capabilities.rb +4 -3
  77. data/lib/datadog/core/remote/component.rb +4 -2
  78. data/lib/datadog/core/remote/negotiation.rb +4 -4
  79. data/lib/datadog/core/remote/tie.rb +2 -0
  80. data/lib/datadog/core/runtime/metrics.rb +1 -1
  81. data/lib/datadog/core/telemetry/component.rb +51 -2
  82. data/lib/datadog/core/telemetry/emitter.rb +9 -11
  83. data/lib/datadog/core/telemetry/event.rb +37 -1
  84. data/lib/datadog/core/telemetry/ext.rb +1 -0
  85. data/lib/datadog/core/telemetry/http/adapters/net.rb +10 -12
  86. data/lib/datadog/core/telemetry/http/ext.rb +3 -0
  87. data/lib/datadog/core/telemetry/http/transport.rb +38 -9
  88. data/lib/datadog/core/telemetry/logger.rb +51 -0
  89. data/lib/datadog/core/telemetry/logging.rb +71 -0
  90. data/lib/datadog/core/telemetry/request.rb +13 -1
  91. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +102 -0
  92. data/lib/datadog/core/utils/time.rb +12 -0
  93. data/lib/datadog/di/code_tracker.rb +168 -0
  94. data/lib/datadog/di/configuration/settings.rb +163 -0
  95. data/lib/datadog/di/configuration.rb +11 -0
  96. data/lib/datadog/di/error.rb +31 -0
  97. data/lib/datadog/di/extensions.rb +16 -0
  98. data/lib/datadog/di/probe.rb +133 -0
  99. data/lib/datadog/di/probe_builder.rb +41 -0
  100. data/lib/datadog/di/redactor.rb +188 -0
  101. data/lib/datadog/di/serializer.rb +193 -0
  102. data/lib/datadog/di.rb +14 -0
  103. data/lib/datadog/kit/appsec/events.rb +2 -4
  104. data/lib/datadog/opentelemetry/sdk/propagator.rb +2 -0
  105. data/lib/datadog/opentelemetry/sdk/span_processor.rb +10 -0
  106. data/lib/datadog/opentelemetry/sdk/trace/span.rb +23 -0
  107. data/lib/datadog/profiling/collectors/code_provenance.rb +7 -7
  108. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +28 -26
  109. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +11 -13
  110. data/lib/datadog/profiling/collectors/info.rb +15 -6
  111. data/lib/datadog/profiling/collectors/thread_context.rb +30 -2
  112. data/lib/datadog/profiling/component.rb +89 -95
  113. data/lib/datadog/profiling/exporter.rb +3 -3
  114. data/lib/datadog/profiling/ext/dir_monkey_patches.rb +3 -3
  115. data/lib/datadog/profiling/ext.rb +21 -21
  116. data/lib/datadog/profiling/flush.rb +1 -1
  117. data/lib/datadog/profiling/http_transport.rb +14 -7
  118. data/lib/datadog/profiling/load_native_extension.rb +5 -5
  119. data/lib/datadog/profiling/preload.rb +1 -1
  120. data/lib/datadog/profiling/profiler.rb +5 -8
  121. data/lib/datadog/profiling/scheduler.rb +33 -25
  122. data/lib/datadog/profiling/stack_recorder.rb +3 -0
  123. data/lib/datadog/profiling/tag_builder.rb +2 -2
  124. data/lib/datadog/profiling/tasks/exec.rb +5 -5
  125. data/lib/datadog/profiling/tasks/setup.rb +16 -35
  126. data/lib/datadog/profiling.rb +4 -5
  127. data/lib/datadog/single_step_instrument.rb +12 -0
  128. data/lib/datadog/tracing/contrib/action_cable/instrumentation.rb +8 -12
  129. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +5 -0
  130. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +78 -0
  131. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/patcher.rb +33 -0
  132. data/lib/datadog/tracing/contrib/action_pack/patcher.rb +2 -0
  133. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +4 -0
  134. data/lib/datadog/tracing/contrib/active_record/events/instantiation.rb +3 -1
  135. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +4 -1
  136. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +5 -1
  137. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +5 -0
  138. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  139. data/lib/datadog/tracing/contrib/ext.rb +14 -0
  140. data/lib/datadog/tracing/contrib/faraday/middleware.rb +9 -0
  141. data/lib/datadog/tracing/contrib/grape/endpoint.rb +19 -0
  142. data/lib/datadog/tracing/contrib/graphql/patcher.rb +9 -12
  143. data/lib/datadog/tracing/contrib/graphql/trace_patcher.rb +3 -3
  144. data/lib/datadog/tracing/contrib/graphql/tracing_patcher.rb +3 -3
  145. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +14 -10
  146. data/lib/datadog/tracing/contrib/graphql/unified_trace_patcher.rb +10 -4
  147. data/lib/datadog/tracing/contrib/http/instrumentation.rb +18 -15
  148. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +6 -5
  149. data/lib/datadog/tracing/contrib/httpclient/patcher.rb +1 -14
  150. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +5 -0
  151. data/lib/datadog/tracing/contrib/httprb/patcher.rb +1 -14
  152. data/lib/datadog/tracing/contrib/lograge/patcher.rb +15 -0
  153. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +2 -0
  154. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +5 -0
  155. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +17 -13
  156. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +13 -6
  157. data/lib/datadog/tracing/contrib/patcher.rb +2 -1
  158. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +5 -0
  159. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +4 -1
  160. data/lib/datadog/tracing/contrib/presto/patcher.rb +1 -13
  161. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +28 -0
  162. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  163. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +22 -10
  164. data/lib/datadog/tracing/contrib/rack/middlewares.rb +27 -0
  165. data/lib/datadog/tracing/contrib/redis/tags.rb +4 -0
  166. data/lib/datadog/tracing/contrib/sinatra/tracer.rb +4 -0
  167. data/lib/datadog/tracing/contrib/stripe/request.rb +3 -2
  168. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +5 -0
  169. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +4 -1
  170. data/lib/datadog/tracing/diagnostics/environment_logger.rb +14 -16
  171. data/lib/datadog/tracing/distributed/propagation.rb +7 -0
  172. data/lib/datadog/tracing/metadata/errors.rb +9 -1
  173. data/lib/datadog/tracing/metadata/ext.rb +6 -0
  174. data/lib/datadog/tracing/pipeline/span_filter.rb +2 -2
  175. data/lib/datadog/tracing/remote.rb +5 -2
  176. data/lib/datadog/tracing/sampling/matcher.rb +6 -1
  177. data/lib/datadog/tracing/sampling/rate_sampler.rb +1 -1
  178. data/lib/datadog/tracing/sampling/rule.rb +2 -0
  179. data/lib/datadog/tracing/sampling/rule_sampler.rb +9 -5
  180. data/lib/datadog/tracing/sampling/span/ext.rb +1 -1
  181. data/lib/datadog/tracing/sampling/span/rule.rb +2 -2
  182. data/lib/datadog/tracing/span.rb +9 -2
  183. data/lib/datadog/tracing/span_event.rb +41 -0
  184. data/lib/datadog/tracing/span_operation.rb +6 -2
  185. data/lib/datadog/tracing/trace_operation.rb +26 -2
  186. data/lib/datadog/tracing/tracer.rb +14 -12
  187. data/lib/datadog/tracing/transport/http/client.rb +1 -0
  188. data/lib/datadog/tracing/transport/io/client.rb +1 -0
  189. data/lib/datadog/tracing/transport/serializable_trace.rb +3 -0
  190. data/lib/datadog/tracing/workers/trace_writer.rb +1 -1
  191. data/lib/datadog/tracing/workers.rb +1 -1
  192. data/lib/datadog/version.rb +1 -1
  193. metadata +46 -11
  194. data/lib/datadog/profiling/crashtracker.rb +0 -91
  195. data/lib/datadog/profiling/ext/forking.rb +0 -98
  196. data/lib/datadog/tracing/sampling/rate_limiter.rb +0 -185
@@ -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++;
@@ -91,10 +92,15 @@ double discrete_dynamic_sampler_probability(discrete_dynamic_sampler *sampler) {
91
92
  return sampler->sampling_probability * 100.;
92
93
  }
93
94
 
94
- size_t discrete_dynamic_sampler_events_since_last_sample(discrete_dynamic_sampler *sampler) {
95
+ unsigned long discrete_dynamic_sampler_events_since_last_sample(discrete_dynamic_sampler *sampler) {
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
@@ -245,7 +259,9 @@ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
245
259
  // are so big they don't fit into the sampling_interval. In both cases lets just disable sampling until next readjustment
246
260
  // by setting interval to 0.
247
261
  double sampling_interval = sampler->sampling_probability == 0 ? 0 : ceil(1.0 / sampler->sampling_probability);
248
- sampler->sampling_interval = sampling_interval > ULONG_MAX ? 0 : sampling_interval;
262
+ // NOTE: We use UINT32_MAX instead of ULONG_MAX here to avoid clang warnings; in practice, we shouldn't ever hit
263
+ // such high sampling intervals.
264
+ sampler->sampling_interval = sampling_interval > UINT32_MAX ? 0 : sampling_interval;
249
265
 
250
266
  #ifdef DD_DEBUG
251
267
  double allocs_in_60s = sampler->events_per_ns * 1e9 * 60;
@@ -255,9 +271,7 @@ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
255
271
  double num_this_windows_in_60s = 60 * 1e9 / this_window_time_ns;
256
272
  double real_total_sampling_time_in_60s = sampler->sampling_time_since_last_readjustment_ns * num_this_windows_in_60s / 1e9;
257
273
 
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);
274
+ fprintf(stderr, "[dds.%s] readjusting...\n", sampler->debug_name);
261
275
  fprintf(stderr, "events_since_last_readjustment=%ld\n", sampler->events_since_last_readjustment);
262
276
  fprintf(stderr, "samples_since_last_readjustment=%ld\n", sampler->samples_since_last_readjustment);
263
277
  fprintf(stderr, "this_window_time=%ld\n", this_window_time_ns);
@@ -286,7 +300,7 @@ static void maybe_readjust(discrete_dynamic_sampler *sampler, long now) {
286
300
  sampler->events_since_last_readjustment = 0;
287
301
  sampler->samples_since_last_readjustment = 0;
288
302
  sampler->sampling_time_since_last_readjustment_ns = 0;
289
- sampler->last_readjust_time_ns = now;
303
+ sampler->last_readjust_time_ns = now_ns;
290
304
  sampler->has_completed_full_adjustment_window = true;
291
305
  }
292
306
 
@@ -359,6 +373,10 @@ static VALUE _native_new(VALUE klass) {
359
373
  }
360
374
  discrete_dynamic_sampler_init(&state->sampler, "test sampler", now_ns);
361
375
 
376
+ // Note: As of this writing, no new Ruby objects get created and stored in the state. If that ever changes, remember
377
+ // to keep them on the stack and mark them with RB_GC_GUARD -- otherwise it's possible for a GC to run and
378
+ // since the instance representing the state does not yet exist, such objects will not get marked.
379
+
362
380
  return TypedData_Wrap_Struct(klass, &sampler_typed_data, state);
363
381
  }
364
382
 
@@ -402,7 +420,14 @@ VALUE _native_should_sample(VALUE self, VALUE now_ns) {
402
420
  sampler_state *state;
403
421
  TypedData_Get_Struct(self, sampler_state, &sampler_typed_data, state);
404
422
 
405
- return discrete_dynamic_sampler_should_sample(&state->sampler, NUM2LONG(now_ns)) ? Qtrue : Qfalse;
423
+ if (discrete_dynamic_sampler_should_sample(&state->sampler)) {
424
+ discrete_dynamic_sampler_before_sample(&state->sampler, NUM2LONG(now_ns));
425
+ return Qtrue;
426
+ } else {
427
+ bool needs_readjust = discrete_dynamic_sampler_skipped_sample(&state->sampler, to_coarse_instant(NUM2LONG(now_ns)));
428
+ if (needs_readjust) discrete_dynamic_sampler_readjust(&state->sampler, NUM2LONG(now_ns));
429
+ return Qfalse;
430
+ }
406
431
  }
407
432
 
408
433
  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,56 +15,72 @@ 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
- static VALUE _native_sample(
26
- VALUE self,
27
- VALUE thread,
28
- VALUE recorder_instance,
29
- VALUE metric_values_hash,
30
- VALUE labels_array,
31
- VALUE numeric_labels_array,
32
- VALUE max_frames,
33
- VALUE in_gc
34
- );
23
+ static VALUE _native_sample(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
24
+ static VALUE native_sample_do(VALUE args);
25
+ static VALUE native_sample_ensure(VALUE args);
35
26
  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);
27
+ static void record_placeholder_stack_in_native_code(VALUE recorder_instance, sample_values values, sample_labels labels);
37
28
  static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_CharSlice *filename_slice);
38
29
 
30
+ // These two functions are exposed as symbols by the VM but are not in any header.
31
+ // Their signatures actually take a `const rb_iseq_t *iseq` but it gets casted back and forth between VALUE.
32
+ extern VALUE rb_iseq_path(const VALUE);
33
+ extern VALUE rb_iseq_base_label(const VALUE);
34
+
39
35
  void collectors_stack_init(VALUE profiling_module) {
40
36
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
41
37
  VALUE collectors_stack_class = rb_define_class_under(collectors_module, "Stack", rb_cObject);
42
38
  // Hosts methods used for testing the native code using RSpec
43
39
  VALUE testing_module = rb_define_module_under(collectors_stack_class, "Testing");
44
40
 
45
- rb_define_singleton_method(testing_module, "_native_sample", _native_sample, 7);
41
+ rb_define_singleton_method(testing_module, "_native_sample", _native_sample, -1);
46
42
 
47
43
  missing_string = rb_str_new2("");
48
44
  rb_global_variable(&missing_string);
49
45
  }
50
46
 
47
+ struct native_sample_args {
48
+ VALUE in_gc;
49
+ VALUE recorder_instance;
50
+ sample_values values;
51
+ sample_labels labels;
52
+ VALUE thread;
53
+ ddog_prof_Location *locations;
54
+ sampling_buffer *buffer;
55
+ };
56
+
51
57
  // This method exists only to enable testing Datadog::Profiling::Collectors::Stack behavior using RSpec.
52
58
  // It SHOULD NOT be used for other purposes.
53
- static VALUE _native_sample(
54
- DDTRACE_UNUSED VALUE _self,
55
- VALUE thread,
56
- VALUE recorder_instance,
57
- VALUE metric_values_hash,
58
- VALUE labels_array,
59
- VALUE numeric_labels_array,
60
- VALUE max_frames,
61
- VALUE in_gc
62
- ) {
59
+ static VALUE _native_sample(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self) {
60
+ // Positional args
61
+ VALUE thread;
62
+ VALUE recorder_instance;
63
+ VALUE metric_values_hash;
64
+ VALUE labels_array;
65
+ VALUE numeric_labels_array;
66
+ VALUE options;
67
+
68
+ rb_scan_args(argc, argv, "5:", &thread, &recorder_instance, &metric_values_hash, &labels_array, &numeric_labels_array, &options);
69
+
70
+ if (options == Qnil) options = rb_hash_new();
71
+
72
+ // Optional keyword args
73
+ VALUE max_frames = rb_hash_lookup2(options, ID2SYM(rb_intern("max_frames")), INT2NUM(400));
74
+ VALUE in_gc = rb_hash_lookup2(options, ID2SYM(rb_intern("in_gc")), Qfalse);
75
+ VALUE is_gvl_waiting_state = rb_hash_lookup2(options, ID2SYM(rb_intern("is_gvl_waiting_state")), Qfalse);
76
+
63
77
  ENFORCE_TYPE(metric_values_hash, T_HASH);
64
78
  ENFORCE_TYPE(labels_array, T_ARRAY);
65
79
  ENFORCE_TYPE(numeric_labels_array, T_ARRAY);
66
80
 
67
81
  VALUE zero = INT2NUM(0);
82
+ VALUE heap_sample = rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("heap_sample"), Qfalse);
83
+ ENFORCE_BOOLEAN(heap_sample);
68
84
  sample_values values = {
69
85
  .cpu_time_ns = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("cpu-time"), zero)),
70
86
  .cpu_or_wall_samples = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("cpu-samples"), zero)),
@@ -72,6 +88,7 @@ static VALUE _native_sample(
72
88
  .alloc_samples = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("alloc-samples"), zero)),
73
89
  .alloc_samples_unscaled = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("alloc-samples-unscaled"), zero)),
74
90
  .timeline_wall_time_ns = NUM2UINT(rb_hash_lookup2(metric_values_hash, rb_str_new_cstr("timeline"), zero)),
91
+ .heap_sample = heap_sample == Qtrue,
75
92
  };
76
93
 
77
94
  long labels_count = RARRAY_LEN(labels_array) + RARRAY_LEN(numeric_labels_array);
@@ -99,32 +116,54 @@ static VALUE _native_sample(
99
116
  };
100
117
  }
101
118
 
102
- int max_frames_requested = NUM2INT(max_frames);
103
- if (max_frames_requested < 0) rb_raise(rb_eArgError, "Invalid max_frames: value must not be negative");
119
+ int max_frames_requested = sampling_buffer_check_max_frames(NUM2INT(max_frames));
104
120
 
105
- sampling_buffer *buffer = sampling_buffer_new(max_frames_requested);
121
+ ddog_prof_Location *locations = ruby_xcalloc(max_frames_requested, sizeof(ddog_prof_Location));
122
+ sampling_buffer *buffer = sampling_buffer_new(max_frames_requested, locations);
106
123
 
107
124
  ddog_prof_Slice_Label slice_labels = {.ptr = labels, .len = labels_count};
108
125
 
109
- if (in_gc == Qtrue) {
126
+ struct native_sample_args args_struct = {
127
+ .in_gc = in_gc,
128
+ .recorder_instance = recorder_instance,
129
+ .values = values,
130
+ .labels = (sample_labels) {.labels = slice_labels, .state_label = state_label, .is_gvl_waiting_state = is_gvl_waiting_state == Qtrue},
131
+ .thread = thread,
132
+ .locations = locations,
133
+ .buffer = buffer,
134
+ };
135
+
136
+ return rb_ensure(native_sample_do, (VALUE) &args_struct, native_sample_ensure, (VALUE) &args_struct);
137
+ }
138
+
139
+ static VALUE native_sample_do(VALUE args) {
140
+ struct native_sample_args *args_struct = (struct native_sample_args *) args;
141
+
142
+ if (args_struct->in_gc == Qtrue) {
110
143
  record_placeholder_stack(
111
- buffer,
112
- recorder_instance,
113
- values,
114
- (sample_labels) {.labels = slice_labels, .state_label = state_label},
144
+ args_struct->recorder_instance,
145
+ args_struct->values,
146
+ args_struct->labels,
115
147
  DDOG_CHARSLICE_C("Garbage Collection")
116
148
  );
117
149
  } else {
118
150
  sample_thread(
119
- thread,
120
- buffer,
121
- recorder_instance,
122
- values,
123
- (sample_labels) {.labels = slice_labels, .state_label = state_label}
151
+ args_struct->thread,
152
+ args_struct->buffer,
153
+ args_struct->recorder_instance,
154
+ args_struct->values,
155
+ args_struct->labels
124
156
  );
125
157
  }
126
158
 
127
- sampling_buffer_free(buffer);
159
+ return Qtrue;
160
+ }
161
+
162
+ static VALUE native_sample_ensure(VALUE args) {
163
+ struct native_sample_args *args_struct = (struct native_sample_args *) args;
164
+
165
+ ruby_xfree(args_struct->locations);
166
+ sampling_buffer_free(args_struct->buffer);
128
167
 
129
168
  return Qtrue;
130
169
  }
@@ -151,47 +190,59 @@ void sample_thread(
151
190
  thread,
152
191
  0 /* stack starting depth */,
153
192
  buffer->max_frames,
154
- buffer->stack_buffer,
155
- buffer->lines_buffer,
156
- buffer->is_ruby_frame
193
+ buffer->stack_buffer
157
194
  );
158
195
 
159
196
  if (captured_frames == PLACEHOLDER_STACK_IN_NATIVE_CODE) {
160
- record_placeholder_stack_in_native_code(buffer, recorder_instance, values, labels);
197
+ record_placeholder_stack_in_native_code(recorder_instance, values, labels);
161
198
  return;
162
199
  }
163
200
 
201
+ // if (captured_frames > 0) {
202
+ // int cache_hits = 0;
203
+ // for (int i = 0; i < captured_frames; i++) {
204
+ // if (buffer->stack_buffer[i].same_frame) cache_hits++;
205
+ // }
206
+ // fprintf(stderr, "Sampling cache hits: %f\n", ((double) cache_hits / captured_frames) * 100);
207
+ // }
208
+
164
209
  // Ruby does not give us path and line number for methods implemented using native code.
165
210
  // The convention in Kernel#caller_locations is to instead use the path and line number of the first Ruby frame
166
211
  // on the stack that is below (e.g. directly or indirectly has called) the native method.
167
212
  // 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;
213
+ // (This is why we also iterate the sampling buffers backwards below -- so that it's easier to keep the last_ruby_frame_filename)
214
+ VALUE last_ruby_frame_filename = Qnil;
170
215
  int last_ruby_line = 0;
171
216
 
172
217
  ddog_prof_Label *state_label = labels.state_label;
173
218
  bool cpu_or_wall_sample = values.cpu_or_wall_samples > 0;
174
219
  bool has_cpu_time = cpu_or_wall_sample && values.cpu_time_ns > 0;
175
- bool only_wall_time = cpu_or_wall_sample && values.cpu_time_ns == 0 && values.wall_time_ns > 0;
220
+ // Note: In theory, a cpu_or_wall_sample should always have some wall-time. In practice, the first sample for a thread
221
+ // will be zero, as well as if the system clock does something weird. Thus, at some point we had values.wall_time_ns > 0
222
+ // here, but >= 0 makes this easier to understand/debug.
223
+ bool only_wall_time = cpu_or_wall_sample && values.cpu_time_ns == 0 && values.wall_time_ns >= 0;
176
224
 
177
225
  if (cpu_or_wall_sample && state_label == NULL) rb_raise(rb_eRuntimeError, "BUG: Unexpected missing state_label");
178
226
 
179
- if (has_cpu_time) state_label->str = DDOG_CHARSLICE_C("had cpu");
227
+ if (has_cpu_time) {
228
+ state_label->str = DDOG_CHARSLICE_C("had cpu");
229
+ if (labels.is_gvl_waiting_state) rb_raise(rb_eRuntimeError, "BUG: Unexpected combination of cpu-time with is_gvl_waiting");
230
+ }
180
231
 
181
232
  for (int i = captured_frames - 1; i >= 0; i--) {
182
233
  VALUE name, filename;
183
234
  int line;
184
235
 
185
- if (buffer->is_ruby_frame[i]) {
186
- last_ruby_frame = buffer->stack_buffer[i];
187
- last_ruby_line = buffer->lines_buffer[i];
236
+ if (buffer->stack_buffer[i].is_ruby_frame) {
237
+ name = rb_iseq_base_label(buffer->stack_buffer[i].as.ruby_frame.iseq);
238
+ filename = rb_iseq_path(buffer->stack_buffer[i].as.ruby_frame.iseq);
239
+ line = buffer->stack_buffer[i].as.ruby_frame.line;
188
240
 
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];
241
+ last_ruby_frame_filename = filename;
242
+ last_ruby_line = line;
192
243
  } 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);
244
+ name = rb_id2str(buffer->stack_buffer[i].as.native_frame.method_id);
245
+ filename = last_ruby_frame_filename;
195
246
  line = last_ruby_line;
196
247
  }
197
248
 
@@ -206,12 +257,15 @@ void sample_thread(
206
257
  bool top_of_the_stack = i == 0;
207
258
 
208
259
  // When there's only wall-time in a sample, this means that the thread was not active in the sampled period.
209
- //
210
- // We try to categorize what it was doing based on what we observe at the top of the stack. This is a very rough
211
- // approximation, and in the future we hope to replace this with a more accurate approach (such as using the
212
- // GVL instrumentation API.)
213
260
  if (top_of_the_stack && only_wall_time) {
214
- if (!buffer->is_ruby_frame[i]) {
261
+ // Did the caller already provide the state?
262
+ if (labels.is_gvl_waiting_state) {
263
+ state_label->str = DDOG_CHARSLICE_C("waiting for gvl");
264
+
265
+ // Otherwise, we try to categorize what the thread was doing based on what we observe at the top of the stack. This is a very rough
266
+ // approximation, and in the future we hope to replace this with a more accurate approach (such as using the
267
+ // GVL instrumentation API.)
268
+ } else if (!buffer->stack_buffer[i].is_ruby_frame) {
215
269
  // We know that known versions of Ruby implement these using native code; thus if we find a method with the
216
270
  // same name that is not native code, we ignore it, as it's probably a user method that coincidentally
217
271
  // has the same name. Thus, even though "matching just by method name" is kinda weak,
@@ -246,10 +300,8 @@ void sample_thread(
246
300
  }
247
301
 
248
302
  buffer->locations[i] = (ddog_prof_Location) {
249
- .function = (ddog_prof_Function) {
250
- .name = name_slice,
251
- .filename = filename_slice,
252
- },
303
+ .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C("")},
304
+ .function = (ddog_prof_Function) {.name = name_slice, .filename = filename_slice},
253
305
  .line = line,
254
306
  };
255
307
  }
@@ -287,7 +339,9 @@ static void maybe_trim_template_random_ids(ddog_CharSlice *name_slice, ddog_Char
287
339
  // Check filename doesn't end with ".rb"; templates are usually along the lines of .html.erb/.html.haml/...
288
340
  if (filename_slice->len < 3 || memcmp(filename_slice->ptr + filename_slice->len - 3, ".rb", 3) == 0) return;
289
341
 
290
- int pos = name_slice->len - 1;
342
+ if (name_slice->len > 1024) return;
343
+
344
+ int pos = ((int) name_slice->len) - 1;
291
345
 
292
346
  // Let's match on something__number_number:
293
347
  // Find start of id suffix from the end...
@@ -325,6 +379,7 @@ static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer*
325
379
  ddog_CharSlice function_name = DDOG_CHARSLICE_C("");
326
380
  ddog_CharSlice function_filename = {.ptr = frames_omitted_message, .len = strlen(frames_omitted_message)};
327
381
  buffer->locations[buffer->max_frames - 1] = (ddog_prof_Location) {
382
+ .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C("")},
328
383
  .function = (ddog_prof_Function) {.name = function_name, .filename = function_filename},
329
384
  .line = 0,
330
385
  };
@@ -352,13 +407,11 @@ static void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer*
352
407
  // with one containing a placeholder frame, so that these threads are properly represented in the UX.
353
408
 
354
409
  static void record_placeholder_stack_in_native_code(
355
- sampling_buffer* buffer,
356
410
  VALUE recorder_instance,
357
411
  sample_values values,
358
412
  sample_labels labels
359
413
  ) {
360
414
  record_placeholder_stack(
361
- buffer,
362
415
  recorder_instance,
363
416
  values,
364
417
  labels,
@@ -367,36 +420,40 @@ static void record_placeholder_stack_in_native_code(
367
420
  }
368
421
 
369
422
  void record_placeholder_stack(
370
- sampling_buffer* buffer,
371
423
  VALUE recorder_instance,
372
424
  sample_values values,
373
425
  sample_labels labels,
374
426
  ddog_CharSlice placeholder_stack
375
427
  ) {
376
- ddog_prof_Function placeholder = {.name = DDOG_CHARSLICE_C(""), .filename = placeholder_stack};
377
- buffer->locations[0] = (ddog_prof_Location) {.function = placeholder, .line = 0};
428
+ ddog_prof_Location placeholder_location = {
429
+ .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C("")},
430
+ .function = {.name = DDOG_CHARSLICE_C(""), .filename = placeholder_stack},
431
+ .line = 0,
432
+ };
378
433
 
379
434
  record_sample(
380
435
  recorder_instance,
381
- (ddog_prof_Slice_Location) {.ptr = buffer->locations, .len = 1},
436
+ (ddog_prof_Slice_Location) {.ptr = &placeholder_location, .len = 1},
382
437
  values,
383
438
  labels
384
439
  );
385
440
  }
386
441
 
387
- sampling_buffer *sampling_buffer_new(unsigned int max_frames) {
442
+ uint16_t sampling_buffer_check_max_frames(int max_frames) {
388
443
  if (max_frames < 5) rb_raise(rb_eArgError, "Invalid max_frames: value must be >= 5");
389
444
  if (max_frames > MAX_FRAMES_LIMIT) rb_raise(rb_eArgError, "Invalid max_frames: value must be <= " MAX_FRAMES_LIMIT_AS_STRING);
445
+ return max_frames;
446
+ }
447
+
448
+ sampling_buffer *sampling_buffer_new(uint16_t max_frames, ddog_prof_Location *locations) {
449
+ sampling_buffer_check_max_frames(max_frames);
390
450
 
391
451
  // Note: never returns NULL; if out of memory, it calls the Ruby out-of-memory handlers
392
452
  sampling_buffer* buffer = ruby_xcalloc(1, sizeof(sampling_buffer));
393
453
 
394
454
  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));
455
+ buffer->locations = locations;
456
+ buffer->stack_buffer = ruby_xcalloc(max_frames, sizeof(frame_info));
400
457
 
401
458
  return buffer;
402
459
  }
@@ -404,10 +461,8 @@ sampling_buffer *sampling_buffer_new(unsigned int max_frames) {
404
461
  void sampling_buffer_free(sampling_buffer *buffer) {
405
462
  if (buffer == NULL) rb_raise(rb_eArgError, "sampling_buffer_free called with NULL buffer");
406
463
 
464
+ // buffer->locations are owned by whoever called sampling_buffer_new, not us
407
465
  ruby_xfree(buffer->stack_buffer);
408
- ruby_xfree(buffer->lines_buffer);
409
- ruby_xfree(buffer->is_ruby_frame);
410
- ruby_xfree(buffer->locations);
411
466
 
412
467
  ruby_xfree(buffer);
413
468
  }
@@ -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);