| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Sorry, something went wrong.
There was a problem hiding this comment.
Sorry, something went wrong.
CI Test ResultsRun: #37651377726 | Commit: a2d6179 | Duration: 17m 37s (longest job)
Status Overview
Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled Summary: Total: 32 | Passed: 32 | Failed: 0 Updated: 2026-10-07 16:43:58 UTC |
Sorry, something went wrong.
Sorry, something went wrong.
CallTraceStorage reclaims table memory in clearTableOnly() and its destructor after a RefCountGuard drain, and in release builds proceeded to free the memory even when that drain timed out. In production this cannot happen: every put() runs under one of the Profiler stripe locks and every processTraces()/clear() caller holds lockAll(), which waits for in-flight puts without a time limit. The class comments claimed the opposite, that processTraces() is safe to run concurrently with put(). Document the real contract on CallTraceStorage and treat the drain as defense in depth. waitForRefCountToClear() now reports whether it drained; on a timeout clearTableOnly() leaks the detached chunks and the destructor leaks the table rather than freeing memory a put() may still be writing. Debug builds keep aborting on a timeout; gtest builds (UNIT_TEST) skip the abort so they can exercise the release behavior. Correct the clearTableOnly() and ProcessCallTracesRaceTest comments that described collect() racing an in-flight put(): with lockAll() held no put() can be in flight, so a global wait could only time out on guards of other resources such as StringDictionary lookups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous wording said processTraces(), clear() and the destructor must never run concurrently with put(). That overstates it: processTraces() and the destructor only reclaim tables that are already swapped out of _active_storage, which put() re-checks after taking its guard, so they tolerate concurrent put() by design. Only clear(), which resets the active table in place, needs put() excluded. Say so, and say that today lockAll() excludes put() from all of them anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ddprof-lib/src/main/cpp/callTraceStorage.cpp:82 [Sphinx Review — MEDIUM] The destructor CallTraceStorage::~CallTraceStorage() was modified to conditionally delete standby only when waitForRefCountToClear(standby) returns true. The new test ClearWithGuardHeldLeaksChunks tests CallTraceHashTable::clearTableOnly() but never instantiates or destroys a CallTraceStorage object. A mutation flipping the condition (if (!RefCountGuard::waitForRefCountToClear(standby))) or removing the delete statement would not be caught by any test in this diff. Suggestion: Add a test that instantiates CallTraceStorage, holds a RefCountGuard to force a drain timeout, then destroys the CallTraceStorage to verify the conditional-delete logic is correct. This would catch mutations of the condition or deletion of the delete statement. This is more important now when we will need to be able to turn on/off profiler while the process is still alive |
Sorry, something went wrong.
|
ddprof-lib/src/main/cpp/callTraceStorage.cpp:85 [Sphinx Review — MEDIUM] The destructor CallTraceStorage::~CallTraceStorage() was modified to conditionally delete scratch only when waitForRefCountToClear(scratch) returns true. The new test ClearWithGuardHeldLeaksChunks tests CallTraceHashTable::clearTableOnly() but never instantiates or destroys a CallTraceStorage object. A mutation flipping the condition or removing the delete statement would not be caught by any test in this diff. Suggestion: Add a test that instantiates CallTraceStorage, holds a RefCountGuard to force a drain timeout, then destroys the CallTraceStorage to verify the conditional-delete logic is correct. This would catch mutations of the condition or deletion of the delete statement. |
Sorry, something went wrong.
|
ddprof-lib/src/main/cpp/callTraceStorage.cpp:229 [Sphinx Review — LOW] waitForRefCountToClear() now returns a safety-relevant bool, and the header says that on false the resource must not be freed. processTraces() is the only production caller in this file that still ignores the result. That is memory-safe today, because the later original_active->clear() drains again and leaks on timeout. But when the contract is broken, the ignored result has costs. (1) The thread holding lockAll() waits a second ~500ms on the same straggler. (2) DICTIONARY_DRAIN_TIMEOUTS is incremented twice and the warning is logged twice for one stall. (3) collect() runs over a table the code already knows a put() may still be writing. The PR description mentions the double wait and log, but the code keeps no record of the first result. So the 'must not be freed' rule for this table depends on clear() draining again, and nothing ties that drain to the decision already made at line 229. Suggestion: Keep the result (const bool drained = ...) and use it. Either pass it to a clear variant that skips the redundant drain and leaks directly, or at least skip the second counter increment and log. If the current behavior is intentional, add a comment here saying the result is deliberately ignored because clear() -> clearTableOnly() re-drains and leaks on timeout. Without it, the ignored value reads like an oversight next to the destructor's checked calls. |
Sorry, something went wrong.
|
ddprof-lib/src/main/cpp/callTraceHashTable.cpp:136 [Sphinx Review — LOW] The decrementCounters() comment (lines 136-137) says the _prev traversal is safe because waitForRefCountToClear(this) in clearTableOnly() "has already drained any in-flight put() operations". After this PR that premise only holds when the drain succeeds. clearTableOnly() now keeps going on a timeout: const bool drained = RefCountGuard::waitForRefCountToClear(this); decrementCounters();. It only checks drained at the very end, to decide whether to leak the chunks. So on the timeout path that the PR introduces, decrementCounters() walks _current_table/_prev while a put() may still hold a guard on this table and may be changing it, for example bumping sizes or expanding. Leaking the chunks keeps this memory-safe, but the comment now states an unconditional guarantee the code no longer gives. The counter decrement can also drift: a stalled put() can add to the trace counters after decrementCounters() has already subtracted them. The new clearTableOnly() comment documents the timeout path, but… (truncated) Suggestion: Make the comment conditional. The traversal is race-free only when the drain succeeds. On a timeout it can race a stalled put(). It stays memory-safe because the chunks are leaked rather than freed, but counters may be slightly off. Alternatively, skip decrementCounters() when !drained, which would make the existing comment true again. Pick one and keep the comment consistent with it. |
Sorry, something went wrong.
|
ddprof-lib/src/main/cpp/callTraceStorage.h:36 [Sphinx Review — LOW] The new class comment (and the matching bullet in doc/architecture/CallTraceStorage.md) says the destructor tolerates concurrent put(). That holds for processTraces(), but not for the destructor. put() loads _active_storage (callTraceStorage.cpp:120) before it takes the RefCountGuard on the table, then re-checks _active_storage. Suppose a put() is preempted between that load and the guard constructor. The destructor can then swap out the active table, find no guard on it, delete it and return, freeing the CallTraceStorage object. The resumed put() then constructs a guard on a deleted table and re-reads _active_storage from freed storage. The table-level drain cannot protect the containing object. The code is not reachable today (the PR notes Profiler::_instance is never deleted), but the new contract text overstates the guarantee, and a future caller could rely on it. Suggestion: Limit the claim to processTraces(). State that the destructor requires put() to be quiesced (no thread can still be inside put() on this storage), and that its drain plus the leak on timeout are defense-in-depth for stragglers that already hold a guard. Apply the same wording to the CallTraceStorage.md bullet. |
Sorry, something went wrong.
|
ddprof-lib/src/main/cpp/refCountGuard.h:122 [Sphinx Review — MEDIUM] The function now returns a value that matters for safety. The doc comment says that on false, ptr_to_delete must not be freed, but nothing in the type system enforces that. The PR itself names two existing callers that drop the result: the post-swap drain in CallTraceStorage::processTraces() and StringDictionary::rotate(). A future caller written in the old 'drain, then free' style compiles cleanly and brings back the use-after-free this PR is meant to prevent. The contract lives only in a comment. Suggestion: Declare it [[nodiscard]] static bool waitForRefCountToClear(void* ptr_to_delete);. At the call sites where dropping the result is intentional (processTraces post-swap drain, StringDictionary::rotate), cast to (void) and add a one-line reason, so each discard is explicit and can be reviewed. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Stacked on #839. Review only the top commit; the base is fix/prof-16136-dictionary-drain-timeout.
What does this PR do?:
Documents how CallTraceStorage reclaims tables relative to put(), and makes it never free a table that a put() may still be writing after a timed-out drain. No memory is leaked in normal operation: the leak path below is only taken after a drain timeout, which no production caller can hit today.
Motivation:
PROF-16136 asked for an audit of the other drain callers. CallTraceStorage uses the same "drain, then free regardless" pattern as the dictionary, but there it is latent, not reachable:
This was already true when #585 (PROF-14889) landed. So the 500 ms timeouts it observed must have come from the old global wait counting guards on other resources, i.e. StringDictionary lookups, which lockAll() does not exclude. #839 removes that global wait.
Additional Notes:
How to test the change?:
New gtest CallTraceHashTableDrainTest.ClearWithGuardHeldLeaksChunks. It holds a RefCountGuard on a table while calling clear(), then reads a trace from the old chunk.
Also run:
For Datadog employees:
credentials of any kind, I've requested a security review (run the dd:platform-security-review
skill, or file a request via the PSEC review form).
bewaire also runs automatically on every PR.
🤖 Generated with Claude Code