Skip to content

perf(native): parse each font face once, and load CJK faces when CJK text comes up - #928

Merged
omartelo merged 2 commits into
mainfrom
perf/native-font-memory
Oct 23, 2026
Merged

omartelo merged 2 commits into
mainfrom
perf/native-font-memory

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

Summary

A heap profile of a real window (LICH_NATIVE_PPROF) 36 s after start showed 403 MB in use, about 240 MB of it fonts parsed at start. Two causes, one commit each.

  • A font face is parsed once while it is held (perf(fonts)). The window asked for the same file several times: the interface and the terminal set in one family, and the bold face fontconfig answers for both semibold and bold. Each ParseFace parsed the file's tables and every glyph outline again, about 30 MB for a Nerd Font face. Parsed faces are now kept by Location under a weak.Pointer and shared while something holds them, so a family that is let go (a font picker's sample, the interface font before a change) is still freed; a plain cache would have kept every sampled family for the run.
  • The CJK faces load when CJK text first comes up (perf(native)). An interface in a language other than Chinese, Japanese or Korean loaded a CJK font at start, 56 MB with wqy-zenhei, whether any Han came up or not. Gio parses every face of a shaper's collection when the shaper is built, so holding one back needs to know when text asks for it: the fork gains text.WithOutsideCollection, called with each rune none of the collection's faces draws. The window marks the first Han, kana or Hangul and, on the next frame, builds the interface's shaper again with the CJK faces, picked by the same rules (hanFaces, weights covered). Until that frame the system font the shaper falls back to draws the rune. A CJK interface language loads them at start, as before.

The fork commit is lichdotdev/gio@07547a4d on branch lich, merged with upstream's main (16fb1737), which native/go.mod now points at.

Measured

Live heap held by the interface and terminal fonts, with ZedMono Nerd Font Mono for the interface, the terminal and the editor, the interface in English:

live heap
before 246 MB
faces shared 175 MB
faces shared, no CJK on screen 119 MB

Test plan

  • gofmt, go vet ./..., go test ./... in native/
  • New TestParseFaceSharesAFaceWhileItIsHeld (fails without the sharing, and fails with a cache that holds faces strongly), -race -count=20
  • New TestCJKFacesJoinTheInterfaceOnceCJKTextComesUp (fails with the CJK faces loaded at start, and fails without the rebuild on the next frame)
  • Fork: new TestShaperReportsRunesOutsideCollection; go test ./text/ after the upstream merge
  • GOOS=darwin and GOOS=windows go vet ./fonts/

The window asked for the same font file several times: the interface and
the terminal set in one family, and the bold face fontconfig answers for
both semibold and bold. Each ParseFace parsed the file's tables and every
glyph outline again, about 30 MB for a Nerd Font face. Parsed faces are
now kept by location under a weak pointer and shared while something holds
them, so a family let go (a font picker's sample, the interface font before
a change) is still freed.

With ZedMono Nerd Font Mono for the interface, the terminal and the editor,
the interface and terminal fonts drop from 246 MB to 175 MB of live heap.
An interface in a language other than Chinese, Japanese or Korean loaded
a CJK font at start for the Han it might draw in a branch or a title,
56 MB with wqy-zenhei, whether any came up or not. The interface's shaper
now leaves the CJK faces out until it is asked to draw Han, kana or
Hangul: the fork's text.WithOutsideCollection reports a rune none of its
faces draws, the window marks it, and on the next frame builds the shaper
again with the faces, picked by the same rules. Until then the system font
the shaper falls back to draws it. A CJK interface language loads them at
start as before.

Bumps lichdotdev/gio to 16fb1737 (branch lich), which carries
WithOutsideCollection and merges upstream's main.

With no CJK on screen the interface and terminal fonts take 119 MB of live
heap, down from 175 MB.
@omartelo
omartelo merged commit 617f018 into main Oct 23, 2026
7 checks passed
@omartelo
omartelo deleted the perf/native-font-memory branch October 23, 2026 22:54
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