Store per_thread_context in each Ruby Thread#5816
Conversation
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 91ccef0 | Docs | Datadog PR Page | Give us feedback! |
a9f446a to
d430c5d
Compare
BenchmarksBenchmark execution time: 2026-06-04 12:12:20 Comparing candidate commit 91ccef0 in PR branch Found 0 performance improvements and 1 performance regressions! Performance is the same for 44 metrics, 1 unstable metrics.
|
|
Mmh, 3.2 doesn't seem to like rb_thread_current() being called from on_gvl_event(): |
d0236e0 to
913bb24
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 913bb24b83
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
913bb24 to
258c15c
Compare
Typing analysisThis PR does not change typing compared to the base branch. |
|
Another idea design-wise given that both ThreadContext and CpuAndWallTimeWorker are de-facto global would be to have a single instance of them, always and be able to reinitialize/reconfigure them. |
ivoanjo
left a comment
There was a problem hiding this comment.
I haven't yet finished doing a full pass but need to switch gears! Here's what I got so far...
fd5ebfd to
27864bf
Compare
77e8026 to
5e775e8
Compare
ivoanjo
left a comment
There was a problem hiding this comment.
👍 Really nice! Thanks for addressing all my notes and nitpicks 🙏
…=5 bundle exec rspec ...` * Very useful to repro transient tests. Re-running the full rspec command multiple times is much slower and less likely to find the transient.
5e775e8 to
98cd669
Compare
Nice, and I can repro locally, 3.417k -> 23.331k. |
d8490cd to
f360872
Compare
|
This seems a pre-existing transient but I fixed it anyway. If the thread was already dead when we sample then we won't init any state for it, AFAIK even before this PR. |
* Store the per_thread_context in the Ruby Thread TLS (rb_internal_thread_specific_set()), so all the state is in one struct and not a struct + a single field in the TLS. * The TLS can be read safely in async-signal safe functions and functions without the GVL. * The per_thread_context for a given Thread lives for the whole Thread lifetime, that way there is no possible mistmatch with a new instance of Datadog::Profiling::Collectors::ThreadContext. * We'll need more place in TLS for #5777 (comment) but there is a hard limit on the number of TLS vars, so this avoids that problem. * Much simpler and more consistent, the state is together vs spread in different places. * Remove hash_map_per_thread_context and its dead theads cleanups, they are now redundant. * Use a TypedData object stored in a hidden instance variable on the Thread for marking the sampling_buffer. Skip profiling frozen threads to avoid FrozenError when setting the instance variable. * Move locations out of sampling_buffer into collector state, since anyway its lifetime was tied to the collector and not the buffers. * Do not call get_or_create_context_for() in thread_context_collector_sample_allocation(), because get_or_create_context_for() allocates. * Clear stale per_thread_context between tests to fix transient test failures and make testing easier. The TLS-based per_thread_context persists across collector instances, so stale gc_tracking state from a previous test caused all cpu-time samples to read as 0. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…s and fix specs * In a real fork() we should only clean the current thread, so do that and fix the specs. * Also remove duplication.
…ions size Since per_thread_context persists in Thread TLS across collector instances, a new collector with a smaller max_frames would write past its locations array using the old buffer's max_frames. Fix: pass sample_locations (ptr + len) to sample_thread, cap captured_frames to locations.len, and reinitialize the buffer afterward to reconcile sizes. Co-Authored-By: Claude Opus 4.6 <[email protected]>
The _native_new_empty_thread helper's thread body returned immediately, so the thread could die before sample ran. Since per_thread_context is now only built for live threads, fetch raised KeyError when the thread was already dead. Co-Authored-By: Claude Opus 4.6 <[email protected]>
4e5d5df to
91ccef0
Compare
What does this PR do?
Store the per_thread_context in the Ruby Thread TLS (rb_internal_thread_specific_set()) so all the state is in one struct and not a struct + a single field in the TLS. Also we'll need more place in TLS for #5777 (comment) but there is a hard limit on TLS, so this avoids that problem.
Motivation:
Much simpler and more consistent, the state is together vs spread in different places.
Change log entry
Yes. Remove overhead related to cleaning up profiler state for dead threads.
Additional Notes:
How to test the change?