ddtrace 1.22.0 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55da7739191e23ba6bb188ade9979c9dbd0a965bddfd2faedd6bf2c41b3a6e97
4
- data.tar.gz: 98bfd95dc6dd156d3d57f2ac22deb081a8e6fb8d3221eadf6e682f45e358ef7c
3
+ metadata.gz: b5e8a3ccb932af75df7d30d6fe59edc8277e21ee34a9562f7c2c8bfba1ea7521
4
+ data.tar.gz: 1508bf7e56a24af598aad666665be6cd0a7b97c9cbf3bbcc95a3fda36b67c07a
5
5
  SHA512:
6
- metadata.gz: 526f2826818f960ca18703a03cc0bde6313203a4e2dd03a696b8bd08f5a12cd963c370a8a9278d7387d81cf4445b849693fdbb8f91989f27f20d4f5bf55ad1a2
7
- data.tar.gz: 9b8d42db31a997fd7f03e383e0e2671730577336f5bdb28bf36afd63e775639f584abb8d2005265835fc175c29ae275f5cdfea79ba9afe02eb16a3a826f47d70
6
+ metadata.gz: afccca8d070d1dfd472af2390da1b343dff3199c42a71e742f10e29e91ddb27a6fa709490adcd639e6914f792b8e8565f667ff5d916f9f0f6690d69b8d8b0b54
7
+ data.tar.gz: 581022bd8d146fe01e9dac70a309e58d503d48ed8420b845a6b91200796ce9a0b1c2bc29fb58382ddc546abd00785d2f6ee42863e1bc6df517052a566352604e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.23.0] - 2024-05-09
6
+
7
+ ### Added
8
+
9
+ * Profiling: Enable endpoint profiling for Sidekiq and similar background job processors ([#3619][])
10
+
11
+ ### Fixed
12
+
13
+ * Fix no such file or directory issue when using single step instrumentation ([#3623][])
14
+ * Fix error during telemetry debug logging attempt ([#3618][])
15
+
5
16
  ## [1.22.0] - 2024-04-16
6
17
 
7
18
  ### Added
@@ -2804,6 +2815,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
2804
2815
 
2805
2816
 
2806
2817
  [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v1.22.0...master
2818
+ [1.23.0]: https://github.com/DataDog/dd-trace-rb/compare/v1.22.0...v1.23.0
2807
2819
  [1.22.0]: https://github.com/DataDog/dd-trace-rb/compare/v1.21.1...v1.22.0
2808
2820
  [2.0.0.beta1]: https://github.com/DataDog/dd-trace-rb/compare/v1.21.1...v2.0.0.beta1
2809
2821
  [1.21.1]: https://github.com/DataDog/dd-trace-rb/compare/v1.21.0...v1.21.1
@@ -4111,6 +4123,9 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
4111
4123
  [#3582]: https://github.com/DataDog/dd-trace-rb/issues/3582
4112
4124
  [#3585]: https://github.com/DataDog/dd-trace-rb/issues/3585
4113
4125
  [#3587]: https://github.com/DataDog/dd-trace-rb/issues/3587
4126
+ [#3618]: https://github.com/DataDog/dd-trace-rb/issues/3618
4127
+ [#3619]: https://github.com/DataDog/dd-trace-rb/issues/3619
4128
+ [#3623]: https://github.com/DataDog/dd-trace-rb/issues/3623
4114
4129
  [@AdrianLC]: https://github.com/AdrianLC
4115
4130
  [@Azure7111]: https://github.com/Azure7111
4116
4131
  [@BabyGroot]: https://github.com/BabyGroot
@@ -929,18 +929,6 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
929
929
  struct cpu_and_wall_time_worker_state *state;
930
930
  TypedData_Get_Struct(instance, struct cpu_and_wall_time_worker_state, &cpu_and_wall_time_worker_typed_data, state);
931
931
 
932
- VALUE pretty_cpu_sampling_time_ns_min = state->stats.cpu_sampling_time_ns_min == UINT64_MAX ? Qnil : ULL2NUM(state->stats.cpu_sampling_time_ns_min);
933
- VALUE pretty_cpu_sampling_time_ns_max = state->stats.cpu_sampling_time_ns_max == 0 ? Qnil : ULL2NUM(state->stats.cpu_sampling_time_ns_max);
934
- VALUE pretty_cpu_sampling_time_ns_total = state->stats.cpu_sampling_time_ns_total == 0 ? Qnil : ULL2NUM(state->stats.cpu_sampling_time_ns_total);
935
- VALUE pretty_cpu_sampling_time_ns_avg =
936
- state->stats.cpu_sampled == 0 ? Qnil : DBL2NUM(((double) state->stats.cpu_sampling_time_ns_total) / state->stats.cpu_sampled);
937
-
938
- VALUE pretty_allocation_sampling_time_ns_min = state->stats.allocation_sampling_time_ns_min == UINT64_MAX ? Qnil : ULL2NUM(state->stats.allocation_sampling_time_ns_min);
939
- VALUE pretty_allocation_sampling_time_ns_max = state->stats.allocation_sampling_time_ns_max == 0 ? Qnil : ULL2NUM(state->stats.allocation_sampling_time_ns_max);
940
- VALUE pretty_allocation_sampling_time_ns_total = state->stats.allocation_sampling_time_ns_total == 0 ? Qnil : ULL2NUM(state->stats.allocation_sampling_time_ns_total);
941
- VALUE pretty_allocation_sampling_time_ns_avg =
942
- state->stats.allocation_sampled == 0 ? Qnil : DBL2NUM(((double) state->stats.allocation_sampling_time_ns_total) / state->stats.allocation_sampled);
943
-
944
932
  unsigned long total_cpu_samples_attempted = state->stats.cpu_sampled + state->stats.cpu_skipped;
945
933
  VALUE effective_cpu_sample_rate =
946
934
  total_cpu_samples_attempted == 0 ? Qnil : DBL2NUM(((double) state->stats.cpu_sampled) / total_cpu_samples_attempted);
@@ -968,19 +956,19 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance) {
968
956
  ID2SYM(rb_intern("cpu_sampled")), /* => */ UINT2NUM(state->stats.cpu_sampled),
969
957
  ID2SYM(rb_intern("cpu_skipped")), /* => */ UINT2NUM(state->stats.cpu_skipped),
970
958
  ID2SYM(rb_intern("cpu_effective_sample_rate")), /* => */ effective_cpu_sample_rate,
971
- ID2SYM(rb_intern("cpu_sampling_time_ns_min")), /* => */ pretty_cpu_sampling_time_ns_min,
972
- ID2SYM(rb_intern("cpu_sampling_time_ns_max")), /* => */ pretty_cpu_sampling_time_ns_max,
973
- ID2SYM(rb_intern("cpu_sampling_time_ns_total")), /* => */ pretty_cpu_sampling_time_ns_total,
974
- ID2SYM(rb_intern("cpu_sampling_time_ns_avg")), /* => */ pretty_cpu_sampling_time_ns_avg,
959
+ ID2SYM(rb_intern("cpu_sampling_time_ns_min")), /* => */ RUBY_NUM_OR_NIL(state->stats.cpu_sampling_time_ns_min, != UINT64_MAX, ULL2NUM),
960
+ ID2SYM(rb_intern("cpu_sampling_time_ns_max")), /* => */ RUBY_NUM_OR_NIL(state->stats.cpu_sampling_time_ns_max, > 0, ULL2NUM),
961
+ ID2SYM(rb_intern("cpu_sampling_time_ns_total")), /* => */ RUBY_NUM_OR_NIL(state->stats.cpu_sampling_time_ns_total, > 0, ULL2NUM),
962
+ ID2SYM(rb_intern("cpu_sampling_time_ns_avg")), /* => */ RUBY_AVG_OR_NIL(state->stats.cpu_sampling_time_ns_total, state->stats.cpu_sampled),
975
963
 
976
964
  // Allocation stats
977
965
  ID2SYM(rb_intern("allocation_sampled")), /* => */ state->allocation_profiling_enabled ? ULONG2NUM(state->stats.allocation_sampled) : Qnil,
978
966
  ID2SYM(rb_intern("allocation_skipped")), /* => */ state->allocation_profiling_enabled ? ULONG2NUM(state->stats.allocation_skipped) : Qnil,
979
967
  ID2SYM(rb_intern("allocation_effective_sample_rate")), /* => */ effective_allocation_sample_rate,
980
- ID2SYM(rb_intern("allocation_sampling_time_ns_min")), /* => */ pretty_allocation_sampling_time_ns_min,
981
- ID2SYM(rb_intern("allocation_sampling_time_ns_max")), /* => */ pretty_allocation_sampling_time_ns_max,
982
- ID2SYM(rb_intern("allocation_sampling_time_ns_total")), /* => */ pretty_allocation_sampling_time_ns_total,
983
- ID2SYM(rb_intern("allocation_sampling_time_ns_avg")), /* => */ pretty_allocation_sampling_time_ns_avg,
968
+ ID2SYM(rb_intern("allocation_sampling_time_ns_min")), /* => */ RUBY_NUM_OR_NIL(state->stats.allocation_sampling_time_ns_min, != UINT64_MAX, ULL2NUM),
969
+ ID2SYM(rb_intern("allocation_sampling_time_ns_max")), /* => */ RUBY_NUM_OR_NIL(state->stats.allocation_sampling_time_ns_max, > 0, ULL2NUM),
970
+ ID2SYM(rb_intern("allocation_sampling_time_ns_total")), /* => */ RUBY_NUM_OR_NIL(state->stats.allocation_sampling_time_ns_total, > 0, ULL2NUM),
971
+ ID2SYM(rb_intern("allocation_sampling_time_ns_avg")), /* => */ RUBY_AVG_OR_NIL(state->stats.allocation_sampling_time_ns_total, state->stats.allocation_sampled),
984
972
  ID2SYM(rb_intern("allocation_sampler_snapshot")), /* => */ allocation_sampler_snapshot,
985
973
  ID2SYM(rb_intern("allocations_during_sample")), /* => */ state->allocation_profiling_enabled ? UINT2NUM(state->stats.allocations_during_sample) : Qnil,
986
974
  };
@@ -217,7 +217,7 @@ static long thread_id_for(VALUE thread);
217
217
  static VALUE _native_stats(VALUE self, VALUE collector_instance);
218
218
  static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
219
219
  static void trace_identifiers_for(struct thread_context_collector_state *state, VALUE thread, struct trace_identifiers *trace_identifiers_result);
220
- static bool should_collect_resource(VALUE root_span_type);
220
+ static bool should_collect_resource(VALUE root_span);
221
221
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
222
222
  static VALUE thread_list(struct thread_context_collector_state *state);
223
223
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object);
@@ -1146,10 +1146,7 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
1146
1146
 
1147
1147
  trace_identifiers_result->valid = true;
1148
1148
 
1149
- if (!state->endpoint_collection_enabled) return;
1150
-
1151
- VALUE root_span_type = rb_ivar_get(root_span, at_type_id /* @type */);
1152
- if (root_span_type == Qnil || !should_collect_resource(root_span_type)) return;
1149
+ if (!state->endpoint_collection_enabled || !should_collect_resource(root_span)) return;
1153
1150
 
1154
1151
  VALUE trace_resource = rb_ivar_get(active_trace, at_resource_id /* @resource */);
1155
1152
  if (RB_TYPE_P(trace_resource, T_STRING)) {
@@ -1160,21 +1157,32 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
1160
1157
  }
1161
1158
  }
1162
1159
 
1163
- // We only collect the resource for spans of types:
1160
+ // We opt-in to collecting the resource for spans of types:
1164
1161
  // * 'web', for web requests
1165
- // * proxy', used by the rack integration with request_queuing: true (e.g. also represents a web request)
1162
+ // * 'proxy', used by the rack integration with request_queuing: true (e.g. also represents a web request)
1163
+ // * 'worker', used for sidekiq and similar background job processors
1166
1164
  //
1167
- // NOTE: Currently we're only interested in HTTP service endpoints. Over time, this list may be expanded.
1165
+ // Over time, this list may be expanded.
1168
1166
  // Resources MUST NOT include personal identifiable information (PII); this should not be the case with
1169
1167
  // ddtrace integrations, but worth mentioning just in case :)
1170
- static bool should_collect_resource(VALUE root_span_type) {
1168
+ static bool should_collect_resource(VALUE root_span) {
1169
+ VALUE root_span_type = rb_ivar_get(root_span, at_type_id /* @type */);
1170
+ if (root_span_type == Qnil) return false;
1171
1171
  ENFORCE_TYPE(root_span_type, T_STRING);
1172
1172
 
1173
1173
  int root_span_type_length = RSTRING_LEN(root_span_type);
1174
1174
  const char *root_span_type_value = StringValuePtr(root_span_type);
1175
1175
 
1176
- return (root_span_type_length == strlen("web") && (memcmp("web", root_span_type_value, strlen("web")) == 0)) ||
1176
+ bool is_web_request =
1177
+ (root_span_type_length == strlen("web") && (memcmp("web", root_span_type_value, strlen("web")) == 0)) ||
1177
1178
  (root_span_type_length == strlen("proxy") && (memcmp("proxy", root_span_type_value, strlen("proxy")) == 0));
1179
+
1180
+ if (is_web_request) return true;
1181
+
1182
+ bool is_worker_request =
1183
+ (root_span_type_length == strlen("worker") && (memcmp("worker", root_span_type_value, strlen("worker")) == 0));
1184
+
1185
+ return is_worker_request;
1178
1186
  }
1179
1187
 
1180
1188
  // After the Ruby VM forks, this method gets called in the child process to clean up any leftover state from the parent.
@@ -158,6 +158,13 @@ struct heap_recorder {
158
158
 
159
159
  // Sampling state
160
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;
161
168
  };
162
169
  static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location);
163
170
  static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*);
@@ -372,6 +379,9 @@ void heap_recorder_prepare_iteration(heap_recorder *heap_recorder) {
372
379
  rb_raise(rb_eRuntimeError, "New heap recorder iteration prepared without the previous one having been finished.");
373
380
  }
374
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
+
375
385
  st_foreach(heap_recorder->object_records, st_object_record_update, (st_data_t) heap_recorder);
376
386
 
377
387
  heap_recorder->object_records_snapshot = st_copy(heap_recorder->object_records);
@@ -427,6 +437,22 @@ bool heap_recorder_for_each_live_object(
427
437
  return true;
428
438
  }
429
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
+
430
456
  void heap_recorder_testonly_assert_hash_matches(ddog_prof_Slice_Location locations) {
431
457
  heap_stack *stack = heap_stack_new(locations);
432
458
  heap_record_key stack_based_key = (heap_record_key) {
@@ -497,12 +523,14 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
497
523
  // no point checking for liveness or updating its size, so exit early.
498
524
  // NOTE: This means that there should be an equivalent check during actual
499
525
  // iteration otherwise we'd iterate/expose stale object data.
526
+ recorder->stats_last_update.objects_skipped++;
500
527
  return ST_CONTINUE;
501
528
  }
502
529
 
503
530
  if (!ruby_ref_from_id(LONG2NUM(obj_id), &ref)) {
504
531
  // Id no longer associated with a valid ref. Need to delete this object record!
505
532
  on_committed_object_record_cleanup(recorder, record);
533
+ recorder->stats_last_update.objects_dead++;
506
534
  return ST_DELETE;
507
535
  }
508
536
 
@@ -537,6 +565,7 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
537
565
  RB_FL_SET(ref, RUBY_FL_SEEN_OBJ_ID);
538
566
 
539
567
  on_committed_object_record_cleanup(recorder, record);
568
+ recorder->stats_last_update.objects_dead++;
540
569
  return ST_DELETE;
541
570
  }
542
571
 
@@ -550,6 +579,11 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
550
579
  record->object_data.is_frozen = RB_OBJ_FROZEN(ref);
551
580
  }
552
581
 
582
+ recorder->stats_last_update.objects_alive++;
583
+ if (record->object_data.is_frozen) {
584
+ recorder->stats_last_update.objects_frozen++;
585
+ }
586
+
553
587
  return ST_CONTINUE;
554
588
  }
555
589
 
@@ -767,9 +801,10 @@ void object_record_free(object_record *record) {
767
801
 
768
802
  VALUE object_record_inspect(object_record *record) {
769
803
  heap_frame top_frame = record->heap_record->stack->frames[0];
770
- VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%s:%d alloc_gen=%zu gen_age=%zu ",
771
- record->obj_id, record->object_data.weight, record->object_data.size, top_frame.filename,
772
- (int) top_frame.line, record->object_data.alloc_gen, record->object_data.gen_age);
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);
773
808
 
774
809
  const char *class = record->object_data.class;
775
810
  if (class != NULL) {
@@ -150,6 +150,11 @@ bool heap_recorder_for_each_live_object(
150
150
  bool (*for_each_callback)(heap_recorder_iteration_data data, void* extra_arg),
151
151
  void *for_each_callback_extra_arg);
152
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
+
153
158
  // v--- TEST-ONLY APIs ---v
154
159
 
155
160
  // Assert internal hashing logic is valid for the provided locations and its
@@ -82,6 +82,9 @@ NORETURN(
82
82
  #define ENFORCE_SUCCESS_HELPER(expression, have_gvl) \
83
83
  { int result_syserr_errno = expression; if (RB_UNLIKELY(result_syserr_errno)) raise_syserr(result_syserr_errno, have_gvl, ADD_QUOTES(expression), __FILE__, __LINE__, __func__); }
84
84
 
85
+ #define RUBY_NUM_OR_NIL(val, condition, conv) ((val condition) ? conv(val) : Qnil)
86
+ #define RUBY_AVG_OR_NIL(total, count) ((count == 0) ? Qnil : DBL2NUM(((double) total) / count))
87
+
85
88
  // Called by ENFORCE_SUCCESS_HELPER; should not be used directly
86
89
  NORETURN(void raise_syserr(
87
90
  int syserr_errno,
@@ -169,28 +169,51 @@ static const uint8_t all_value_types_positions[] =
169
169
 
170
170
  #define ALL_VALUE_TYPES_COUNT (sizeof(all_value_types) / sizeof(ddog_prof_ValueType))
171
171
 
172
+ // Struct for storing stats related to a profile in a particular slot.
173
+ // These stats will share the same lifetime as the data in that profile slot.
174
+ typedef struct slot_stats {
175
+ // How many individual samples were recorded into this slot (un-weighted)
176
+ uint64_t recorded_samples;
177
+ } stats_slot;
178
+
179
+ typedef struct profile_slot {
180
+ ddog_prof_Profile profile;
181
+ stats_slot stats;
182
+ } profile_slot;
183
+
172
184
  // Contains native state for each instance
173
185
  struct stack_recorder_state {
174
186
  // Heap recorder instance
175
187
  heap_recorder *heap_recorder;
176
188
 
177
- pthread_mutex_t slot_one_mutex;
178
- ddog_prof_Profile slot_one_profile;
179
-
180
- pthread_mutex_t slot_two_mutex;
181
- ddog_prof_Profile slot_two_profile;
189
+ pthread_mutex_t mutex_slot_one;
190
+ profile_slot profile_slot_one;
191
+ pthread_mutex_t mutex_slot_two;
192
+ profile_slot profile_slot_two;
182
193
 
183
194
  short active_slot; // MUST NEVER BE ACCESSED FROM record_sample; this is NOT for the sampler thread to use.
184
195
 
185
196
  uint8_t position_for[ALL_VALUE_TYPES_COUNT];
186
197
  uint8_t enabled_values_count;
198
+
199
+ // Struct for storing stats related to behaviour of a stack recorder instance during its entire lifetime.
200
+ struct lifetime_stats {
201
+ // How many profiles have we serialized successfully so far
202
+ uint64_t serialization_successes;
203
+ // How many profiles have we serialized unsuccessfully so far
204
+ uint64_t serialization_failures;
205
+ // Stats on profile serialization time
206
+ long serialization_time_ns_min;
207
+ long serialization_time_ns_max;
208
+ uint64_t serialization_time_ns_total;
209
+ } stats_lifetime;
187
210
  };
188
211
 
189
- // Used to return a pair of values from sampler_lock_active_profile()
190
- struct active_slot_pair {
212
+ // Used to group mutex and the corresponding profile slot for easy unlocking after work is done.
213
+ typedef struct locked_profile_slot {
191
214
  pthread_mutex_t *mutex;
192
- ddog_prof_Profile *profile;
193
- };
215
+ profile_slot *data;
216
+ } locked_profile_slot;
194
217
 
195
218
  struct call_serialize_without_gvl_arguments {
196
219
  // Set by caller
@@ -198,8 +221,10 @@ struct call_serialize_without_gvl_arguments {
198
221
  ddog_Timespec finish_timestamp;
199
222
 
200
223
  // Set by callee
201
- ddog_prof_Profile *profile;
224
+ profile_slot *slot;
202
225
  ddog_prof_Profile_SerializeResult result;
226
+ long heap_profile_build_time_ns;
227
+ long serialize_no_gvl_time_ns;
203
228
 
204
229
  // Set by both
205
230
  bool serialize_ran;
@@ -222,9 +247,9 @@ static VALUE _native_initialize(
222
247
  static VALUE _native_serialize(VALUE self, VALUE recorder_instance);
223
248
  static VALUE ruby_time_from(ddog_Timespec ddprof_time);
224
249
  static void *call_serialize_without_gvl(void *call_args);
225
- static struct active_slot_pair sampler_lock_active_profile(struct stack_recorder_state *state);
226
- static void sampler_unlock_active_profile(struct active_slot_pair active_slot);
227
- static ddog_prof_Profile *serializer_flip_active_and_inactive_slots(struct stack_recorder_state *state);
250
+ static locked_profile_slot sampler_lock_active_profile(struct stack_recorder_state *state);
251
+ static void sampler_unlock_active_profile(locked_profile_slot active_slot);
252
+ static profile_slot* serializer_flip_active_and_inactive_slots(struct stack_recorder_state *state);
228
253
  static VALUE _native_active_slot(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
229
254
  static VALUE _native_is_slot_one_mutex_locked(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
230
255
  static VALUE _native_is_slot_two_mutex_locked(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
@@ -233,7 +258,7 @@ static ddog_Timespec system_epoch_now_timespec(void);
233
258
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE recorder_instance);
234
259
  static void serializer_set_start_timestamp_for_next_profile(struct stack_recorder_state *state, ddog_Timespec start_time);
235
260
  static VALUE _native_record_endpoint(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE local_root_span_id, VALUE endpoint);
236
- static void reset_profile(ddog_prof_Profile *profile, ddog_Timespec *start_time /* Can be null */);
261
+ static void reset_profile_slot(profile_slot *slot, ddog_Timespec *start_time /* Can be null */);
237
262
  static VALUE _native_track_object(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE new_obj, VALUE weight, VALUE alloc_class);
238
263
  static VALUE _native_check_heap_hashes(DDTRACE_UNUSED VALUE _self, VALUE locations);
239
264
  static VALUE _native_start_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
@@ -241,6 +266,8 @@ static VALUE _native_end_fake_slow_heap_serialization(DDTRACE_UNUSED VALUE _self
241
266
  static VALUE _native_debug_heap_recorder(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance);
242
267
  static VALUE _native_gc_force_recycle(DDTRACE_UNUSED VALUE _self, VALUE obj);
243
268
  static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj);
269
+ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE instance);
270
+ static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns, long heap_iteration_prep_time_ns, long heap_profile_build_time_ns);
244
271
 
245
272
 
246
273
  void stack_recorder_init(VALUE profiling_module) {
@@ -261,6 +288,7 @@ void stack_recorder_init(VALUE profiling_module) {
261
288
  rb_define_singleton_method(stack_recorder_class, "_native_initialize", _native_initialize, 7);
262
289
  rb_define_singleton_method(stack_recorder_class, "_native_serialize", _native_serialize, 1);
263
290
  rb_define_singleton_method(stack_recorder_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
291
+ rb_define_singleton_method(stack_recorder_class, "_native_stats", _native_stats, 1);
264
292
  rb_define_singleton_method(testing_module, "_native_active_slot", _native_active_slot, 1);
265
293
  rb_define_singleton_method(testing_module, "_native_slot_one_mutex_locked?", _native_is_slot_one_mutex_locked, 1);
266
294
  rb_define_singleton_method(testing_module, "_native_slot_two_mutex_locked?", _native_is_slot_two_mutex_locked, 1);
@@ -305,6 +333,9 @@ static VALUE _native_new(VALUE klass) {
305
333
  initialize_slot_concurrency_control(state);
306
334
  for (uint8_t i = 0; i < ALL_VALUE_TYPES_COUNT; i++) { state->position_for[i] = all_value_types_positions[i]; }
307
335
  state->enabled_values_count = ALL_VALUE_TYPES_COUNT;
336
+ state->stats_lifetime = (struct lifetime_stats) {
337
+ .serialization_time_ns_min = INT64_MAX,
338
+ };
308
339
 
309
340
  // Note: At this point, slot_one_profile and slot_two_profile contain null pointers. Libdatadog validates pointers
310
341
  // before using them so it's ok for us to go ahead and create the StackRecorder object.
@@ -325,11 +356,11 @@ static VALUE _native_new(VALUE klass) {
325
356
  }
326
357
 
327
358
  static void initialize_slot_concurrency_control(struct stack_recorder_state *state) {
328
- state->slot_one_mutex = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
329
- state->slot_two_mutex = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
359
+ state->mutex_slot_one = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
360
+ state->mutex_slot_two = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
330
361
 
331
362
  // A newly-created StackRecorder starts with slot one being active for samples, so let's lock slot two
332
- ENFORCE_SUCCESS_GVL(pthread_mutex_lock(&state->slot_two_mutex));
363
+ ENFORCE_SUCCESS_GVL(pthread_mutex_lock(&state->mutex_slot_two));
333
364
 
334
365
  state->active_slot = 1;
335
366
  }
@@ -352,18 +383,22 @@ static void initialize_profiles(struct stack_recorder_state *state, ddog_prof_Sl
352
383
  rb_raise(rb_eRuntimeError, "Failed to initialize slot two profile: %"PRIsVALUE, get_error_details_and_drop(&slot_two_profile_result.err));
353
384
  }
354
385
 
355
- state->slot_one_profile = slot_one_profile_result.ok;
356
- state->slot_two_profile = slot_two_profile_result.ok;
386
+ state->profile_slot_one = (profile_slot) {
387
+ .profile = slot_one_profile_result.ok,
388
+ };
389
+ state->profile_slot_two = (profile_slot) {
390
+ .profile = slot_two_profile_result.ok,
391
+ };
357
392
  }
358
393
 
359
394
  static void stack_recorder_typed_data_free(void *state_ptr) {
360
395
  struct stack_recorder_state *state = (struct stack_recorder_state *) state_ptr;
361
396
 
362
- pthread_mutex_destroy(&state->slot_one_mutex);
363
- ddog_prof_Profile_drop(&state->slot_one_profile);
397
+ pthread_mutex_destroy(&state->mutex_slot_one);
398
+ ddog_prof_Profile_drop(&state->profile_slot_one.profile);
364
399
 
365
- pthread_mutex_destroy(&state->slot_two_mutex);
366
- ddog_prof_Profile_drop(&state->slot_two_profile);
400
+ pthread_mutex_destroy(&state->mutex_slot_two);
401
+ ddog_prof_Profile_drop(&state->profile_slot_two.profile);
367
402
 
368
403
  heap_recorder_free(state->heap_recorder);
369
404
 
@@ -462,8 +497,8 @@ static VALUE _native_initialize(
462
497
  state->position_for[TIMELINE_VALUE_ID] = next_disabled_pos++;
463
498
  }
464
499
 
465
- ddog_prof_Profile_drop(&state->slot_one_profile);
466
- ddog_prof_Profile_drop(&state->slot_two_profile);
500
+ ddog_prof_Profile_drop(&state->profile_slot_one.profile);
501
+ ddog_prof_Profile_drop(&state->profile_slot_two.profile);
467
502
 
468
503
  ddog_prof_Slice_ValueType sample_types = {.ptr = enabled_value_types, .len = state->enabled_values_count};
469
504
  initialize_profiles(state, sample_types);
@@ -479,9 +514,11 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
479
514
  // Need to do this while still holding on to the Global VM Lock; see comments on method for why
480
515
  serializer_set_start_timestamp_for_next_profile(state, finish_timestamp);
481
516
 
517
+ long heap_iteration_prep_start_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
482
518
  // Prepare the iteration on heap recorder we'll be doing outside the GVL. The preparation needs to
483
519
  // happen while holding on to the GVL.
484
520
  heap_recorder_prepare_iteration(state->heap_recorder);
521
+ long heap_iteration_prep_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - heap_iteration_prep_start_time_ns;
485
522
 
486
523
  // We'll release the Global VM Lock while we're calling serialize, so that the Ruby VM can continue to work while this
487
524
  // is pending
@@ -508,12 +545,27 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
508
545
  // Cleanup after heap recorder iteration. This needs to happen while holding on to the GVL.
509
546
  heap_recorder_finish_iteration(state->heap_recorder);
510
547
 
548
+ // NOTE: We are focusing on the serialization time outside of the GVL in this stat here. This doesn't
549
+ // really cover the full serialization process but it gives a more useful number since it bypasses
550
+ // the noise of acquiring GVLs and dealing with interruptions which is highly specific to runtime
551
+ // conditions and over which we really have no control about.
552
+ long serialization_time_ns = args.serialize_no_gvl_time_ns;
553
+ if (serialization_time_ns >= 0) {
554
+ // Only update stats if our serialization time is valid.
555
+ state->stats_lifetime.serialization_time_ns_max = long_max_of(state->stats_lifetime.serialization_time_ns_max, serialization_time_ns);
556
+ state->stats_lifetime.serialization_time_ns_min = long_min_of(state->stats_lifetime.serialization_time_ns_min, serialization_time_ns);
557
+ state->stats_lifetime.serialization_time_ns_total += serialization_time_ns;
558
+ }
559
+
511
560
  ddog_prof_Profile_SerializeResult serialized_profile = args.result;
512
561
 
513
562
  if (serialized_profile.tag == DDOG_PROF_PROFILE_SERIALIZE_RESULT_ERR) {
563
+ state->stats_lifetime.serialization_failures++;
514
564
  return rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&serialized_profile.err));
515
565
  }
516
566
 
567
+ state->stats_lifetime.serialization_successes++;
568
+
517
569
  VALUE encoded_pprof = ruby_string_from_vec_u8(serialized_profile.ok.buffer);
518
570
 
519
571
  ddog_Timespec ddprof_start = serialized_profile.ok.start;
@@ -523,8 +575,9 @@ static VALUE _native_serialize(DDTRACE_UNUSED VALUE _self, VALUE recorder_instan
523
575
 
524
576
  VALUE start = ruby_time_from(ddprof_start);
525
577
  VALUE finish = ruby_time_from(ddprof_finish);
578
+ VALUE profile_stats = build_profile_stats(args.slot, serialization_time_ns, heap_iteration_prep_time_ns, args.heap_profile_build_time_ns);
526
579
 
527
- return rb_ary_new_from_args(2, ok_symbol, rb_ary_new_from_args(3, start, finish, encoded_pprof));
580
+ return rb_ary_new_from_args(2, ok_symbol, rb_ary_new_from_args(4, start, finish, encoded_pprof, profile_stats));
528
581
  }
529
582
 
530
583
  static VALUE ruby_time_from(ddog_Timespec ddprof_time) {
@@ -537,7 +590,7 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
537
590
  struct stack_recorder_state *state;
538
591
  TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
539
592
 
540
- struct active_slot_pair active_slot = sampler_lock_active_profile(state);
593
+ locked_profile_slot active_slot = sampler_lock_active_profile(state);
541
594
 
542
595
  // Note: We initialize this array to have ALL_VALUE_TYPES_COUNT but only tell libdatadog to use the first
543
596
  // state->enabled_values_count values. This simplifies handling disabled value types -- we still put them on the
@@ -561,7 +614,7 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
561
614
  }
562
615
 
563
616
  ddog_prof_Profile_Result result = ddog_prof_Profile_add(
564
- active_slot.profile,
617
+ &active_slot.data->profile,
565
618
  (ddog_prof_Sample) {
566
619
  .locations = locations,
567
620
  .values = (ddog_Slice_I64) {.ptr = metric_values, .len = state->enabled_values_count},
@@ -570,6 +623,8 @@ void record_sample(VALUE recorder_instance, ddog_prof_Slice_Location locations,
570
623
  labels.end_timestamp_ns
571
624
  );
572
625
 
626
+ active_slot.data->stats.recorded_samples++;
627
+
573
628
  sampler_unlock_active_profile(active_slot);
574
629
 
575
630
  if (result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
@@ -590,9 +645,9 @@ void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_
590
645
  struct stack_recorder_state *state;
591
646
  TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
592
647
 
593
- struct active_slot_pair active_slot = sampler_lock_active_profile(state);
648
+ locked_profile_slot active_slot = sampler_lock_active_profile(state);
594
649
 
595
- ddog_prof_Profile_Result result = ddog_prof_Profile_set_endpoint(active_slot.profile, local_root_span_id, endpoint);
650
+ ddog_prof_Profile_Result result = ddog_prof_Profile_set_endpoint(&active_slot.data->profile, local_root_span_id, endpoint);
596
651
 
597
652
  sampler_unlock_active_profile(active_slot);
598
653
 
@@ -607,7 +662,7 @@ void record_endpoint(VALUE recorder_instance, uint64_t local_root_span_id, ddog_
607
662
  // during iteration of heap recorder live objects.
608
663
  typedef struct heap_recorder_iteration_context {
609
664
  struct stack_recorder_state *state;
610
- ddog_prof_Profile *profile;
665
+ profile_slot *slot;
611
666
 
612
667
  bool error;
613
668
  char error_msg[MAX_LEN_HEAP_ITERATION_ERROR_MSG];
@@ -643,7 +698,7 @@ static bool add_heap_sample_to_active_profile_without_gvl(heap_recorder_iteratio
643
698
  };
644
699
 
645
700
  ddog_prof_Profile_Result result = ddog_prof_Profile_add(
646
- context->profile,
701
+ &context->slot->profile,
647
702
  (ddog_prof_Sample) {
648
703
  .locations = iteration_data.locations,
649
704
  .values = (ddog_Slice_I64) {.ptr = metric_values, .len = context->state->enabled_values_count},
@@ -655,6 +710,8 @@ static bool add_heap_sample_to_active_profile_without_gvl(heap_recorder_iteratio
655
710
  0
656
711
  );
657
712
 
713
+ context->slot->stats.recorded_samples++;
714
+
658
715
  if (result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
659
716
  read_ddogerr_string_and_drop(&result.err, context->error_msg, MAX_LEN_HEAP_ITERATION_ERROR_MSG);
660
717
  context->error = true;
@@ -666,10 +723,10 @@ static bool add_heap_sample_to_active_profile_without_gvl(heap_recorder_iteratio
666
723
  return true;
667
724
  }
668
725
 
669
- static void build_heap_profile_without_gvl(struct stack_recorder_state *state, ddog_prof_Profile *profile) {
726
+ static void build_heap_profile_without_gvl(struct stack_recorder_state *state, profile_slot *slot) {
670
727
  heap_recorder_iteration_context iteration_context = {
671
728
  .state = state,
672
- .profile = profile,
729
+ .slot = slot,
673
730
  .error = false,
674
731
  .error_msg = {0},
675
732
  };
@@ -689,15 +746,21 @@ static void build_heap_profile_without_gvl(struct stack_recorder_state *state, d
689
746
  static void *call_serialize_without_gvl(void *call_args) {
690
747
  struct call_serialize_without_gvl_arguments *args = (struct call_serialize_without_gvl_arguments *) call_args;
691
748
 
692
- args->profile = serializer_flip_active_and_inactive_slots(args->state);
749
+ long serialize_no_gvl_start_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE);
750
+
751
+ profile_slot *slot_now_inactive = serializer_flip_active_and_inactive_slots(args->state);
752
+
753
+ args->slot = slot_now_inactive;
693
754
 
694
755
  // Now that we have the inactive profile with all but heap samples, lets fill it with heap data
695
756
  // without needing to race with the active sampler
696
- build_heap_profile_without_gvl(args->state, args->profile);
757
+ build_heap_profile_without_gvl(args->state, args->slot);
758
+ args->heap_profile_build_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - serialize_no_gvl_start_time_ns;
697
759
 
698
760
  // Note: The profile gets reset by the serialize call
699
- args->result = ddog_prof_Profile_serialize(args->profile, &args->finish_timestamp, NULL /* duration_nanos is optional */, NULL /* start_time is optional */);
761
+ args->result = ddog_prof_Profile_serialize(&args->slot->profile, &args->finish_timestamp, NULL /* duration_nanos is optional */, NULL /* start_time is optional */);
700
762
  args->serialize_ran = true;
763
+ args->serialize_no_gvl_time_ns = monotonic_wall_time_now_ns(DO_NOT_RAISE_ON_FAILURE) - serialize_no_gvl_start_time_ns;
701
764
 
702
765
  return NULL; // Unused
703
766
  }
@@ -707,42 +770,42 @@ VALUE enforce_recorder_instance(VALUE object) {
707
770
  return object;
708
771
  }
709
772
 
710
- static struct active_slot_pair sampler_lock_active_profile(struct stack_recorder_state *state) {
773
+ static locked_profile_slot sampler_lock_active_profile(struct stack_recorder_state *state) {
711
774
  int error;
712
775
 
713
776
  for (int attempts = 0; attempts < 2; attempts++) {
714
- error = pthread_mutex_trylock(&state->slot_one_mutex);
777
+ error = pthread_mutex_trylock(&state->mutex_slot_one);
715
778
  if (error && error != EBUSY) ENFORCE_SUCCESS_GVL(error);
716
779
 
717
780
  // Slot one is active
718
- if (!error) return (struct active_slot_pair) {.mutex = &state->slot_one_mutex, .profile = &state->slot_one_profile};
781
+ if (!error) return (locked_profile_slot) {.mutex = &state->mutex_slot_one, .data = &state->profile_slot_one};
719
782
 
720
783
  // If we got here, slot one was not active, let's try slot two
721
784
 
722
- error = pthread_mutex_trylock(&state->slot_two_mutex);
785
+ error = pthread_mutex_trylock(&state->mutex_slot_two);
723
786
  if (error && error != EBUSY) ENFORCE_SUCCESS_GVL(error);
724
787
 
725
788
  // Slot two is active
726
- if (!error) return (struct active_slot_pair) {.mutex = &state->slot_two_mutex, .profile = &state->slot_two_profile};
789
+ if (!error) return (locked_profile_slot) {.mutex = &state->mutex_slot_two, .data = &state->profile_slot_two};
727
790
  }
728
791
 
729
792
  // We already tried both multiple times, and we did not succeed. This is not expected to happen. Let's stop sampling.
730
793
  rb_raise(rb_eRuntimeError, "Failed to grab either mutex in sampler_lock_active_profile");
731
794
  }
732
795
 
733
- static void sampler_unlock_active_profile(struct active_slot_pair active_slot) {
796
+ static void sampler_unlock_active_profile(locked_profile_slot active_slot) {
734
797
  ENFORCE_SUCCESS_GVL(pthread_mutex_unlock(active_slot.mutex));
735
798
  }
736
799
 
737
- static ddog_prof_Profile *serializer_flip_active_and_inactive_slots(struct stack_recorder_state *state) {
800
+ static profile_slot* serializer_flip_active_and_inactive_slots(struct stack_recorder_state *state) {
738
801
  int previously_active_slot = state->active_slot;
739
802
 
740
803
  if (previously_active_slot != 1 && previously_active_slot != 2) {
741
804
  grab_gvl_and_raise(rb_eRuntimeError, "Unexpected active_slot state %d in serializer_flip_active_and_inactive_slots", previously_active_slot);
742
805
  }
743
806
 
744
- pthread_mutex_t *previously_active = (previously_active_slot == 1) ? &state->slot_one_mutex : &state->slot_two_mutex;
745
- pthread_mutex_t *previously_inactive = (previously_active_slot == 1) ? &state->slot_two_mutex : &state->slot_one_mutex;
807
+ pthread_mutex_t *previously_active = (previously_active_slot == 1) ? &state->mutex_slot_one : &state->mutex_slot_two;
808
+ pthread_mutex_t *previously_inactive = (previously_active_slot == 1) ? &state->mutex_slot_two : &state->mutex_slot_one;
746
809
 
747
810
  // Release the lock, thus making this slot active
748
811
  ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(previously_inactive));
@@ -753,8 +816,8 @@ static ddog_prof_Profile *serializer_flip_active_and_inactive_slots(struct stack
753
816
  // Update active_slot
754
817
  state->active_slot = (previously_active_slot == 1) ? 2 : 1;
755
818
 
756
- // Return profile for previously active slot (now inactive)
757
- return (previously_active_slot == 1) ? &state->slot_one_profile : &state->slot_two_profile;
819
+ // Return pointer to previously active slot (now inactive)
820
+ return (previously_active_slot == 1) ? &state->profile_slot_one : &state->profile_slot_two;
758
821
  }
759
822
 
760
823
  // This method exists only to enable testing Datadog::Profiling::StackRecorder behavior using RSpec.
@@ -778,7 +841,7 @@ static VALUE test_slot_mutex_state(VALUE recorder_instance, int slot) {
778
841
  struct stack_recorder_state *state;
779
842
  TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
780
843
 
781
- pthread_mutex_t *slot_mutex = (slot == 1) ? &state->slot_one_mutex : &state->slot_two_mutex;
844
+ pthread_mutex_t *slot_mutex = (slot == 1) ? &state->mutex_slot_one : &state->mutex_slot_two;
782
845
 
783
846
  // Like Heisenberg's uncertainty principle, we can't observe without affecting...
784
847
  int error = pthread_mutex_trylock(slot_mutex);
@@ -813,8 +876,8 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE recorder_
813
876
  // resulting state is inconsistent, we make sure to reset it back to the initial state.
814
877
  initialize_slot_concurrency_control(state);
815
878
 
816
- reset_profile(&state->slot_one_profile, /* start_time: */ NULL);
817
- reset_profile(&state->slot_two_profile, /* start_time: */ NULL);
879
+ reset_profile_slot(&state->profile_slot_one, /* start_time: */ NULL);
880
+ reset_profile_slot(&state->profile_slot_two, /* start_time: */ NULL);
818
881
 
819
882
  heap_recorder_after_fork(state->heap_recorder);
820
883
 
@@ -825,8 +888,8 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE recorder_
825
888
  // not be interrupted part-way through by a VM fork.
826
889
  static void serializer_set_start_timestamp_for_next_profile(struct stack_recorder_state *state, ddog_Timespec start_time) {
827
890
  // Before making this profile active, we reset it so that it uses the correct start_time for its start
828
- ddog_prof_Profile *next_profile = (state->active_slot == 1) ? &state->slot_two_profile : &state->slot_one_profile;
829
- reset_profile(next_profile, &start_time);
891
+ profile_slot *next_profile_slot = (state->active_slot == 1) ? &state->profile_slot_two : &state->profile_slot_one;
892
+ reset_profile_slot(next_profile_slot, &start_time);
830
893
  }
831
894
 
832
895
  static VALUE _native_record_endpoint(DDTRACE_UNUSED VALUE _self, VALUE recorder_instance, VALUE local_root_span_id, VALUE endpoint) {
@@ -872,11 +935,12 @@ static VALUE _native_check_heap_hashes(DDTRACE_UNUSED VALUE _self, VALUE locatio
872
935
  return Qnil;
873
936
  }
874
937
 
875
- static void reset_profile(ddog_prof_Profile *profile, ddog_Timespec *start_time /* Can be null */) {
876
- ddog_prof_Profile_Result reset_result = ddog_prof_Profile_reset(profile, start_time);
938
+ static void reset_profile_slot(profile_slot *slot, ddog_Timespec *start_time /* Can be null */) {
939
+ ddog_prof_Profile_Result reset_result = ddog_prof_Profile_reset(&slot->profile, start_time);
877
940
  if (reset_result.tag == DDOG_PROF_PROFILE_RESULT_ERR) {
878
941
  rb_raise(rb_eRuntimeError, "Failed to reset profile: %"PRIsVALUE, get_error_details_and_drop(&reset_result.err));
879
942
  }
943
+ slot->stats = (stats_slot) {};
880
944
  }
881
945
 
882
946
  // This method exists only to enable testing Datadog::Profiling::StackRecorder behavior using RSpec.
@@ -937,3 +1001,40 @@ static VALUE _native_has_seen_id_flag(DDTRACE_UNUSED VALUE _self, VALUE obj) {
937
1001
  return Qfalse;
938
1002
  #endif
939
1003
  }
1004
+
1005
+ static VALUE _native_stats(DDTRACE_UNUSED VALUE self, VALUE recorder_instance) {
1006
+ struct stack_recorder_state *state;
1007
+ TypedData_Get_Struct(recorder_instance, struct stack_recorder_state, &stack_recorder_typed_data, state);
1008
+
1009
+ uint64_t total_serializations = state->stats_lifetime.serialization_successes + state->stats_lifetime.serialization_failures;
1010
+
1011
+ VALUE heap_recorder_snapshot = state->heap_recorder ?
1012
+ heap_recorder_state_snapshot(state->heap_recorder) : Qnil;
1013
+
1014
+ VALUE stats_as_hash = rb_hash_new();
1015
+ VALUE arguments[] = {
1016
+ ID2SYM(rb_intern("serialization_successes")), /* => */ ULL2NUM(state->stats_lifetime.serialization_successes),
1017
+ ID2SYM(rb_intern("serialization_failures")), /* => */ ULL2NUM(state->stats_lifetime.serialization_failures),
1018
+
1019
+ ID2SYM(rb_intern("serialization_time_ns_min")), /* => */ RUBY_NUM_OR_NIL(state->stats_lifetime.serialization_time_ns_min, != INT64_MAX, LONG2NUM),
1020
+ ID2SYM(rb_intern("serialization_time_ns_max")), /* => */ RUBY_NUM_OR_NIL(state->stats_lifetime.serialization_time_ns_max, > 0, LONG2NUM),
1021
+ ID2SYM(rb_intern("serialization_time_ns_total")), /* => */ RUBY_NUM_OR_NIL(state->stats_lifetime.serialization_time_ns_total, > 0, LONG2NUM),
1022
+ ID2SYM(rb_intern("serialization_time_ns_avg")), /* => */ RUBY_AVG_OR_NIL(state->stats_lifetime.serialization_time_ns_total, total_serializations),
1023
+
1024
+ ID2SYM(rb_intern("heap_recorder_snapshot")), /* => */ heap_recorder_snapshot,
1025
+ };
1026
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(stats_as_hash, arguments[i], arguments[i+1]);
1027
+ return stats_as_hash;
1028
+ }
1029
+
1030
+ static VALUE build_profile_stats(profile_slot *slot, long serialization_time_ns, long heap_iteration_prep_time_ns, long heap_profile_build_time_ns) {
1031
+ VALUE stats_as_hash = rb_hash_new();
1032
+ VALUE arguments[] = {
1033
+ ID2SYM(rb_intern("recorded_samples")), /* => */ ULL2NUM(slot->stats.recorded_samples),
1034
+ ID2SYM(rb_intern("serialization_time_ns")), /* => */ LONG2NUM(serialization_time_ns),
1035
+ ID2SYM(rb_intern("heap_iteration_prep_time_ns")), /* => */ LONG2NUM(heap_iteration_prep_time_ns),
1036
+ ID2SYM(rb_intern("heap_profile_build_time_ns")), /* => */ LONG2NUM(heap_profile_build_time_ns),
1037
+ };
1038
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(stats_as_hash, arguments[i], arguments[i+1]);
1039
+ return stats_as_hash;
1040
+ }
@@ -24,7 +24,7 @@ module Datadog
24
24
  seq_id = self.class.sequence.next
25
25
  payload = Request.build_payload(event, seq_id)
26
26
  res = @http_transport.request(request_type: event.type, payload: payload.to_json)
27
- Datadog.logger.debug { "Telemetry sent for event `#{event.type}` (status code: #{res.code})" }
27
+ Datadog.logger.debug { "Telemetry sent for event `#{event.type}` (code: #{res.code.inspect})" }
28
28
  res
29
29
  rescue => e
30
30
  Datadog.logger.debug("Unable to send telemetry request for event `#{event.type rescue 'unknown'}`: #{e}")
@@ -32,6 +32,10 @@ module Datadog
32
32
  nil
33
33
  end
34
34
 
35
+ def code
36
+ nil
37
+ end
38
+
35
39
  def inspect
36
40
  "#{self.class} ok?:#{ok?} unsupported?:#{unsupported?}, " \
37
41
  "not_found?:#{not_found?}, client_error?:#{client_error?}, " \
@@ -52,10 +52,11 @@ module Datadog
52
52
 
53
53
  def flush
54
54
  worker_stats = @worker.stats_and_reset_not_thread_safe
55
- start, finish, compressed_pprof = pprof_recorder.serialize
56
- @last_flush_finish_at = finish
55
+ serialization_result = pprof_recorder.serialize
56
+ return if serialization_result.nil?
57
57
 
58
- return if compressed_pprof.nil? # We don't want to report empty profiles
58
+ start, finish, compressed_pprof, profile_stats = serialization_result
59
+ @last_flush_finish_at = finish
59
60
 
60
61
  if duration_below_threshold?(start, finish)
61
62
  Datadog.logger.debug('Skipped exporting profiling events as profile duration is below minimum')
@@ -75,6 +76,8 @@ module Datadog
75
76
  internal_metadata: internal_metadata.merge(
76
77
  {
77
78
  worker_stats: worker_stats,
79
+ profile_stats: profile_stats,
80
+ recorder_stats: pprof_recorder.stats,
78
81
  gc: GC.stat,
79
82
  }
80
83
  ),
@@ -31,11 +31,11 @@ module Datadog
31
31
  status, result = @no_concurrent_synchronize_mutex.synchronize { self.class._native_serialize(self) }
32
32
 
33
33
  if status == :ok
34
- start, finish, encoded_pprof = result
34
+ start, finish, encoded_pprof, profile_stats = result
35
35
 
36
36
  Datadog.logger.debug { "Encoded profile covering #{start.iso8601} to #{finish.iso8601}" }
37
37
 
38
- [start, finish, encoded_pprof]
38
+ [start, finish, encoded_pprof, profile_stats]
39
39
  else
40
40
  error_message = result
41
41
 
@@ -62,6 +62,10 @@ module Datadog
62
62
  def reset_after_fork
63
63
  self.class._native_reset_after_fork(self)
64
64
  end
65
+
66
+ def stats
67
+ self.class._native_stats(self)
68
+ end
65
69
  end
66
70
  end
67
71
  end
@@ -3,7 +3,7 @@
3
3
  module DDTrace
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 22
6
+ MINOR = 23
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
  BUILD = nil
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ddtrace
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.22.0
4
+ version: 1.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-16 00:00:00.000000000 Z
11
+ date: 2024-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -879,7 +879,7 @@ licenses:
879
879
  metadata:
880
880
  allowed_push_host: https://rubygems.org
881
881
  changelog_uri: https://github.com/DataDog/dd-trace-rb/blob/master/CHANGELOG.md
882
- post_install_message:
882
+ post_install_message:
883
883
  rdoc_options: []
884
884
  require_paths:
885
885
  - lib
@@ -897,8 +897,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
897
897
  - !ruby/object:Gem::Version
898
898
  version: 2.0.0
899
899
  requirements: []
900
- rubygems_version: 3.5.3
901
- signing_key:
900
+ rubygems_version: 3.4.10
901
+ signing_key:
902
902
  specification_version: 4
903
903
  summary: Datadog tracing code for your Ruby applications
904
904
  test_files: []