datadog 2.16.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +12 -46
  4. data/ext/datadog_profiling_native_extension/collectors_stack.c +227 -49
  5. data/ext/datadog_profiling_native_extension/collectors_stack.h +19 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +63 -12
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +1 -0
  8. data/ext/datadog_profiling_native_extension/encoded_profile.c +22 -12
  9. data/ext/datadog_profiling_native_extension/encoded_profile.h +1 -0
  10. data/ext/datadog_profiling_native_extension/extconf.rb +7 -0
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +239 -363
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -6
  13. data/ext/datadog_profiling_native_extension/http_transport.c +45 -72
  14. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +22 -0
  15. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +8 -5
  16. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +1 -0
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +6 -3
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -13
  19. data/ext/datadog_profiling_native_extension/ruby_helpers.h +2 -10
  20. data/ext/datadog_profiling_native_extension/stack_recorder.c +156 -60
  21. data/ext/libdatadog_api/crashtracker.c +10 -3
  22. data/ext/libdatadog_api/extconf.rb +2 -2
  23. data/ext/libdatadog_api/library_config.c +54 -12
  24. data/ext/libdatadog_api/library_config.h +6 -0
  25. data/ext/libdatadog_api/macos_development.md +3 -3
  26. data/ext/libdatadog_api/process_discovery.c +2 -7
  27. data/ext/libdatadog_extconf_helpers.rb +2 -2
  28. data/lib/datadog/appsec/api_security/lru_cache.rb +56 -0
  29. data/lib/datadog/appsec/api_security/route_extractor.rb +65 -0
  30. data/lib/datadog/appsec/api_security/sampler.rb +59 -0
  31. data/lib/datadog/appsec/api_security.rb +23 -0
  32. data/lib/datadog/appsec/assets/waf_rules/recommended.json +257 -85
  33. data/lib/datadog/appsec/assets/waf_rules/strict.json +10 -78
  34. data/lib/datadog/appsec/component.rb +30 -54
  35. data/lib/datadog/appsec/configuration/settings.rb +60 -2
  36. data/lib/datadog/appsec/context.rb +6 -6
  37. data/lib/datadog/appsec/contrib/devise/tracking_middleware.rb +1 -1
  38. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +27 -16
  39. data/lib/datadog/appsec/processor/rule_loader.rb +5 -6
  40. data/lib/datadog/appsec/remote.rb +15 -55
  41. data/lib/datadog/appsec/security_engine/engine.rb +194 -0
  42. data/lib/datadog/appsec/security_engine/runner.rb +10 -11
  43. data/lib/datadog/appsec.rb +4 -7
  44. data/lib/datadog/core/buffer/random.rb +18 -2
  45. data/lib/datadog/core/configuration/agent_settings.rb +52 -0
  46. data/lib/datadog/core/configuration/agent_settings_resolver.rb +4 -46
  47. data/lib/datadog/core/configuration/components.rb +31 -24
  48. data/lib/datadog/core/configuration/components_state.rb +23 -0
  49. data/lib/datadog/core/configuration/option.rb +27 -27
  50. data/lib/datadog/core/configuration/option_definition.rb +4 -4
  51. data/lib/datadog/core/configuration/options.rb +1 -1
  52. data/lib/datadog/core/configuration/settings.rb +32 -20
  53. data/lib/datadog/core/configuration/stable_config.rb +1 -2
  54. data/lib/datadog/core/configuration.rb +16 -16
  55. data/lib/datadog/core/crashtracking/component.rb +2 -1
  56. data/lib/datadog/core/crashtracking/tag_builder.rb +4 -22
  57. data/lib/datadog/core/encoding.rb +1 -1
  58. data/lib/datadog/core/environment/cgroup.rb +10 -12
  59. data/lib/datadog/core/environment/container.rb +38 -40
  60. data/lib/datadog/core/environment/ext.rb +6 -6
  61. data/lib/datadog/core/environment/identity.rb +3 -3
  62. data/lib/datadog/core/environment/platform.rb +3 -3
  63. data/lib/datadog/core/error.rb +11 -9
  64. data/lib/datadog/core/logger.rb +2 -2
  65. data/lib/datadog/core/metrics/client.rb +12 -14
  66. data/lib/datadog/core/metrics/logging.rb +5 -5
  67. data/lib/datadog/core/process_discovery/tracer_memfd.rb +15 -0
  68. data/lib/datadog/core/process_discovery.rb +5 -1
  69. data/lib/datadog/core/rate_limiter.rb +4 -2
  70. data/lib/datadog/core/remote/client.rb +32 -31
  71. data/lib/datadog/core/remote/component.rb +3 -3
  72. data/lib/datadog/core/remote/configuration/digest.rb +7 -7
  73. data/lib/datadog/core/remote/configuration/path.rb +1 -1
  74. data/lib/datadog/core/remote/configuration/repository.rb +12 -0
  75. data/lib/datadog/core/remote/transport/http/client.rb +1 -1
  76. data/lib/datadog/core/remote/transport/http/config.rb +21 -5
  77. data/lib/datadog/core/remote/transport/http/negotiation.rb +1 -1
  78. data/lib/datadog/core/runtime/metrics.rb +3 -3
  79. data/lib/datadog/core/tag_builder.rb +56 -0
  80. data/lib/datadog/core/telemetry/component.rb +39 -24
  81. data/lib/datadog/core/telemetry/emitter.rb +7 -1
  82. data/lib/datadog/core/telemetry/event/app_client_configuration_change.rb +66 -0
  83. data/lib/datadog/core/telemetry/event/app_closing.rb +18 -0
  84. data/lib/datadog/core/telemetry/event/app_dependencies_loaded.rb +33 -0
  85. data/lib/datadog/core/telemetry/event/app_heartbeat.rb +18 -0
  86. data/lib/datadog/core/telemetry/event/app_integrations_change.rb +58 -0
  87. data/lib/datadog/core/telemetry/event/app_started.rb +269 -0
  88. data/lib/datadog/core/telemetry/event/base.rb +40 -0
  89. data/lib/datadog/core/telemetry/event/distributions.rb +18 -0
  90. data/lib/datadog/core/telemetry/event/generate_metrics.rb +43 -0
  91. data/lib/datadog/core/telemetry/event/log.rb +76 -0
  92. data/lib/datadog/core/telemetry/event/message_batch.rb +42 -0
  93. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +43 -0
  94. data/lib/datadog/core/telemetry/event.rb +17 -475
  95. data/lib/datadog/core/telemetry/logger.rb +5 -4
  96. data/lib/datadog/core/telemetry/logging.rb +11 -5
  97. data/lib/datadog/core/telemetry/metric.rb +3 -3
  98. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +2 -2
  99. data/lib/datadog/core/telemetry/transport/telemetry.rb +0 -1
  100. data/lib/datadog/core/telemetry/worker.rb +48 -27
  101. data/lib/datadog/core/transport/http/adapters/net.rb +17 -2
  102. data/lib/datadog/core/transport/http/adapters/test.rb +2 -1
  103. data/lib/datadog/core/transport/http/builder.rb +14 -14
  104. data/lib/datadog/core/transport/http/env.rb +8 -0
  105. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +6 -6
  106. data/lib/datadog/core/utils/duration.rb +32 -32
  107. data/lib/datadog/core/utils/forking.rb +2 -2
  108. data/lib/datadog/core/utils/network.rb +6 -6
  109. data/lib/datadog/core/utils/only_once_successful.rb +16 -5
  110. data/lib/datadog/core/utils/time.rb +10 -2
  111. data/lib/datadog/core/utils/truncation.rb +21 -0
  112. data/lib/datadog/core/utils.rb +7 -0
  113. data/lib/datadog/core/vendor/multipart-post/multipart/post/composite_read_io.rb +1 -1
  114. data/lib/datadog/core/vendor/multipart-post/multipart/post/multipartable.rb +8 -8
  115. data/lib/datadog/core/vendor/multipart-post/multipart/post/parts.rb +7 -7
  116. data/lib/datadog/core/worker.rb +1 -1
  117. data/lib/datadog/core/workers/async.rb +9 -10
  118. data/lib/datadog/di/instrumenter.rb +52 -2
  119. data/lib/datadog/di/probe_notification_builder.rb +31 -41
  120. data/lib/datadog/di/probe_notifier_worker.rb +9 -1
  121. data/lib/datadog/di/serializer.rb +6 -2
  122. data/lib/datadog/di/transport/http/input.rb +10 -0
  123. data/lib/datadog/di/transport/input.rb +10 -2
  124. data/lib/datadog/error_tracking/component.rb +2 -2
  125. data/lib/datadog/profiling/collectors/code_provenance.rb +18 -9
  126. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +4 -0
  127. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -0
  128. data/lib/datadog/profiling/collectors/thread_context.rb +16 -1
  129. data/lib/datadog/profiling/component.rb +7 -9
  130. data/lib/datadog/profiling/ext.rb +0 -13
  131. data/lib/datadog/profiling/flush.rb +1 -1
  132. data/lib/datadog/profiling/http_transport.rb +3 -8
  133. data/lib/datadog/profiling/profiler.rb +2 -0
  134. data/lib/datadog/profiling/scheduler.rb +10 -2
  135. data/lib/datadog/profiling/stack_recorder.rb +5 -5
  136. data/lib/datadog/profiling/tag_builder.rb +5 -41
  137. data/lib/datadog/profiling/tasks/setup.rb +2 -0
  138. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +15 -0
  139. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +19 -12
  140. data/lib/datadog/tracing/contrib/action_pack/ext.rb +2 -0
  141. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +4 -1
  142. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +33 -0
  143. data/lib/datadog/tracing/contrib/active_support/cache/patcher.rb +4 -0
  144. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +2 -4
  145. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +10 -0
  146. data/lib/datadog/tracing/contrib/aws/parsed_context.rb +5 -1
  147. data/lib/datadog/tracing/contrib/http/instrumentation.rb +1 -5
  148. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +1 -5
  149. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +1 -5
  150. data/lib/datadog/tracing/contrib/lograge/patcher.rb +4 -2
  151. data/lib/datadog/tracing/contrib/patcher.rb +5 -2
  152. data/lib/datadog/tracing/contrib/sidekiq/ext.rb +1 -0
  153. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +5 -2
  154. data/lib/datadog/tracing/contrib/support.rb +28 -0
  155. data/lib/datadog/tracing/metadata/errors.rb +4 -4
  156. data/lib/datadog/tracing/sync_writer.rb +1 -1
  157. data/lib/datadog/tracing/trace_operation.rb +12 -4
  158. data/lib/datadog/tracing/tracer.rb +6 -2
  159. data/lib/datadog/version.rb +1 -1
  160. metadata +31 -12
  161. data/lib/datadog/appsec/assets/waf_rules/processors.json +0 -321
  162. data/lib/datadog/appsec/assets/waf_rules/scanners.json +0 -1023
  163. data/lib/datadog/appsec/processor/rule_merger.rb +0 -171
  164. data/lib/datadog/appsec/processor.rb +0 -107
@@ -5,6 +5,18 @@
5
5
  #include "libdatadog_helpers.h"
6
6
  #include "time_helpers.h"
7
7
 
8
+ // Note on calloc vs ruby_xcalloc use:
9
+ // * Whenever we're allocating memory after being called by the Ruby VM in a "regular" situation (e.g. initializer)
10
+ // we should use `ruby_xcalloc` to give the VM visibility into what we're doing + give it a chance to manage GC
11
+ // * BUT, when we're being called during a sample, being in the middle of an object allocation is a very special
12
+ // situation for the VM to be in, and we've found the hard way (e.g. https://bugs.ruby-lang.org/issues/20629 and
13
+ // https://github.com/DataDog/dd-trace-rb/pull/4240 ) that it can be easy to do things the VM didn't expect.
14
+ // * Thus, out of caution and to avoid future potential issues such as the ones above, whenever we allocate memory
15
+ // during **sampling** we use `calloc` instead of `ruby_xcalloc`. Note that we've never seen issues from using
16
+ // `ruby_xcalloc` at any time, so this is a **precaution** not a "we've seen it break". But it seems a harmless
17
+ // one to use.
18
+ // This applies to both heap_recorder.c and collectors_thread_context.c
19
+
8
20
  // Minimum age (in GC generations) of heap objects we want to include in heap
9
21
  // recorder iterations. Object with age 0 represent objects that have yet to undergo
10
22
  // a GC and, thus, may just be noise/trash at instant of iteration and are usually not
@@ -24,80 +36,37 @@
24
36
 
25
37
  // A compact representation of a stacktrace frame for a heap allocation.
26
38
  typedef struct {
27
- char *name;
28
- char *filename;
39
+ ddog_prof_ManagedStringId name;
40
+ ddog_prof_ManagedStringId filename;
29
41
  int32_t line;
30
42
  } heap_frame;
31
43
 
44
+ // We use memcmp/st_hash below to compare/hash an entire array of heap_frames, so want to make sure no padding is added
45
+ // We could define the structure to be packed, but that seems even weirder across compilers, and this seems more portable?
46
+ _Static_assert(
47
+ sizeof(heap_frame) == sizeof(ddog_prof_ManagedStringId) * 2 + sizeof(int32_t),
48
+ "Size of heap_frame does not match the sum of its members. Padding detected."
49
+ );
50
+
32
51
  // A compact representation of a stacktrace for a heap allocation.
33
- //
34
- // We could use a ddog_prof_Slice_Location instead but it has a lot of
35
- // unused fields. Because we have to keep these stacks around for at
36
- // least the lifetime of the objects allocated therein, we would be
37
- // incurring a non-negligible memory overhead for little purpose.
52
+ // Used to dedup heap allocation stacktraces across multiple objects sharing the same allocation location.
38
53
  typedef struct {
54
+ // How many objects are currently tracked in object_records recorder for this heap record.
55
+ uint32_t num_tracked_objects;
56
+
39
57
  uint16_t frames_len;
40
58
  heap_frame frames[];
41
- } heap_stack;
42
- static heap_stack* heap_stack_new(ddog_prof_Slice_Location);
43
- static void heap_stack_free(heap_stack*);
44
- static st_index_t heap_stack_hash(heap_stack*, st_index_t);
59
+ } heap_record;
60
+ static heap_record* heap_record_new(heap_recorder*, ddog_prof_Slice_Location);
61
+ static void heap_record_free(heap_recorder*, heap_record*);
45
62
 
46
63
  #if MAX_FRAMES_LIMIT > UINT16_MAX
47
64
  #error Frames len type not compatible with MAX_FRAMES_LIMIT
48
65
  #endif
49
66
 
50
- enum heap_record_key_type {
51
- HEAP_STACK,
52
- LOCATION_SLICE
53
- };
54
- // This struct allows us to use two different types of stacks when
55
- // interacting with a heap_record hash.
56
- //
57
- // The idea is that we'll always want to use heap_stack-keys when
58
- // adding new entries to the hash since that's the compact stack
59
- // representation we rely on internally.
60
- //
61
- // However, when querying for an existing heap record, we'd save a
62
- // lot of allocations if we could query with the
63
- // ddog_prof_Slice_Location we receive in our external API.
64
- //
65
- // To allow this interchange, we need a union and need to ensure
66
- // that whatever shape of the union, the heap_record_key_cmp_st
67
- // and heap_record_hash_st functions return the same results for
68
- // equivalent stacktraces.
69
- typedef struct {
70
- enum heap_record_key_type type;
71
- union {
72
- // key never owns this if set
73
- heap_stack *heap_stack;
74
- // key never owns this if set
75
- ddog_prof_Slice_Location *location_slice;
76
- };
77
- } heap_record_key;
78
- static heap_record_key* heap_record_key_new(heap_stack*);
79
- static void heap_record_key_free(heap_record_key*);
80
- static int heap_record_key_cmp_st(st_data_t, st_data_t);
81
- static st_index_t heap_record_key_hash_st(st_data_t);
82
- static const struct st_hash_type st_hash_type_heap_record_key = {
83
- heap_record_key_cmp_st,
84
- heap_record_key_hash_st,
85
- };
86
-
87
- // Need to implement these functions to support the location-slice based keys
88
- static st_index_t ddog_location_hash(ddog_prof_Location, st_index_t seed);
89
- static st_index_t ddog_location_slice_hash(ddog_prof_Slice_Location, st_index_t seed);
90
-
91
- // A heap record is used for deduping heap allocation stacktraces across multiple
92
- // objects sharing the same allocation location.
93
- typedef struct {
94
- // How many objects are currently tracked by the heap recorder for this heap record.
95
- uint32_t num_tracked_objects;
96
- // stack is owned by the associated record and gets cleaned up alongside it
97
- heap_stack *stack;
98
- } heap_record;
99
- static heap_record* heap_record_new(heap_stack*);
100
- static void heap_record_free(heap_record*);
67
+ static int heap_record_cmp_st(st_data_t, st_data_t);
68
+ static st_index_t heap_record_hash_st(st_data_t);
69
+ static const struct st_hash_type st_hash_type_heap_record = { .compare = heap_record_cmp_st, .hash = heap_record_hash_st };
101
70
 
102
71
  // An object record is used for storing data about currently tracked live objects
103
72
  typedef struct {
@@ -106,8 +75,8 @@ typedef struct {
106
75
  live_object_data object_data;
107
76
  } object_record;
108
77
  static object_record* object_record_new(long, heap_record*, live_object_data);
109
- static void object_record_free(object_record*);
110
- static VALUE object_record_inspect(object_record*);
78
+ static void object_record_free(heap_recorder*, object_record*);
79
+ static VALUE object_record_inspect(heap_recorder*, object_record*);
111
80
  static object_record SKIPPED_RECORD = {0};
112
81
 
113
82
  struct heap_recorder {
@@ -116,12 +85,15 @@ struct heap_recorder {
116
85
  bool size_enabled;
117
86
  uint sample_rate;
118
87
 
119
- // Map[key: heap_record_key*, record: heap_record*]
120
- // NOTE: We always use heap_record_key.type == HEAP_STACK for storage but support lookups
121
- // via heap_record_key.type == LOCATION_SLICE to allow for allocation-free fast-paths.
88
+ // Map[key: heap_record*, record: nothing] (This is a set, basically)
122
89
  // NOTE: This table is currently only protected by the GVL since we never interact with it
123
90
  // outside the GVL.
124
- // NOTE: This table has ownership of both its heap_record_keys and heap_records.
91
+ // NOTE: This table has ownership of its heap_records.
92
+ //
93
+ // This is a cpu/memory trade-off: Maintaining the "heap_records" map means we spend extra CPU when sampling as we need
94
+ // to do de-duplication, but we reduce the memory footprint of the heap profiler.
95
+ // In the future, it may be worth revisiting if we can move this inside libdatadog: if libdatadog was able to track
96
+ // entire stacks for us, then we wouldn't need to do it on the Ruby side.
125
97
  st_table *heap_records;
126
98
 
127
99
  // Map[obj_id: long, record: object_record*]
@@ -132,6 +104,8 @@ struct heap_recorder {
132
104
  //
133
105
  // TODO: @ivoanjo We've evolved to actually never need to look up on object_records (we only insert and iterate),
134
106
  // so right now this seems to be just a really really fancy self-resizing list/set.
107
+ // If we replace this with a list, we could record the latest id and compare it when inserting to make sure our
108
+ // assumption of ids never reused + always increasing always holds. (This as an alternative to checking for duplicates)
135
109
  st_table *object_records;
136
110
 
137
111
  // Map[obj_id: long, record: object_record*]
@@ -156,12 +130,19 @@ struct heap_recorder {
156
130
  // Data for a heap recording that was started but not yet ended
157
131
  object_record *active_recording;
158
132
 
159
- // Reusable location array, implementing a flyweight pattern for things like iteration.
133
+ // Reusable arrays, implementing a flyweight pattern for things like iteration
134
+ #define REUSABLE_LOCATIONS_SIZE MAX_FRAMES_LIMIT
160
135
  ddog_prof_Location *reusable_locations;
161
136
 
137
+ #define REUSABLE_FRAME_DETAILS_SIZE (2 * MAX_FRAMES_LIMIT) // because it'll be used for both function names AND file names)
138
+ ddog_prof_ManagedStringId *reusable_ids;
139
+ ddog_CharSlice *reusable_char_slices;
140
+
162
141
  // Sampling state
163
142
  uint num_recordings_skipped;
164
143
 
144
+ ddog_prof_ManagedStringStorage string_storage;
145
+
165
146
  struct stats_last_update {
166
147
  size_t objects_alive;
167
148
  size_t objects_dead;
@@ -185,10 +166,10 @@ struct heap_recorder {
185
166
  } stats_lifetime;
186
167
  };
187
168
 
188
- struct end_heap_allocation_args {
189
- struct heap_recorder *heap_recorder;
169
+ typedef struct {
170
+ heap_recorder *heap_recorder;
190
171
  ddog_prof_Slice_Location locations;
191
- };
172
+ } end_heap_allocation_args;
192
173
 
193
174
  static heap_record* get_or_create_heap_record(heap_recorder*, ddog_prof_Slice_Location);
194
175
  static void cleanup_heap_record_if_unused(heap_recorder*, heap_record*);
@@ -203,6 +184,9 @@ static void commit_recording(heap_recorder *, heap_record *, object_record *acti
203
184
  static VALUE end_heap_allocation_recording(VALUE end_heap_allocation_args);
204
185
  static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update);
205
186
  static inline double ewma_stat(double previous, double current);
187
+ static void unintern_or_raise(heap_recorder *, ddog_prof_ManagedStringId);
188
+ static void unintern_all_or_raise(heap_recorder *recorder, ddog_prof_Slice_ManagedStringId ids);
189
+ static VALUE get_ruby_string_or_raise(heap_recorder*, ddog_prof_ManagedStringId);
206
190
 
207
191
  // ==========================
208
192
  // Heap Recorder External API
@@ -213,16 +197,19 @@ static inline double ewma_stat(double previous, double current);
213
197
  // happens under the GVL.
214
198
  //
215
199
  // ==========================
216
- heap_recorder* heap_recorder_new(void) {
200
+ heap_recorder* heap_recorder_new(ddog_prof_ManagedStringStorage string_storage) {
217
201
  heap_recorder *recorder = ruby_xcalloc(1, sizeof(heap_recorder));
218
202
 
219
- recorder->heap_records = st_init_table(&st_hash_type_heap_record_key);
203
+ recorder->heap_records = st_init_table(&st_hash_type_heap_record);
220
204
  recorder->object_records = st_init_numtable();
221
205
  recorder->object_records_snapshot = NULL;
222
- recorder->reusable_locations = ruby_xcalloc(MAX_FRAMES_LIMIT, sizeof(ddog_prof_Location));
206
+ recorder->reusable_locations = ruby_xcalloc(REUSABLE_LOCATIONS_SIZE, sizeof(ddog_prof_Location));
207
+ recorder->reusable_ids = ruby_xcalloc(REUSABLE_FRAME_DETAILS_SIZE, sizeof(ddog_prof_ManagedStringId));
208
+ recorder->reusable_char_slices = ruby_xcalloc(REUSABLE_FRAME_DETAILS_SIZE, sizeof(ddog_CharSlice));
223
209
  recorder->active_recording = NULL;
224
210
  recorder->size_enabled = true;
225
211
  recorder->sample_rate = 1; // By default do no sampling on top of what allocation profiling already does
212
+ recorder->string_storage = string_storage;
226
213
 
227
214
  return recorder;
228
215
  }
@@ -239,19 +226,21 @@ void heap_recorder_free(heap_recorder *heap_recorder) {
239
226
  }
240
227
 
241
228
  // Clean-up all object records
242
- st_foreach(heap_recorder->object_records, st_object_record_entry_free, 0);
229
+ st_foreach(heap_recorder->object_records, st_object_record_entry_free, (st_data_t) heap_recorder);
243
230
  st_free_table(heap_recorder->object_records);
244
231
 
245
232
  // Clean-up all heap records (this includes those only referred to by queued_samples)
246
- st_foreach(heap_recorder->heap_records, st_heap_record_entry_free, 0);
233
+ st_foreach(heap_recorder->heap_records, st_heap_record_entry_free, (st_data_t) heap_recorder);
247
234
  st_free_table(heap_recorder->heap_records);
248
235
 
249
236
  if (heap_recorder->active_recording != NULL && heap_recorder->active_recording != &SKIPPED_RECORD) {
250
237
  // If there's a partial object record, clean it up as well
251
- object_record_free(heap_recorder->active_recording);
238
+ object_record_free(heap_recorder, heap_recorder->active_recording);
252
239
  }
253
240
 
254
241
  ruby_xfree(heap_recorder->reusable_locations);
242
+ ruby_xfree(heap_recorder->reusable_ids);
243
+ ruby_xfree(heap_recorder->reusable_char_slices);
255
244
 
256
245
  ruby_xfree(heap_recorder);
257
246
  }
@@ -321,6 +310,10 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
321
310
  #else
322
311
  false
323
312
  #endif
313
+ // If we got really unlucky and an allocation showed up during an update (because it triggered an allocation
314
+ // directly OR because the GVL got released in the middle of an update), let's skip this sample as well.
315
+ // See notes on `heap_recorder_update` for details.
316
+ || heap_recorder->updating
324
317
  ) {
325
318
  heap_recorder->active_recording = &SKIPPED_RECORD;
326
319
  return;
@@ -338,7 +331,7 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
338
331
  NULL,
339
332
  (live_object_data) {
340
333
  .weight = weight * heap_recorder->sample_rate,
341
- .class = string_from_char_slice(alloc_class),
334
+ .class = intern_or_raise(heap_recorder->string_storage, alloc_class),
342
335
  .alloc_gen = rb_gc_count(),
343
336
  }
344
337
  );
@@ -348,24 +341,30 @@ void start_heap_allocation_recording(heap_recorder *heap_recorder, VALUE new_obj
348
341
  // locks. To enable us to correctly unlock the profile on exception, we wrap the call to end_heap_allocation_recording
349
342
  // with an rb_protect.
350
343
  __attribute__((warn_unused_result))
351
- int end_heap_allocation_recording_with_rb_protect(struct heap_recorder *heap_recorder, ddog_prof_Slice_Location locations) {
344
+ int end_heap_allocation_recording_with_rb_protect(heap_recorder *heap_recorder, ddog_prof_Slice_Location locations) {
352
345
  if (heap_recorder == NULL) {
353
346
  return 0;
354
347
  }
348
+ if (heap_recorder->active_recording == &SKIPPED_RECORD) {
349
+ // Short circuit, in this case there's nothing to be done
350
+ heap_recorder->active_recording = NULL;
351
+ return 0;
352
+ }
353
+
355
354
 
356
355
  int exception_state;
357
- struct end_heap_allocation_args end_heap_allocation_args = {
356
+ end_heap_allocation_args args = {
358
357
  .heap_recorder = heap_recorder,
359
358
  .locations = locations,
360
359
  };
361
- rb_protect(end_heap_allocation_recording, (VALUE) &end_heap_allocation_args, &exception_state);
360
+ rb_protect(end_heap_allocation_recording, (VALUE) &args, &exception_state);
362
361
  return exception_state;
363
362
  }
364
363
 
365
- static VALUE end_heap_allocation_recording(VALUE end_heap_allocation_args) {
366
- struct end_heap_allocation_args *args = (struct end_heap_allocation_args *) end_heap_allocation_args;
364
+ static VALUE end_heap_allocation_recording(VALUE protect_args) {
365
+ end_heap_allocation_args *args = (end_heap_allocation_args *) protect_args;
367
366
 
368
- struct heap_recorder *heap_recorder = args->heap_recorder;
367
+ heap_recorder *heap_recorder = args->heap_recorder;
369
368
  ddog_prof_Slice_Location locations = args->locations;
370
369
 
371
370
  object_record *active_recording = heap_recorder->active_recording;
@@ -380,6 +379,7 @@ static VALUE end_heap_allocation_recording(VALUE end_heap_allocation_args) {
380
379
  heap_recorder->active_recording = NULL;
381
380
 
382
381
  if (active_recording == &SKIPPED_RECORD) { // special marker when we decided to skip due to sampling
382
+ // Note: Remember to update the short circuit in end_heap_allocation_recording_with_rb_protect if this logic changes
383
383
  return Qnil;
384
384
  }
385
385
 
@@ -399,15 +399,28 @@ void heap_recorder_update_young_objects(heap_recorder *heap_recorder) {
399
399
  heap_recorder_update(heap_recorder, /* full_update: */ false);
400
400
  }
401
401
 
402
+ // NOTE: This function needs and assumes it gets called with the GVL being held.
403
+ // But importantly **some of the operations inside `st_object_record_update` may cause a thread switch**,
404
+ // so we can't assume a single update happens in a single "atomic" step -- other threads may get some running time
405
+ // in the meanwhile.
402
406
  static void heap_recorder_update(heap_recorder *heap_recorder, bool full_update) {
403
407
  if (heap_recorder->updating) {
404
- if (full_update) rb_raise(rb_eRuntimeError, "BUG: full_update should not be triggered during another update");
405
-
406
- // If we try to update while another update is still running, short-circuit.
407
- // NOTE: This runs while holding the GVL. But since updates may be triggered from GC activity, there's still
408
- // a chance for updates to be attempted concurrently if scheduling gods so determine.
409
- heap_recorder->stats_lifetime.updates_skipped_concurrent++;
410
- return;
408
+ if (full_update) {
409
+ // There's another thread that's already doing an update :(
410
+ //
411
+ // Because there's a lock on the `StackRecorder` (see @no_concurrent_serialize_mutex) then it's not possible that
412
+ // the other update is a full update.
413
+ // Thus we expect is happening is that the GVL got released by the other thread in the middle of a non-full update
414
+ // and the scheduler thread decided now was a great time to serialize the profile.
415
+ //
416
+ // So, let's yield the time on the current thread until Ruby goes back to the other thread doing the update and
417
+ // it finishes cleanly.
418
+ while (heap_recorder->updating) { rb_thread_schedule(); }
419
+ } else {
420
+ // Non-full updates are optional, so let's walk away
421
+ heap_recorder->stats_lifetime.updates_skipped_concurrent++;
422
+ return;
423
+ }
411
424
  }
412
425
 
413
426
  if (heap_recorder->object_records_snapshot != NULL) {
@@ -561,26 +574,10 @@ VALUE heap_recorder_state_snapshot(heap_recorder *heap_recorder) {
561
574
  return hash;
562
575
  }
563
576
 
564
- void heap_recorder_testonly_assert_hash_matches(ddog_prof_Slice_Location locations) {
565
- heap_stack *stack = heap_stack_new(locations);
566
- heap_record_key stack_based_key = (heap_record_key) {
567
- .type = HEAP_STACK,
568
- .heap_stack = stack,
569
- };
570
- heap_record_key location_based_key = (heap_record_key) {
571
- .type = LOCATION_SLICE,
572
- .location_slice = &locations,
573
- };
574
-
575
- st_index_t stack_hash = heap_record_key_hash_st((st_data_t) &stack_based_key);
576
- st_index_t location_hash = heap_record_key_hash_st((st_data_t) &location_based_key);
577
-
578
- heap_stack_free(stack);
579
-
580
- if (stack_hash != location_hash) {
581
- rb_raise(rb_eRuntimeError, "Heap record key hashes built from the same locations differ. stack_based_hash=%"PRI_VALUE_PREFIX"u location_based_hash=%"PRI_VALUE_PREFIX"u", stack_hash, location_hash);
582
- }
583
- }
577
+ typedef struct {
578
+ heap_recorder *recorder;
579
+ VALUE debug_str;
580
+ } debug_context;
584
581
 
585
582
  VALUE heap_recorder_testonly_debug(heap_recorder *heap_recorder) {
586
583
  if (heap_recorder == NULL) {
@@ -588,7 +585,8 @@ VALUE heap_recorder_testonly_debug(heap_recorder *heap_recorder) {
588
585
  }
589
586
 
590
587
  VALUE debug_str = rb_str_new2("object records:\n");
591
- st_foreach(heap_recorder->object_records, st_object_records_debug, (st_data_t) debug_str);
588
+ debug_context context = (debug_context) {.recorder = heap_recorder, .debug_str = debug_str};
589
+ st_foreach(heap_recorder->object_records, st_object_records_debug, (st_data_t) &context);
592
590
 
593
591
  rb_str_catf(debug_str, "state snapshot: %"PRIsVALUE"\n------\n", heap_recorder_state_snapshot(heap_recorder));
594
592
 
@@ -598,18 +596,19 @@ VALUE heap_recorder_testonly_debug(heap_recorder *heap_recorder) {
598
596
  // ==========================
599
597
  // Heap Recorder Internal API
600
598
  // ==========================
601
- static int st_heap_record_entry_free(st_data_t key, st_data_t value, DDTRACE_UNUSED st_data_t extra_arg) {
602
- heap_record_key *record_key = (heap_record_key*) key;
603
- heap_record_key_free(record_key);
604
- heap_record_free((heap_record *) value);
599
+ static int st_heap_record_entry_free(st_data_t key, DDTRACE_UNUSED st_data_t value, st_data_t extra_arg) {
600
+ heap_recorder *recorder = (heap_recorder *) extra_arg;
601
+ heap_record_free(recorder, (heap_record *) key);
605
602
  return ST_DELETE;
606
603
  }
607
604
 
608
- static int st_object_record_entry_free(DDTRACE_UNUSED st_data_t key, st_data_t value, DDTRACE_UNUSED st_data_t extra_arg) {
609
- object_record_free((object_record *) value);
605
+ static int st_object_record_entry_free(DDTRACE_UNUSED st_data_t key, st_data_t value, st_data_t extra_arg) {
606
+ heap_recorder *recorder = (heap_recorder *) extra_arg;
607
+ object_record_free(recorder, (object_record *) value);
610
608
  return ST_DELETE;
611
609
  }
612
610
 
611
+ // NOTE: Some operations inside this function can cause the GVL to be released! Plan accordingly.
613
612
  static int st_object_record_update(st_data_t key, st_data_t value, st_data_t extra_arg) {
614
613
  long obj_id = (long) key;
615
614
  object_record *record = (object_record*) value;
@@ -635,7 +634,7 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
635
634
  return ST_CONTINUE;
636
635
  }
637
636
 
638
- if (!ruby_ref_from_id(LONG2NUM(obj_id), &ref)) {
637
+ if (!ruby_ref_from_id(LONG2NUM(obj_id), &ref)) { // Note: This function call can cause the GVL to be released
639
638
  // Id no longer associated with a valid ref. Need to delete this object record!
640
639
  on_committed_object_record_cleanup(recorder, record);
641
640
  recorder->stats_last_update.objects_dead++;
@@ -651,7 +650,8 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
651
650
  ) {
652
651
  // if we were asked to update sizes and this object was not already seen as being frozen,
653
652
  // update size again.
654
- record->object_data.size = ruby_obj_memsize_of(ref);
653
+ record->object_data.size = ruby_obj_memsize_of(ref); // Note: This function call can cause the GVL to be released... maybe?
654
+ // (With T_DATA for instance, since it can be a custom method supplied by extensions)
655
655
  // Check if it's now frozen so we skip a size update next time
656
656
  record->object_data.is_frozen = RB_OBJ_FROZEN(ref);
657
657
  }
@@ -671,7 +671,7 @@ static int st_object_record_update(st_data_t key, st_data_t value, st_data_t ext
671
671
  // WARN: This can get called outside the GVL. NO HEAP ALLOCATIONS OR EXCEPTIONS ARE ALLOWED.
672
672
  static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t value, st_data_t extra) {
673
673
  object_record *record = (object_record*) value;
674
- const heap_stack *stack = record->heap_record->stack;
674
+ const heap_record *stack = record->heap_record;
675
675
  iteration_context *context = (iteration_context*) extra;
676
676
 
677
677
  const heap_recorder *recorder = context->heap_recorder;
@@ -687,8 +687,10 @@ static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t val
687
687
  locations[i] = (ddog_prof_Location) {
688
688
  .mapping = {.filename = DDOG_CHARSLICE_C(""), .build_id = DDOG_CHARSLICE_C(""), .build_id_id = {}},
689
689
  .function = {
690
- .name = {.ptr = frame->name, .len = strlen(frame->name)},
691
- .filename = {.ptr = frame->filename, .len = strlen(frame->filename)},
690
+ .name = DDOG_CHARSLICE_C(""),
691
+ .name_id = frame->name,
692
+ .filename = DDOG_CHARSLICE_C(""),
693
+ .filename_id = frame->filename,
692
694
  },
693
695
  .line = frame->line,
694
696
  };
@@ -707,11 +709,12 @@ static int st_object_records_iterate(DDTRACE_UNUSED st_data_t key, st_data_t val
707
709
  }
708
710
 
709
711
  static int st_object_records_debug(DDTRACE_UNUSED st_data_t key, st_data_t value, st_data_t extra) {
710
- VALUE debug_str = (VALUE) extra;
712
+ debug_context *context = (debug_context*) extra;
713
+ VALUE debug_str = context->debug_str;
711
714
 
712
715
  object_record *record = (object_record*) value;
713
716
 
714
- rb_str_catf(debug_str, "%"PRIsVALUE"\n", object_record_inspect(record));
717
+ rb_str_catf(debug_str, "%"PRIsVALUE"\n", object_record_inspect(context->recorder, record));
715
718
 
716
719
  return ST_CONTINUE;
717
720
  }
@@ -740,60 +743,35 @@ static void commit_recording(heap_recorder *heap_recorder, heap_record *heap_rec
740
743
  st_lookup(heap_recorder->object_records, active_recording->obj_id, (st_data_t *) &existing_record);
741
744
  if (existing_record == NULL) rb_raise(rb_eRuntimeError, "Unexpected NULL when reading existing record");
742
745
 
743
- VALUE existing_inspect = object_record_inspect(existing_record);
744
- VALUE new_inspect = object_record_inspect(active_recording);
746
+ VALUE existing_inspect = object_record_inspect(heap_recorder, existing_record);
747
+ VALUE new_inspect = object_record_inspect(heap_recorder, active_recording);
745
748
  rb_raise(rb_eRuntimeError, "Object ids are supposed to be unique. We got 2 allocation recordings with "
746
749
  "the same id. previous={%"PRIsVALUE"} new={%"PRIsVALUE"}", existing_inspect, new_inspect);
747
750
  }
748
751
  }
749
752
 
750
- // Struct holding data required for an update operation on heap_records
751
- typedef struct {
752
- // [in] The locations we did this update with
753
- ddog_prof_Slice_Location locations;
754
- // [out] Pointer that will be updated to the updated heap record to prevent having to do
755
- // another lookup to access the updated heap record.
756
- heap_record **record;
757
- } heap_record_update_data;
758
-
759
- // This function assumes ownership of stack_data is passed on to it so it'll either transfer ownership or clean-up.
760
753
  static int update_heap_record_entry_with_new_allocation(st_data_t *key, st_data_t *value, st_data_t data, int existing) {
761
- heap_record_update_data *update_data = (heap_record_update_data*) data;
754
+ heap_record **new_or_existing_record = (heap_record **) data;
755
+ (*new_or_existing_record) = (heap_record *) (*key);
762
756
 
763
757
  if (!existing) {
764
- // there was no matching heap record so lets create a new one...
765
- // we need to initialize a heap_record_key with a new stack and use that for the key storage. We can't use the
766
- // locations-based key we used for the update call because we don't own its lifecycle. So we create a new
767
- // heap stack and will pass ownership of it to the heap_record.
768
- heap_stack *stack = heap_stack_new(update_data->locations);
769
- (*key) = (st_data_t) heap_record_key_new(stack);
770
- (*value) = (st_data_t) heap_record_new(stack);
758
+ (*value) = (st_data_t) true; // We're only using this hash as a set
771
759
  }
772
760
 
773
- heap_record *record = (heap_record*) (*value);
774
- (*update_data->record) = record;
775
-
776
761
  return ST_CONTINUE;
777
762
  }
778
763
 
779
764
  static heap_record* get_or_create_heap_record(heap_recorder *heap_recorder, ddog_prof_Slice_Location locations) {
780
- // For performance reasons we use a stack-allocated location-slice based key. This allows us
781
- // to do allocation-free lookups and reuse of a matching existing heap record.
782
- // NOTE: If we end up creating a new record, we'll create a heap-allocated key we own and use that for storage
783
- // instead of this one.
784
- heap_record_key lookup_key = (heap_record_key) {
785
- .type = LOCATION_SLICE,
786
- .location_slice = &locations,
787
- };
765
+ // See note on "heap_records" definition for why we keep this map.
766
+ heap_record *stack = heap_record_new(heap_recorder, locations);
788
767
 
789
- heap_record *heap_record = NULL;
790
- heap_record_update_data update_data = (heap_record_update_data) {
791
- .locations = locations,
792
- .record = &heap_record,
793
- };
794
- st_update(heap_recorder->heap_records, (st_data_t) &lookup_key, update_heap_record_entry_with_new_allocation, (st_data_t) &update_data);
768
+ heap_record *new_or_existing_record = NULL; // Will be set inside update_heap_record_entry_with_new_allocation
769
+ bool existing = st_update(heap_recorder->heap_records, (st_data_t) stack, update_heap_record_entry_with_new_allocation, (st_data_t) &new_or_existing_record);
770
+ if (existing) {
771
+ heap_record_free(heap_recorder, stack);
772
+ }
795
773
 
796
- return heap_record;
774
+ return new_or_existing_record;
797
775
  }
798
776
 
799
777
  static void cleanup_heap_record_if_unused(heap_recorder *heap_recorder, heap_record *heap_record) {
@@ -802,18 +780,10 @@ static void cleanup_heap_record_if_unused(heap_recorder *heap_recorder, heap_rec
802
780
  return;
803
781
  }
804
782
 
805
- heap_record_key heap_key = (heap_record_key) {
806
- .type = HEAP_STACK,
807
- .heap_stack = heap_record->stack,
808
- };
809
- // We need to access the deleted key to free it since we gave ownership of the keys to the hash.
810
- // st_delete will change this pointer to point to the removed key if one is found.
811
- heap_record_key *deleted_key = &heap_key;
812
- if (!st_delete(heap_recorder->heap_records, (st_data_t*) &deleted_key, NULL)) {
783
+ if (!st_delete(heap_recorder->heap_records, (st_data_t*) &heap_record, NULL)) {
813
784
  rb_raise(rb_eRuntimeError, "Attempted to cleanup an untracked heap_record");
814
785
  };
815
- heap_record_key_free(deleted_key);
816
- heap_record_free(heap_record);
786
+ heap_record_free(heap_recorder, heap_record);
817
787
  }
818
788
 
819
789
  static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, object_record *record) {
@@ -829,59 +799,44 @@ static void on_committed_object_record_cleanup(heap_recorder *heap_recorder, obj
829
799
  heap_record *heap_record = record->heap_record;
830
800
 
831
801
  if (heap_record == NULL) rb_raise(rb_eRuntimeError, "heap_record was NULL in on_committed_object_record_cleanup");
832
- if (heap_record->stack == NULL) rb_raise(rb_eRuntimeError, "heap_record->stack was NULL in on_committed_object_record_cleanup");
833
802
 
834
803
  heap_record->num_tracked_objects--;
835
804
 
836
805
  // One less object using this heap record, it may have become unused...
837
806
  cleanup_heap_record_if_unused(heap_recorder, heap_record);
838
807
 
839
- object_record_free(record);
840
- }
841
-
842
- // ===============
843
- // Heap Record API
844
- // ===============
845
- heap_record* heap_record_new(heap_stack *stack) {
846
- heap_record *record = ruby_xcalloc(1, sizeof(heap_record));
847
- record->num_tracked_objects = 0;
848
- record->stack = stack;
849
- return record;
850
- }
851
-
852
- void heap_record_free(heap_record *record) {
853
- heap_stack_free(record->stack);
854
- ruby_xfree(record);
808
+ object_record_free(heap_recorder, record);
855
809
  }
856
810
 
857
811
  // =================
858
812
  // Object Record API
859
813
  // =================
860
814
  object_record* object_record_new(long obj_id, heap_record *heap_record, live_object_data object_data) {
861
- object_record *record = ruby_xcalloc(1, sizeof(object_record));
815
+ object_record *record = calloc(1, sizeof(object_record)); // See "note on calloc vs ruby_xcalloc use" above
862
816
  record->obj_id = obj_id;
863
817
  record->heap_record = heap_record;
864
818
  record->object_data = object_data;
865
819
  return record;
866
820
  }
867
821
 
868
- void object_record_free(object_record *record) {
869
- if (record->object_data.class != NULL) {
870
- ruby_xfree(record->object_data.class);
871
- }
872
- ruby_xfree(record);
822
+ void object_record_free(heap_recorder *recorder, object_record *record) {
823
+ unintern_or_raise(recorder, record->object_data.class);
824
+ free(record); // See "note on calloc vs ruby_xcalloc use" above
873
825
  }
874
826
 
875
- VALUE object_record_inspect(object_record *record) {
876
- heap_frame top_frame = record->heap_record->stack->frames[0];
827
+ VALUE object_record_inspect(heap_recorder *recorder, object_record *record) {
828
+ heap_frame top_frame = record->heap_record->frames[0];
829
+ VALUE filename = get_ruby_string_or_raise(recorder, top_frame.filename);
877
830
  live_object_data object_data = record->object_data;
878
- VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%s:%d alloc_gen=%zu gen_age=%zu frozen=%d ",
879
- record->obj_id, object_data.weight, object_data.size, top_frame.filename,
831
+
832
+ VALUE inspect = rb_sprintf("obj_id=%ld weight=%d size=%zu location=%"PRIsVALUE":%d alloc_gen=%zu gen_age=%zu frozen=%d ",
833
+ record->obj_id, object_data.weight, object_data.size, filename,
880
834
  (int) top_frame.line, object_data.alloc_gen, object_data.gen_age, object_data.is_frozen);
881
835
 
882
- const char *class = record->object_data.class;
883
- if (class != NULL) {
884
- rb_str_catf(inspect, "class=%s ", class);
836
+ if (record->object_data.class.value > 0) {
837
+ VALUE class = get_ruby_string_or_raise(recorder, record->object_data.class);
838
+
839
+ rb_str_catf(inspect, "class=%"PRIsVALUE" ", class);
885
840
  }
886
841
  VALUE ref;
887
842
 
@@ -901,202 +856,103 @@ VALUE object_record_inspect(object_record *record) {
901
856
  }
902
857
 
903
858
  // ==============
904
- // Heap Frame API
905
- // ==============
906
- // WARN: Must be kept in-sync with ::char_slice_hash
907
- st_index_t string_hash(char *str, st_index_t seed) {
908
- return st_hash(str, strlen(str), seed);
909
- }
910
-
911
- // WARN: Must be kept in-sync with ::string_hash
912
- st_index_t char_slice_hash(ddog_CharSlice char_slice, st_index_t seed) {
913
- return st_hash(char_slice.ptr, char_slice.len, seed);
914
- }
915
-
916
- // WARN: Must be kept in-sync with ::ddog_location_hash
917
- st_index_t heap_frame_hash(heap_frame *frame, st_index_t seed) {
918
- st_index_t hash = string_hash(frame->name, seed);
919
- hash = string_hash(frame->filename, hash);
920
- hash = st_hash(&frame->line, sizeof(frame->line), hash);
921
- return hash;
922
- }
923
-
924
- // WARN: Must be kept in-sync with ::heap_frame_hash
925
- st_index_t ddog_location_hash(ddog_prof_Location location, st_index_t seed) {
926
- st_index_t hash = char_slice_hash(location.function.name, seed);
927
- hash = char_slice_hash(location.function.filename, hash);
928
- // Convert ddog_prof line type to the same type we use for our heap_frames to
929
- // ensure we have compatible hashes
930
- int32_t line_as_int32 = (int32_t) location.line;
931
- hash = st_hash(&line_as_int32, sizeof(line_as_int32), hash);
932
- return hash;
933
- }
934
-
935
- // ==============
936
- // Heap Stack API
859
+ // Heap Record API
937
860
  // ==============
938
- heap_stack* heap_stack_new(ddog_prof_Slice_Location locations) {
861
+ heap_record* heap_record_new(heap_recorder *recorder, ddog_prof_Slice_Location locations) {
939
862
  uint16_t frames_len = locations.len;
940
863
  if (frames_len > MAX_FRAMES_LIMIT) {
941
864
  // This is not expected as MAX_FRAMES_LIMIT is shared with the stacktrace construction mechanism
942
865
  rb_raise(rb_eRuntimeError, "Found stack with more than %d frames (%d)", MAX_FRAMES_LIMIT, frames_len);
943
866
  }
944
- heap_stack *stack = ruby_xcalloc(1, sizeof(heap_stack) + frames_len * sizeof(heap_frame));
867
+ heap_record *stack = calloc(1, sizeof(heap_record) + frames_len * sizeof(heap_frame)); // See "note on calloc vs ruby_xcalloc use" above
868
+ stack->num_tracked_objects = 0;
945
869
  stack->frames_len = frames_len;
870
+
871
+ // Intern all these strings...
872
+ ddog_CharSlice *strings = recorder->reusable_char_slices;
873
+ // Put all the char slices in the same array; we'll pull them out in the same order from the ids array
946
874
  for (uint16_t i = 0; i < stack->frames_len; i++) {
947
875
  const ddog_prof_Location *location = &locations.ptr[i];
876
+ strings[i] = location->function.filename;
877
+ strings[i + stack->frames_len] = location->function.name;
878
+ }
879
+ intern_all_or_raise(recorder->string_storage, (ddog_prof_Slice_CharSlice) { .ptr = strings, .len = stack->frames_len * 2 }, recorder->reusable_ids, stack->frames_len * 2);
880
+
881
+ // ...and record them for later use
882
+ for (uint16_t i = 0; i < stack->frames_len; i++) {
948
883
  stack->frames[i] = (heap_frame) {
949
- .name = string_from_char_slice(location->function.name),
950
- .filename = string_from_char_slice(location->function.filename),
884
+ .filename = recorder->reusable_ids[i],
885
+ .name = recorder->reusable_ids[i + stack->frames_len],
951
886
  // ddog_prof_Location is a int64_t. We don't expect to have to profile files with more than
952
887
  // 2M lines so this cast should be fairly safe?
953
- .line = (int32_t) location->line,
888
+ .line = (int32_t) locations.ptr[i].line,
954
889
  };
955
890
  }
956
- return stack;
957
- }
958
891
 
959
- void heap_stack_free(heap_stack *stack) {
960
- for (uint64_t i = 0; i < stack->frames_len; i++) {
961
- heap_frame *frame = &stack->frames[i];
962
- ruby_xfree(frame->name);
963
- ruby_xfree(frame->filename);
964
- }
965
- ruby_xfree(stack);
892
+ return stack;
966
893
  }
967
894
 
968
- // WARN: Must be kept in-sync with ::ddog_location_slice_hash
969
- st_index_t heap_stack_hash(heap_stack *stack, st_index_t seed) {
970
- st_index_t hash = seed;
971
- for (uint64_t i = 0; i < stack->frames_len; i++) {
972
- hash = heap_frame_hash(&stack->frames[i], hash);
973
- }
974
- return hash;
975
- }
895
+ void heap_record_free(heap_recorder *recorder, heap_record *stack) {
896
+ ddog_prof_ManagedStringId *ids = recorder->reusable_ids;
976
897
 
977
- // WARN: Must be kept in-sync with ::heap_stack_hash
978
- st_index_t ddog_location_slice_hash(ddog_prof_Slice_Location locations, st_index_t seed) {
979
- st_index_t hash = seed;
980
- for (uint64_t i = 0; i < locations.len; i++) {
981
- hash = ddog_location_hash(locations.ptr[i], hash);
898
+ // Put all the ids in the same array; doesn't really matter the order
899
+ for (u_int16_t i = 0; i < stack->frames_len; i++) {
900
+ ids[i] = stack->frames[i].filename;
901
+ ids[i + stack->frames_len] = stack->frames[i].name;
982
902
  }
983
- return hash;
984
- }
903
+ unintern_all_or_raise(recorder, (ddog_prof_Slice_ManagedStringId) { .ptr = ids, .len = stack->frames_len * 2 });
985
904
 
986
- // ===================
987
- // Heap Record Key API
988
- // ===================
989
- heap_record_key* heap_record_key_new(heap_stack *stack) {
990
- heap_record_key *key = ruby_xmalloc(sizeof(heap_record_key));
991
- key->type = HEAP_STACK;
992
- key->heap_stack = stack;
993
- return key;
905
+ free(stack); // See "note on calloc vs ruby_xcalloc use" above
994
906
  }
995
907
 
996
- void heap_record_key_free(heap_record_key *key) {
997
- ruby_xfree(key);
998
- }
999
-
1000
- static inline size_t heap_record_key_len(heap_record_key *key) {
1001
- if (key->type == HEAP_STACK) {
1002
- return key->heap_stack->frames_len;
1003
- } else {
1004
- return key->location_slice->len;
1005
- }
1006
- }
908
+ // The entire stack is represented by ids (name, filename) and lines (integers) so we can treat is as just
909
+ // a big string of bytes and compare it all in one go.
910
+ int heap_record_cmp_st(st_data_t key1, st_data_t key2) {
911
+ heap_record *stack1 = (heap_record*) key1;
912
+ heap_record *stack2 = (heap_record*) key2;
1007
913
 
1008
- static inline int64_t heap_record_key_entry_line(heap_record_key *key, size_t entry_i) {
1009
- if (key->type == HEAP_STACK) {
1010
- return key->heap_stack->frames[entry_i].line;
914
+ if (stack1->frames_len != stack2->frames_len) {
915
+ return ((int) stack1->frames_len) - ((int) stack2->frames_len);
1011
916
  } else {
1012
- return key->location_slice->ptr[entry_i].line;
917
+ return memcmp(stack1->frames, stack2->frames, stack1->frames_len * sizeof(heap_frame));
1013
918
  }
1014
919
  }
1015
920
 
1016
- static inline size_t heap_record_key_entry_name(heap_record_key *key, size_t entry_i, const char **name_ptr) {
1017
- if (key->type == HEAP_STACK) {
1018
- char *name = key->heap_stack->frames[entry_i].name;
1019
- (*name_ptr) = name;
1020
- return strlen(name);
1021
- } else {
1022
- ddog_CharSlice name = key->location_slice->ptr[entry_i].function.name;
1023
- (*name_ptr) = name.ptr;
1024
- return name.len;
1025
- }
1026
- }
921
+ // Initial seed for hash function, same as Ruby uses
922
+ #define FNV1_32A_INIT 0x811c9dc5
1027
923
 
1028
- static inline size_t heap_record_key_entry_filename(heap_record_key *key, size_t entry_i, const char **filename_ptr) {
1029
- if (key->type == HEAP_STACK) {
1030
- char *filename = key->heap_stack->frames[entry_i].filename;
1031
- (*filename_ptr) = filename;
1032
- return strlen(filename);
1033
- } else {
1034
- ddog_CharSlice filename = key->location_slice->ptr[entry_i].function.filename;
1035
- (*filename_ptr) = filename.ptr;
1036
- return filename.len;
1037
- }
924
+ // The entire stack is represented by ids (name, filename) and lines (integers) so we can treat is as just
925
+ // a big string of bytes and hash it all in one go.
926
+ st_index_t heap_record_hash_st(st_data_t key) {
927
+ heap_record *stack = (heap_record*) key;
928
+ return st_hash(stack->frames, stack->frames_len * sizeof(heap_frame), FNV1_32A_INIT);
1038
929
  }
1039
930
 
1040
- int heap_record_key_cmp_st(st_data_t key1, st_data_t key2) {
1041
- heap_record_key *key_record1 = (heap_record_key*) key1;
1042
- heap_record_key *key_record2 = (heap_record_key*) key2;
931
+ static void unintern_or_raise(heap_recorder *recorder, ddog_prof_ManagedStringId id) {
932
+ if (id.value == 0) return; // Empty string, nothing to do
1043
933
 
1044
- // Fast path, check if lengths differ
1045
- size_t key_record1_len = heap_record_key_len(key_record1);
1046
- size_t key_record2_len = heap_record_key_len(key_record2);
1047
-
1048
- if (key_record1_len != key_record2_len) {
1049
- return ((int) key_record1_len) - ((int) key_record2_len);
934
+ ddog_prof_MaybeError result = ddog_prof_ManagedStringStorage_unintern(recorder->string_storage, id);
935
+ if (result.tag == DDOG_PROF_OPTION_ERROR_SOME_ERROR) {
936
+ rb_raise(rb_eRuntimeError, "Failed to unintern id: %"PRIsVALUE, get_error_details_and_drop(&result.some));
1050
937
  }
938
+ }
1051
939
 
1052
- // If we got this far, we have same lengths so need to check item-by-item
1053
- for (size_t i = 0; i < key_record1_len; i++) {
1054
- // Lines are faster to compare, lets do that first
1055
- size_t line1 = heap_record_key_entry_line(key_record1, i);
1056
- size_t line2 = heap_record_key_entry_line(key_record2, i);
1057
- if (line1 != line2) {
1058
- return ((int) line1) - ((int)line2);
1059
- }
1060
-
1061
- // Then come names, they are usually smaller than filenames
1062
- const char *name1, *name2;
1063
- size_t name1_len = heap_record_key_entry_name(key_record1, i, &name1);
1064
- size_t name2_len = heap_record_key_entry_name(key_record2, i, &name2);
1065
- if (name1_len != name2_len) {
1066
- return ((int) name1_len) - ((int) name2_len);
1067
- }
1068
- int name_cmp_result = strncmp(name1, name2, name1_len);
1069
- if (name_cmp_result != 0) {
1070
- return name_cmp_result;
1071
- }
1072
-
1073
- // Then come filenames
1074
- const char *filename1, *filename2;
1075
- int64_t filename1_len = heap_record_key_entry_filename(key_record1, i, &filename1);
1076
- int64_t filename2_len = heap_record_key_entry_filename(key_record2, i, &filename2);
1077
- if (filename1_len != filename2_len) {
1078
- return ((int) filename1_len) - ((int) filename2_len);
1079
- }
1080
- int filename_cmp_result = strncmp(filename1, filename2, filename1_len);
1081
- if (filename_cmp_result != 0) {
1082
- return filename_cmp_result;
1083
- }
940
+ static void unintern_all_or_raise(heap_recorder *recorder, ddog_prof_Slice_ManagedStringId ids) {
941
+ ddog_prof_MaybeError result = ddog_prof_ManagedStringStorage_unintern_all(recorder->string_storage, ids);
942
+ if (result.tag == DDOG_PROF_OPTION_ERROR_SOME_ERROR) {
943
+ rb_raise(rb_eRuntimeError, "Failed to unintern_all: %"PRIsVALUE, get_error_details_and_drop(&result.some));
1084
944
  }
1085
-
1086
- // If we survived the above for, then everything matched
1087
- return 0;
1088
945
  }
1089
946
 
1090
- // Initial seed for hash functions
1091
- #define FNV1_32A_INIT 0x811c9dc5
1092
-
1093
- st_index_t heap_record_key_hash_st(st_data_t key) {
1094
- heap_record_key *record_key = (heap_record_key*) key;
1095
- if (record_key->type == HEAP_STACK) {
1096
- return heap_stack_hash(record_key->heap_stack, FNV1_32A_INIT);
1097
- } else {
1098
- return ddog_location_slice_hash(*record_key->location_slice, FNV1_32A_INIT);
947
+ static VALUE get_ruby_string_or_raise(heap_recorder *recorder, ddog_prof_ManagedStringId id) {
948
+ ddog_StringWrapperResult get_string_result = ddog_prof_ManagedStringStorage_get_string(recorder->string_storage, id);
949
+ if (get_string_result.tag == DDOG_STRING_WRAPPER_RESULT_ERR) {
950
+ rb_raise(rb_eRuntimeError, "Failed to get string: %"PRIsVALUE, get_error_details_and_drop(&get_string_result.err));
1099
951
  }
952
+ VALUE ruby_string = ruby_string_from_vec_u8(get_string_result.ok.message);
953
+ ddog_StringWrapper_drop((ddog_StringWrapper *) &get_string_result.ok);
954
+
955
+ return ruby_string;
1100
956
  }
1101
957
 
1102
958
  static inline double ewma_stat(double previous, double current) {
@@ -1120,3 +976,23 @@ void heap_recorder_testonly_reset_last_update(heap_recorder *heap_recorder) {
1120
976
 
1121
977
  heap_recorder->last_update_ns = 0;
1122
978
  }
979
+
980
+ void heap_recorder_testonly_benchmark_intern(heap_recorder *heap_recorder, ddog_CharSlice string, int times, bool use_all) {
981
+ if (heap_recorder == NULL) rb_raise(rb_eArgError, "heap profiling must be enabled");
982
+ if (times > REUSABLE_FRAME_DETAILS_SIZE) rb_raise(rb_eArgError, "times cannot be > than REUSABLE_FRAME_DETAILS_SIZE");
983
+
984
+ if (use_all) {
985
+ ddog_CharSlice *strings = heap_recorder->reusable_char_slices;
986
+
987
+ for (int i = 0; i < times; i++) strings[i] = string;
988
+
989
+ intern_all_or_raise(
990
+ heap_recorder->string_storage,
991
+ (ddog_prof_Slice_CharSlice) { .ptr = strings, .len = times },
992
+ heap_recorder->reusable_ids,
993
+ times
994
+ );
995
+ } else {
996
+ for (int i = 0; i < times; i++) intern_or_raise(heap_recorder->string_storage, string);
997
+ }
998
+ }