Route DebuggerController operations through a per-controller worker queue#1087
Closed
xusheng6 wants to merge 3 commits into
Closed
Route DebuggerController operations through a per-controller worker queue#1087xusheng6 wants to merge 3 commits into
xusheng6 wants to merge 3 commits into
Conversation
The 17 `Launch`/`Attach`/`Connect`/`Go`/`Step*`/`RunTo*`/`Restart`/`Detach`/ `Quit`/`Pause` methods each spawn a detached `std::thread` that runs the corresponding `*AndWait` work, capturing `this` by reference. Nothing keeps the controller alive for the duration of the detached call, so if the controller's refcount drops to zero before the worker exits (e.g. the user closes the tab), the worker dereferences a dangling pointer. Capture a `DbgRef<DebuggerController>` by value into each lambda so the controller stays alive until the worker thread finishes. Fixes #1083. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er queue
Replaces the previous "spawn one std::thread per operation and detach it"
pattern with a single persistent worker thread per controller plus a FIFO
work queue. Every public Xxx()/XxxAndWait() method now goes through Submit().
The worker thread is owned by the controller, started in the constructor
and joined in the destructor (after draining), so any task can no longer
outlive the controller it touches -- the lifetime guarantee is structural
rather than reliant on per-call DbgRef captures (which this PR removes).
Highlights:
* Submit() detects re-entrant calls from the worker itself and runs them
inline, so e.g. RestartAndWaitOnWorker can call QuitAndWaitOnWorker and
LaunchAndWaitOnWorker without deadlocking on the queue.
* SubmitAndWait() submits a task and waits on the resulting future. Every
XxxAndWait now takes an optional std::chrono::milliseconds timeout,
defaulting to milliseconds::max() ("wait forever") so existing callers
in ffi.cpp and uinotification.cpp keep their current behavior. If a
non-infinite timeout elapses, the engine is signaled to break and we
still wait for the in-flight op to settle before returning.
* Pause()/PauseAndWait() are special-cased: they bypass the queue and call
m_adapter->BreakInto() directly on the caller's thread. The worker is
blocked inside ExecuteAdapterAndWait for whatever resume op is in
flight; BreakInto unsticks it. PauseAndWait then waits for the worker
to drain past the running task by submitting a no-op probe.
The existing public/internal split (XxxAndWaitInternal does the actual
work; the old XxxAndWait was the lock-Internal-notify wrapper) is
preserved. The old wrapper is renamed to XxxAndWaitOnWorker (private) and
the new public XxxAndWait(timeout) is a thin SubmitAndWait wrapper around
it. This keeps the public API surface identical to today for any caller
that doesn't pass a timeout.
Out of scope for this PR: adapter-internal threads (DbgEngAdapter::Attach
spawned thread, EngineLoop, LLDB EventListener, RSP send/receive loops).
Those have their own lifetime concerns and will be handled separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 20, 2026
…stop machinery Adapter stops are now an internal signal from the adapter thread to the worker, not events on the public dispatcher queue. PostDebuggerEvent intercepts AdapterStoppedEventType and delivers the stop reason to a new mutex+condvar pair (m_adapterStopMutex / m_adapterStopCv / m_adapterStopPending). The worker consumes them via two paths: 1. In-flight ops: ExecuteAdapterAndWait now claims the channel for its entire duration (m_inAdapterWait = true), kicks off the adapter operation, then loops on WaitForAdapterStop. The conditional- breakpoint check moves here -- on a false condition we silently resume via m_adapter->Go() and wait again, without releasing the channel. This eliminates the old "drive m_adapter->Go() from the dispatcher thread" hack and the m_suppressResumeEvent flag that papered over it. 2. Spontaneous stops: when the user types e.g. `si` directly into the LLDB REPL while no controller op is in flight, m_inAdapterWait is false, so PostDebuggerEvent queues HandleSpontaneousAdapterStop on the worker -- which updates caches and calls NotifyStopped, the same effect as the dispatcher synthesis the old code did at debuggercontroller.cpp:2279. Target-exit / detach events continue through the dispatcher queue (they're user-visible) but ALSO signal the adapter-stop channel so an in-flight WaitForAdapterStop unblocks when the target dies. The dispatcher (DebuggerMainThread) no longer has any AdapterStoppedEventType-specific code paths. m_lastAdapterStopEventConsumed and m_suppressResumeEvent are removed; the temporary RegisterEventCallback/Semaphore pattern inside ExecuteAdapterAndWait is replaced by the direct WaitForAdapterStop wait. AdapterStoppedEventType remains in the public enum so adapters keep posting it the same way -- the change is purely in how the controller routes it once it arrives at PostDebuggerEvent. Adapter implementations need no changes. Stacked on the worker-queue refactor (this PR / #1087). Implements #1089. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the previous "spawn one
std::threadper operation and detach it" pattern (17 sites acrossLaunch,Attach,Connect,Go,GoReverse,Step{Into,Over,Return}{,Reverse},RunTo{,Reverse},Restart,Detach,Quit,Pause) with a single persistent worker thread perDebuggerControllerplus a FIFO work queue. Every publicXxx()/XxxAndWait()method now routes throughSubmit().The worker thread is owned by the controller, started in the constructor and joined in the destructor (after draining the queue). Async work can no longer outlive the controller it touches — the lifetime guarantee is structural rather than reliant on per-call
DbgRefcaptures, which this PR removes (those were added in #1086 as a stopgap; the queue model makes them unnecessary).Builds on #1086.
Design notes
Submit (templated, header)
std::this_thread::get_id() == m_workerThreadId) and runs them inline. This is what letsRestartAndWaitOnWorkercallQuitAndWaitOnWorker+LaunchAndWaitOnWorkerwithout deadlocking on its own queue.std::futureso sync callers can wait.SubmitAndWait (templated, header)
std::chrono::milliseconds timeout.milliseconds::max()("wait forever") is the default and bypasseswait_forentirely (avoids overflow inside the stdlib).m_adapter->BreakInto()and still waits for the in-flight op to settle before returning.Public/Internal/OnWorker split
XxxAndWaitInternaldoes the actual work; the oldXxxAndWaitwas the lock-Internal-notify wrapper) is preserved.XxxAndWaitis renamedXxxAndWaitOnWorker(private) — same body, now invoked from the worker thread.XxxAndWait(timeout)is a one-lineSubmitAndWaitwrapper.Public API compatibility
XxxAndWaitkeeps its current signature with an appended optionaltimeoutparameter defaulted to infinite. Existing callers incore/ffi.cppandui/uinotification.cppkeep their current behavior.Pause / BreakInto (special-cased)
Pause()andPauseAndWait()bypass the queue and callm_adapter->BreakInto()directly on the caller's thread. The worker is presumed blocked insideExecuteAdapterAndWaitfor whatever resume op is in flight;BreakIntounsticks it.PauseAndWaitthen waits for the worker to drain past the in-flight task by submitting a no-op probe.IL stepping fits without changes
StepIntoIL/StepOverILwhich callXxxAndWaitInternalmultiple times. The loop runs as a single queued task on the worker — no change to event-filtering semantics or stop-reason logic.Out of scope (follow-ups)
DbgEngAdapter::Attach's spawned thread (BINARYNINJA-1A),DbgEngAdapter::EngineLoop, LLDBEventListener, RSP send/receive loops. These need adapter destructors to act as join barriers; that's a separate concern from the controller's queue.m_targetControlMutexcleanup. With single-worker serialization it's largely redundant, but I left it in place to minimize behavioral risk in this PR.BinaryDataNotificationand settings-callback unregistration inDestroy(BINARYNINJA-75 / -2H — theRebaseToAddressfamily of crashes).m_userRequestedBreakshould arguably bestd::atomic<bool>now thatPausewrites it from outside the worker. Left as-is for this PR.Test plan
ninja— bothlibdebuggercore.dylibandlibdebuggerui.dylib)test/debugger_test.py🤖 Generated with Claude Code