Skip to content

fix: run swing mutations on the edt to stop client freezes - #116

Merged
AntonyGarand merged 1 commit into
masterfrom
fix/edt-threading-deadlock
Oct 21, 2026
Merged

AntonyGarand merged 1 commit into
masterfrom
fix/edt-threading-deadlock

Conversation

@AntonyGarand

Copy link
Copy Markdown
Contributor

Problem

The client intermittently froze completely (no crash, no log output) when doing certain GE actions. Reproduced on a developer-mode client and captured thread dumps of the frozen JVM (jcmd <pid> Thread.print -l, saved during diagnosis).

Root cause: an AWT tree lock ↔ document lock deadlock

Two threads were taking the same two locks in opposite order:

Game Client thread (RuneLite delivers WidgetLoaded/ScriptPostFired on it) — holds the AWT tree lock, wants a document lock:

GameUiChangesHandler.onWidgetLoaded (GE_OFFERS/BANKPIN_KEYPAD) → MasterPanel.selectPreviouslySelectedTab
  → FastTabGroup.select → CardLayout.show → Container.validate      [holds AWTTreeLock]
  → layout → JTextField.getPreferredSize → AbstractDocument.readLock [waits for PlainDocument]

EDT — holds that document's write lock, wants the tree lock:

FlippingPanel.rebuild → Paginator.setPageNumber → updateControls → JTextComponent.setText
  → AbstractDocument.replace                                            [holds document write lock]
  → removeUpdate → PlainView → JComponent.revalidate → Component.invalidate [waits for AWTTreeLock]

Once the cycle closes, both threads freeze forever: the game loop stops ticking (whole client frozen) and nothing throws, which is why nothing lands in the logs. Note jstack's own deadlock detector misses this cycle because readLock() waits inside Object.wait() (releasing the monitor), hiding the edge.

CPU at freeze: ~10% of one core (not a spin — threads parked). The same audit found more off-EDT Swing mutations of the same class, listed below.

Fix

All Swing mutations now bounce to the EDT (no behavior change beyond threading):

  • FastTabGroup — select, showView, selectPreviouslySelectedTab, revertToSafeDisplay guard on SwingUtilities.isEventDispatchThread() and re-post via invokeLater. This centrally fixes the game-thread paths: onWidgetLoaded (GE offers/bankpin → selectPreviouslySelectedTab, GE history → showView), onScriptPostFired (script 29), the offer editor tab selects from onVarClientIntChanged, and handleLogout's revertToSafeDisplay.
  • 1-second repeating task (FlippingPlugin.setupRepeatingTasks) — updateTimerDisplays / updateTimeDisplay / updateSessionTime / updateAutoSaveDisplay were mutating labels/panels from the scheduler thread (the frozen dumps showed it blocked in StatsPanel.updateAutoSaveDisplay → Container.add). They're now batched into one invokeLater per tick. This also removes the cross-thread panel iteration that the old ConcurrentModificationException-restart hack was papering over.
  • handleLogin — account selector setVisible/setSelectedItem (fires the listener that rebuilds all panels) bounced to the EDT; login runs on the client thread.
  • MasterPanel.setupAccSelectorDropdown — called on the client thread during startup; guarded the same way.

Already-EDT-safe paths verified and left untouched: AbstractOfferEditorPanel.rebuild, GeHistoryTabPanel.rebuild, FlippingPanel.rebuild/highlightItem, StatsPanel.rebuildItemsDisplay, and the existing updateAutoSaveDisplay call sites in FlippingPlugin.

Verification

  • Reproduced the freeze on the unpatched client (thread dumps show the lock cycle; baseline dump healthy).
  • Same actions on the patched client (GE open/close, GE history tab, offer setup editors, bankpin, tab switches, logout/login): no freeze, client stays responsive.
  • Full test suite passes.

Widget/script events and the plugin''s repeating scheduler task mutated
swing components off the edt. Switching views through FastTabGroup
validates the component tree (taking the awt tree lock and reading text
field documents) while the edt was simultaneously setting text on those
same fields (holding the document write lock while waiting for the tree
lock), closing a lock-order cycle that froze both the game client thread
and the edt: no exception, no log output, whole client frozen.

- FastTabGroup entry points (select, showView, selectPreviouslySelectedTab,
  revertToSafeDisplay) bounce to the edt, covering the WidgetLoaded and
  ScriptPostFired handlers, the ge history view and the offer editors
- the 1s repeating task ui updates are batched into one invokeLater per
  tick instead of mutating labels/panels from the scheduler thread
- handleLogin account selector updates and
  MasterPanel.setupAccSelectorDropdown (startup) bounce to the edt too

reproduced the freeze on the unpatched client and captured thread dumps
showing the cycle, then verified the same actions no longer freeze the
patched client
@AntonyGarand
AntonyGarand marked this pull request as ready for review October 21, 2026 15:10
@AntonyGarand
AntonyGarand merged commit 450f362 into master Oct 21, 2026
2 checks passed
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant