Skip to content

Commit 2014d66

Browse files
authored
Merge pull request #1596 from microsoft/dev/waan/2987836
Fix UI-thread hang when resuming the target during launch
1 parent 8ee6a04 commit 2014d66

2 files changed

Lines changed: 228 additions & 50 deletions

File tree

src/MIDebugEngine/AD7.Impl/AD7Engine.cs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -319,28 +319,24 @@ public int ContinueFromSynchronousEvent(IDebugEvent2 eventObject)
319319
{
320320
if (eventObject is AD7ProgramCreateEvent)
321321
{
322-
Exception exception = null;
323-
324322
try
325323
{
326324
_engineCallback.OnLoadComplete();
327-
// At this point breakpoints and exception settings have been sent down, so we can resume the target
328-
_pollThread.RunOperation(() =>
329-
{
330-
return _debuggedProcess.ResumeFromLaunch();
331-
});
325+
326+
// Resume the target on the worker thread without blocking the UI thread this runs on.
327+
// Resume faults are reported via onError, since the SDM drops errors returned from here.
328+
_pollThread.PostAsyncOperation(
329+
() => _debuggedProcess.ResumeFromLaunch(),
330+
(exception) =>
331+
{
332+
SendStartDebuggingError(exception);
333+
_debuggedProcess.Terminate();
334+
});
332335
}
333336
catch (Exception e)
334337
{
335-
exception = e;
336-
// Return from the catch block so that we can let the exception unwind - the stack can get kind of big
337-
}
338-
339-
if (exception != null)
340-
{
341-
// If something goes wrong, report the error and then stop debugging. The SDM will drop errors
342-
// from ContinueFromSynchronousEvent, so we want to deal with them ourself.
343-
SendStartDebuggingError(exception);
338+
// Report synchronous failures ourselves, since the SDM drops errors returned from here.
339+
SendStartDebuggingError(e);
344340
_debuggedProcess.Terminate();
345341
}
346342

src/MIDebugEngine/Engine.Impl/OperationThread.cs

Lines changed: 216 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal class WorkerThread : IDisposable
2828
private readonly ManualResetEvent _runningOpCompleteEvent; // fired when either m_syncOp finishes, or the kick off of m_async
2929
private readonly Object _eventLock = new object(); // Locking on an event directly can cause Mono to stop responding.
3030
private readonly Queue<Operation> _postedOperations; // queue of fire-and-forget operations
31+
private readonly Queue<(AsyncOperation Operation, Action<Exception> OnError)> _postedAsyncOperations; // queue of fire-and-forget async operations
3132

3233
public event EventHandler<Exception> PostedOperationErrorEvent;
3334

@@ -37,6 +38,13 @@ private class OperationDescriptor
3738
/// Delegate that was added via 'RunOperation'. Is of type 'Operation' or 'AsyncOperation'
3839
/// </summary>
3940
public readonly Delegate Target;
41+
42+
/// <summary>
43+
/// Handler invoked if the operation faults. For async operations, this may be invoked on a non-worker thread.
44+
/// Only set for PostAsyncOperation.
45+
/// </summary>
46+
public Action<Exception> ErrorHandler;
47+
4048
public ExceptionDispatchInfo ExceptionDispatchInfo;
4149
public Task Task;
4250
private bool _isStarted;
@@ -81,6 +89,7 @@ public WorkerThread(Logger logger)
8189
_opSet = new AutoResetEvent(false);
8290
_runningOpCompleteEvent = new ManualResetEvent(true);
8391
_postedOperations = new Queue<Operation>();
92+
_postedAsyncOperations = new Queue<(AsyncOperation Operation, Action<Exception> OnError)>();
8493

8594
_thread = new Thread(new ThreadStart(ThreadFunc));
8695
_thread.Name = "MIDebugger.PollThread";
@@ -121,6 +130,30 @@ public void RunOperation(string text, CancellationTokenSource canTokenSource, As
121130
SetOperationInternalWithProgress(op, text, canTokenSource);
122131
}
123132

133+
/// <summary>
134+
/// Queue an async operation to run on the worker thread and return immediately, without waiting for the
135+
/// operation to start or finish. Posted async operations run one at a time, in the order posted, and
136+
/// serialize behind any in-flight operation. Faults are reported to <paramref name="onError"/>.
137+
/// </summary>
138+
public void PostAsyncOperation(AsyncOperation op, Action<Exception> onError)
139+
{
140+
if (op == null)
141+
throw new ArgumentNullException(nameof(op));
142+
if (onError == null)
143+
throw new ArgumentNullException(nameof(onError));
144+
145+
if (_isClosed)
146+
throw new ObjectDisposedException("WorkerThread");
147+
148+
lock (_postedAsyncOperations)
149+
{
150+
if (_isClosed)
151+
throw new ObjectDisposedException("WorkerThread");
152+
153+
_postedAsyncOperations.Enqueue((op, onError));
154+
_opSet.Set();
155+
}
156+
}
124157

125158
public void Close()
126159
{
@@ -150,6 +183,22 @@ public void Close()
150183
}
151184
}
152185
}
186+
187+
// Fail any queued async operations that will never run now that we are closed, instead of
188+
// dropping them silently. The poll thread won't promote them once _isClosed is set.
189+
while (true)
190+
{
191+
Action<Exception> onError;
192+
lock (_postedAsyncOperations)
193+
{
194+
if (_postedAsyncOperations.Count == 0)
195+
break;
196+
197+
onError = _postedAsyncOperations.Dequeue().OnError;
198+
}
199+
200+
InvokeErrorHandler(onError, new ObjectDisposedException("WorkerThread"));
201+
}
153202
}
154203

155204
internal void SetOperationInternal(Delegate op)
@@ -189,6 +238,7 @@ internal void SetOperationInternalWithProgress(AsyncProgressOperation op, string
189238
}
190239
}
191240
}
241+
192242
public void PostOperation(Operation op)
193243
{
194244
if (op == null)
@@ -212,71 +262,154 @@ public void PostOperation(Operation op)
212262

213263
private bool TrySetOperationInternal(Delegate op)
214264
{
215-
lock (_eventLock)
265+
bool claimed = false;
266+
try
216267
{
217-
if (_isClosed)
218-
throw new ObjectDisposedException("WorkerThread");
219-
220-
if (_runningOp == null)
268+
lock (_eventLock)
221269
{
222-
_runningOpCompleteEvent.Reset();
270+
if (_isClosed)
271+
throw new ObjectDisposedException("WorkerThread");
223272

224-
OperationDescriptor runningOp = new OperationDescriptor(op);
225-
_runningOp = runningOp;
273+
if (_runningOp == null)
274+
{
275+
_runningOpCompleteEvent.Reset();
226276

227-
_opSet.Set();
277+
OperationDescriptor runningOp = new OperationDescriptor(op);
278+
_runningOp = runningOp;
228279

229-
_runningOpCompleteEvent.WaitOne();
280+
_opSet.Set();
230281

231-
Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?");
282+
_runningOpCompleteEvent.WaitOne();
283+
claimed = true;
232284

233-
if (runningOp.ExceptionDispatchInfo != null)
234-
{
235-
runningOp.ExceptionDispatchInfo.Throw();
285+
Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?");
286+
287+
if (runningOp.ExceptionDispatchInfo != null)
288+
{
289+
runningOp.ExceptionDispatchInfo.Throw();
290+
}
291+
292+
return true;
236293
}
294+
}
237295

238-
return true;
296+
return false;
297+
}
298+
finally
299+
{
300+
// The running-op slot was just freed and _eventLock is now released. If promotion on the poll
301+
// thread lost the TryEnter race against this method, re-arm _opSet so a queued async operation
302+
// is promoted immediately instead of stranded until the next unrelated wakeup.
303+
if (claimed && HasPostedAsyncOperation())
304+
{
305+
_opSet.Set();
239306
}
240307
}
241-
242-
return false;
243308
}
244309

245310
private bool TrySetOperationInternalWithProgress(AsyncProgressOperation op, string text, CancellationTokenSource canTokenSource)
246311
{
247312
var waitLoop = new HostWaitLoop(text);
248313

249-
lock (_eventLock)
314+
bool claimed = false;
315+
try
250316
{
251-
if (_isClosed)
252-
throw new ObjectDisposedException("WorkerThread");
253-
254-
if (_runningOp == null)
317+
lock (_eventLock)
255318
{
256-
_runningOpCompleteEvent.Reset();
319+
if (_isClosed)
320+
throw new ObjectDisposedException("WorkerThread");
257321

258-
OperationDescriptor runningOp = new OperationDescriptor(new AsyncOperation(() => { return op(waitLoop); }));
259-
_runningOp = runningOp;
322+
if (_runningOp == null)
323+
{
324+
_runningOpCompleteEvent.Reset();
260325

261-
_opSet.Set();
326+
OperationDescriptor runningOp = new OperationDescriptor(new AsyncOperation(() => { return op(waitLoop); }));
327+
_runningOp = runningOp;
262328

263-
waitLoop.Wait(_runningOpCompleteEvent, canTokenSource);
329+
_opSet.Set();
264330

265-
Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?");
331+
waitLoop.Wait(_runningOpCompleteEvent, canTokenSource);
332+
claimed = true;
266333

267-
if (runningOp.ExceptionDispatchInfo != null)
268-
{
269-
runningOp.ExceptionDispatchInfo.Throw();
334+
Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?");
335+
336+
if (runningOp.ExceptionDispatchInfo != null)
337+
{
338+
runningOp.ExceptionDispatchInfo.Throw();
339+
}
340+
341+
return true;
270342
}
343+
}
271344

272-
return true;
345+
return false;
346+
}
347+
finally
348+
{
349+
// The running-op slot was just freed and _eventLock is now released. If promotion on the poll
350+
// thread lost the TryEnter race against this method, re-arm _opSet so a queued async operation
351+
// is promoted immediately instead of stranded until the next unrelated wakeup.
352+
if (claimed && HasPostedAsyncOperation())
353+
{
354+
_opSet.Set();
273355
}
274356
}
275-
276-
return false;
277357
}
278358

359+
// Called on the poll thread to promote the next posted async operation into the running-op slot when it
360+
// is free. Returns true if an operation was moved into the slot.
361+
private bool TryStartPostedAsyncOperation()
362+
{
363+
Debug.Assert(IsPollThread(), "TryStartPostedAsyncOperation must run on the poll thread.");
364+
365+
// Cheap early-out so the poll loop does not contend on _eventLock when there is nothing to promote.
366+
lock (_postedAsyncOperations)
367+
{
368+
if (_isClosed || _postedAsyncOperations.Count == 0)
369+
return false;
370+
}
371+
372+
// Never block on _eventLock here: a client in TrySetOperationInternal holds it across
373+
// _runningOpCompleteEvent.WaitOne() until its operation completes, and only the poll thread can
374+
// complete that operation, so blocking here would deadlock. If a client is mid-set, skip promotion;
375+
// it will be retried on a later poll-loop iteration or wakeup.
376+
if (!Monitor.TryEnter(_eventLock))
377+
return false;
378+
379+
try
380+
{
381+
if (_isClosed || _runningOp != null)
382+
return false;
383+
384+
(AsyncOperation Operation, Action<Exception> OnError) posted;
385+
lock (_postedAsyncOperations)
386+
{
387+
if (_postedAsyncOperations.Count == 0)
388+
return false;
389+
390+
posted = _postedAsyncOperations.Dequeue();
391+
}
392+
393+
_runningOpCompleteEvent.Reset();
394+
395+
// Unlike TrySetOperationInternal, no one waits for completion; faults are routed to the handler.
396+
_runningOp = new OperationDescriptor(posted.Operation) { ErrorHandler = posted.OnError };
397+
398+
return true;
399+
}
400+
finally
401+
{
402+
Monitor.Exit(_eventLock);
403+
}
404+
}
279405

406+
private bool HasPostedAsyncOperation()
407+
{
408+
lock (_postedAsyncOperations)
409+
{
410+
return _postedAsyncOperations.Count > 0;
411+
}
412+
}
280413

281414
// Thread routine for the poll loop. It handles calls coming in from the debug engine as well as polling for debug events.
282415
private void ThreadFunc()
@@ -292,6 +425,11 @@ private void ThreadFunc()
292425
{
293426
ranOperation = false;
294427

428+
if (_runningOp == null)
429+
{
430+
TryStartPostedAsyncOperation();
431+
}
432+
295433
OperationDescriptor runningOp = _runningOp;
296434
if (runningOp != null && !runningOp.IsStarted)
297435
{
@@ -333,11 +471,20 @@ private void ThreadFunc()
333471

334472
if (!completeAsync)
335473
{
474+
// Capture the fault before clearing the slot so a synchronous throw is still reported.
475+
Action<Exception> errorHandler = runningOp.ErrorHandler;
476+
ExceptionDispatchInfo exceptionDispatchInfo = runningOp.ExceptionDispatchInfo;
477+
336478
runningOp.MarkComplete();
337479

338480
Debug.Assert(_runningOp == runningOp, "How did m_runningOp change?");
339481
_runningOp = null;
340482
_runningOpCompleteEvent.Set();
483+
484+
if (errorHandler != null && exceptionDispatchInfo != null)
485+
{
486+
InvokeErrorHandler(errorHandler, exceptionDispatchInfo.SourceException);
487+
}
341488
}
342489
}
343490

@@ -389,8 +536,43 @@ internal void OnAsyncRunningOpComplete(Task t)
389536
}
390537
}
391538
_runningOp.MarkComplete();
539+
540+
// Capture the fault before clearing the slot so it is routed to the handler, not discarded.
541+
Action<Exception> errorHandler = _runningOp.ErrorHandler;
542+
ExceptionDispatchInfo exceptionDispatchInfo = _runningOp.ExceptionDispatchInfo;
543+
544+
// Invoke the handler while the running-op slot is still held so it does not run concurrently
545+
// with the next queued operation. This may still run on the task's completion thread.
546+
if (errorHandler != null && exceptionDispatchInfo != null)
547+
{
548+
InvokeErrorHandler(errorHandler, exceptionDispatchInfo.SourceException);
549+
}
550+
392551
_runningOp = null;
393552
_runningOpCompleteEvent.Set();
553+
554+
lock (_postedAsyncOperations)
555+
{
556+
if (_postedAsyncOperations.Count > 0)
557+
{
558+
_opSet.Set();
559+
}
560+
}
561+
}
562+
563+
private void InvokeErrorHandler(Action<Exception> errorHandler, Exception exception)
564+
{
565+
try
566+
{
567+
errorHandler(exception);
568+
}
569+
catch (Exception e) when (ExceptionHelper.BeforeCatch(e, Logger, reportOnlyCorrupting: false))
570+
{
571+
if (PostedOperationErrorEvent != null)
572+
{
573+
PostedOperationErrorEvent(this, e);
574+
}
575+
}
394576
}
395577

396578
internal bool IsPollThread()

0 commit comments

Comments
 (0)