Repository navigation
#3222 Fix macOS freeze when opening the Overview tab - #3226
Conversation
NewtCanvasAWT.addNotify() locked the JAWT surface on the AWT EDT. On macOS that call blocks in OSXUtil.RunOnMainThread until AppKit creates the native window, which deadlocks whenever AppKit is itself waiting on the AWT EDT through CAccessibility. Any accessibility client polling the process can trigger it, and the mutual wait spans two runtime-managed threads, so deadlock detection reports nothing. Extend the existing Bug 1478 mitigation to that call site: extract the SecondaryLoop body of runOnNewtEDTAndWaitOnAWTEDT() into runOffAWTEDTAndPumpEvents(), and add lockSurfaceInitially(), which runs the lock off the AWT EDT while the EDT keeps dispatching events. Guard validateComponent() against attaching the NEWT child during that window. Other platforms and non-EDT callers keep the original inline behavior.
|
Hi @MattArtzAnthro thanks a lot for the detailed report, and the fix. I still need to wrap my head around this as this is JOGL code but I wonder if you could also share the way you reproduce this? I would like to try on my own before concluding. Thanks in advance! |
|
Thanks Mathieu. Happy to give you a recipe you can run yourself. First, one clarification that I should have made clearer in the PR, because I think my So none of what follows needs gephi-ai, or any Gephi plugin at all. It runs against a The short versionThe freeze needs two things at the same time:
Either one alone is harmless. Together they deadlock. That is why this has been so hard to By handIf you want to feel it rather than script it, this is what I originally did:
That last step is the part I would encourage you to try, because it is the control rather Scripted, zero dependenciesTwo small AppleScript files. One polls accessibility, one drives the window. Both are in a One gotcha that cost me some time: Gephi registers in the accessibility tree as Find the pid first: PID=$(pgrep -f "Gephi.app/Contents/PlugIns.*java")
on run argv
tell application "System Events"
set p to first process whose unix id is (item 1 of argv as integer)
repeat 100000 times
try
get value of attribute "AXFocusedUIElement" of p
end try
end repeat
end tell
end run
on run argv
tell application "System Events"
set p to first process whose unix id is (item 1 of argv as integer)
set frontmost of p to true
delay 0.2
set wm to (first menu bar item of menu bar 1 of p whose name is "Window")
click menu item "Graph" of menu 1 of wm
delay 0.4
click menu item "Close Window" of menu 1 of wm
delay 0.4
click menu item "Graph" of menu 1 of wm
delay 0.4
end tell
end runRun them together: osascript poller.applescript $PID & # the trigger
for i in $(seq 1 200); do # the driver
echo "cycle $i"
osascript cycle.applescript $PID
doneWhen it deadlocks, the loop simply stops advancing: the current In my runs it froze somewhere between cycle 100 and cycle 150, which is a couple of Terminal needs Accessibility permission in System Settings, Privacy and Security, When it freezes, take a dump with What I measuredGrammarly quit and fully unloaded for all of these, so the poller above was the only
The first row matters as much as the second. Driving the window system hard, on its own, For the branch run I removed my own plugin from the user directory first, so both builds So that is 200 real canvas creations and teardowns with no freeze, and afterwards zero The dump from the stock run is identical to the one in #3222:
I have the full thread dumps for every run above and am glad to post them, or to run any One small thing still open from the PR description, whenever you get to it: there is a As with the original report and the PR, this round of testing was worked out together with |
|
Thanks a lot @MattArtzAnthro, I verified the fixes and everything seems to check out. |
Description
Fixes #3222.
NewtCanvasAWT.addNotify()takes an initial JAWT surface lock on the AWT EDT:On macOS that call reaches
OSXUtil.RunOnMainThread()and blocks the AWT EDT until AppKit's main thread has created the native window. If AppKit is concurrently waiting on the AWT EDT, both threads stop. The reported thread dumps show exactly that pair:LWCToolkit.invokeAndWait()runs the main run loop in a private mode that does not execute the blocks JOGL posts throughperformSelectorOnMainThread, so neither side can progress. Because the wait spans two runtime-managed threads rather than Java monitors,ThreadMXBeandeadlock detection reports nothing, and the process never crashes. Line 693 in the dumps is thejawtWindow.lockSurface()call in the patchedNewtCanvasAWTshipped in 0.11.2, so this is not a stale-JOGL artifact.This is the same failure mode as JOGL Bug 1478, which 10cfb23 addressed for
attachNewtChild(). That fix leftaddNotify()untouched, andaddNotify()blocks the AWT EDT in the same way. The trigger is any macOS accessibility client polling the application (in my case Grammarly Desktop, and a screen reader would do the same), which explains why the freeze is intermittent and why the reports in #2546 were so hard to reproduce.Change
addNotify()now performs its initial surface lock through the existingSecondaryLoopmechanism instead of blocking the AWT EDT:SecondaryLoopbody ofrunOnNewtEDTAndWaitOnAWTEDT()intorunOffAWTEDTAndPumpEvents(), which takes the dispatch strategy as a parameter.runOnNewtEDTAndWaitOnAWTEDT()keeps its behavior and now delegates to it.lockSurfaceInitially(), which on macOS runslockSurface()/unlockSurface()on a short-lived daemon thread while the AWT EDT keeps dispatching events. AppKit's pendinginvokeAndWaitis therefore served, the main thread returns to its normal run loop, the native window is created, and the lock completes.initialSurfaceLockPendingguard. Because the AWT EDT dispatches events during that wait, a queued paint can re-entervalidateComponent()and attach the NEWT child whileaddNotify()is still in flight, which would lock the very surface the pending task holds.validateComponent()now returns false untiladdNotify()has returned, and the normal paint path attaches afterwards.Every other platform, and any call not on the AWT EDT, keeps the original behavior of locking inline.
Reproduction
The freeze is a race, so I built a driver for it rather than clicking. Switching perspectives runs
PerspectiveController.selectPerspective()on the EDT, which rearranges the NetBeans window system throughEditorView.updateAWTHierarchyand reachesTopComponent.addNotify, the exact path in the dumps. Driving that switch in a loop, with a macOS accessibility client polling the focused application, reproduces the freeze on demand.CAccessibility.getAccessibleIndexInParentrather thangetFocusOwner. Worth noting, because it shows the trigger is not one particular accessibility call. Any AppKit query that reaches the EDT throughLWCToolkit.invokeAndWaitduring the surface lock will do it.NewtCanvasAWT-InitialSurfaceLockworkers, and a capture of the live canvas came back at 1962 by 1448 pixels on a roughly 981 by 724 point window, so the 2x pixel scale the initial lock exists to acquire is still being applied.A dump taken during the surface lock on the patched build shows both halves of the fix at the same instant:
The frames that used to block the EDT are on the worker thread, and the EDT is dispatching at that moment, so AppKit's pending
invokeAndWaitis served and the native window creation completes. It also confirms the new path is the one running, rather thancreateSecondaryLoop()returning null and falling back to the original inline lock.Scope and residual risk
The change is limited to macOS on the AWT EDT. Taking a JAWT surface lock off the EDT is ordinary JOGL practice, since the GL animator thread does it on every frame, and
MacOSXJAWTWindow.lockSurfaceImpl()is written to be callable from any thread. The pattern itself is already in production in this file forattachNewtChild().The one behavior that is genuinely new is that the AWT EDT dispatches events inside
Container.addNotify(), while it holds the AWT tree lock. TheinitialSurfaceLockPendingguard covers the re-entrant path that matters here, which is attaching the NEWT child early. I would welcome a second opinion on whether any other re-entrant path through this component deserves the same guard.detachNewtChild()is untouched, for the reason already documented in the file header.A separate pre-existing defect, noticed while testing
Not fixed here, and worth its own issue if you agree it is real.
Those line numbers are current master, not this branch.
attachNewtChild()readsjawtWindowimmediately afterrunOnNewtEDTAndWaitOnAWTEDT(), which pumps AWT events, so aremoveNotify()dispatched during that pump nulls the field before the next statement dereferences it.It appeared once in 500 cycles on master and once in 650 on this branch, with the same stack, so the rates are indistinguishable and this branch does not introduce or worsen it. I also instrumented the branch to report any destroy occurring during the new
addNotify()wait, and the probe never fired, so the new pump is not a path to it. Happy to open the issue, or to fold a null re-check into this PR if you would rather have it in one place.Checklist
Added tests?
The failure is a timing race between the AWT EDT, AppKit's main thread, and an external macOS accessibility client. It cannot be reproduced deterministically in a unit test, and it does not reproduce at all without an accessibility client polling the process. Verification is therefore manual, on macOS 26.5.1 (Apple Silicon), driving the perspective switch through a local automation API rather than clicking. Numbers are in the Reproduction section above. With an accessibility client running, master froze inside 50 cycles and this branch ran 650 without a freeze. A further 500 cycles were run with the accessibility client stopped, which says nothing about the deadlock and was there to measure the unrelated NPE rate.
Added to documentation?
The Gephi patch header in
NewtCanvasAWT.javarecords the second patched site, andlockSurfaceInitially()carries the full reasoning.As with the original report, the fix and the testing above were worked out together with Claude, Anthropic's AI assistant. I built, ran, and verified everything on my own machine, and I am happy to re-run any of it or to gather more dumps if that would help.