Guard the global appearance behind one lock - #458
Merged
Conversation
The metric scale, the touch density, the active font and the OpenType size a host last asked for are package globals: a host SETS them, and every widget READS them on every metric it lays out. Nothing synchronised the two. Found by -race from an application whose tests set the scale in one test while another was constructing a list. It is not a test artefact: moving a window between displays is exactly this pairing — the scale set from whatever thread the display-change event arrives on, while the UI is laying out at the old one. What a race costs here is not a crash but silence, chrome laid out at one scale around type measured at another. One RWMutex for the four, because they are one fact — "how big is everything" — SetMetricScale re-renders the font so the scale and the font change together, and two locks taken in two orders is how a toolkit deadlocks on a display change. The invariant, written down in appearance.go: no call holds the lock across another call that takes it. SetMetricScale drops it before rescaleText, which installs a face through SetFont, which takes it again; CurrentFont reads the scale once and passes it to scaledDefaultFont rather than reading it again, because RLock is not safely reentrant either — a writer arriving between two nested RLocks deadlocks both. The test fails without the change: eight races over one run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The metric scale, the touch density, the active font and the OpenType size a
host last asked for are package globals. A host sets them; every widget
reads them, on every metric it lays out. Nothing synchronised the two.
Found by
-racefrom an application whose tests set the scale in one test whileanother was constructing a
ListBox. It is not a test artefact — moving awindow between displays is exactly this pairing: the scale set from whatever
thread the display-change event arrives on, while the UI is laying out at the
old one. What a race costs here is not a crash but silence: chrome laid out at
one scale around type measured at another.
One
RWMutexfor the four, because they are one fact — how big is everything.SetMetricScalere-renders the font, so the scale and the font change together,and two locks taken in two orders is how a toolkit deadlocks on a display
change.
The invariant, written down in
appearance.go: no call holds the lockacross another call that takes it.
SetMetricScaledrops it beforerescaleText, which installs a face throughSetFont, which takes it again;CurrentFontreads the scale once and passes it toscaledDefaultFontratherthan reading it again, because
RLockis not safely reentrant either — a writerarriving between two nested
RLocks deadlocks both.Evidence
TestAppearanceSurvivesConcurrentUseruns three writers and six readers overthe four globals.
Full suite green, plain and under
-race.🤖 Generated with Claude Code