ddtrace 1.22.0 → 1.23.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.
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: []