Sitelet https://github.com/unoplatform/uno/pull/24973
Skip to content

perf(text): Read seekable font streams into a pre-sized buffer - #24973

Open
AtakanUk wants to merge 1 commit into
unoplatform:masterfrom
AtakanUk:fix/font-bytes-allocation
Open

AtakanUk wants to merge 1 commit into
unoplatform:masterfrom
AtakanUk:fix/font-bytes-allocation

Conversation

@AtakanUk

@AtakanUk AtakanUk commented Oct 4, 2026

Copy link
Copy Markdown

GitHub Issue: closes #24861

PR Type:

🐞 Bugfix

What changed? 🚀

FontDetailsCache.LoadFontBytesAsync copied each font into a growing MemoryStream and then called ToArray(), so every face paid for the buffer doubling plus a second full copy. During PreloadFonts this is the ~14 MB of transient byte[] reported in the issue.

When the stream is seekable, the font is now read with ReadExactlyAsync straight into an array of the stream length. That is the case for every app-package font: ms-appx resolves to a local FileStream on Skia desktop, WebAssembly (after the asset download) and Android, and the HTTP path returns a buffered stream. Non-seekable streams keep the previous behaviour.

I first tried the existing StreamExtensions.ReadBytesAsync() helper, which also pre-sizes the buffer. It awaits without ConfigureAwait(false) though, so each read hops back to the UI thread, and it made the font preload about 240 ms slower in the measurements below. ReadExactlyAsync resumes on the UI thread only once per face.

Measured with SamplesApp, which preloads the same Uno.Fonts.OpenSans manifest (37 faces, 5.35 MB): Skia desktop, Win32, Release, net10.0-desktop, 9 interleaved launches each, medians, using the marks from the issue:

master this PR
Allocated at OnLaunched start 18.86 MB 11.18 MB
Allocated when PreloadFonts completes 34.17 MB 20.99 MB
Allocated at first frame 44.04 MB 30.75 MB (-13.3 MB)
Gen0 / Gen1 / Gen2 at first frame 3 / 3 / 2 2 / 2 / 1
Font preload duration 1468 ms 1464 ms

The instrumentation was local only and is not part of this PR.

Tests run locally on Skia desktop (Win32): Given_TextBlock*, Given_GlyphRunRenderer, Given_PersonPicture, Given_SkiaTextFormatter, Given_BlockLayoutEngine and Given_TextBox. 390 passed, 10 skipped, 1 failed: Given_TextBlock.When_Inlines_Transitively_Change, which fails the same way on master on my machine (the screenshot comes back empty).

PR Checklist ✅

  • Added Runtime tests, UI tests, or a manual test sample (for bug fixes / features, if applicable)
    No new test: the change is limited to how the bytes are read, and the existing font loading tests cover that path. An allocation assertion in a runtime test would be too noisy to be reliable.
  • Docs have been added/updated following the documentation template (for bug fixes / features)
    Not needed: internal change, no public API or behaviour change.
  • Validated PR Screenshots Compare Test Run results.
  • Contains NO breaking changes
  • Reviewed 2 other open pull requests (optional but appreciated!)

LoadFontBytesAsync copied each font into a growing MemoryStream and then
ToArray()'d it, allocating about 3.4x the file size per face. When the
stream is seekable, read it straight into an array of its length.

Fixes unoplatform#24861
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The localized optimization preserves stream-position handling and fallback behavior, with no identified regressions.

Review effort: Balanced
Findings: None

What changed in this PR

Reduces Skia font-loading allocations to address #24861 without changing the font cache’s behavior.

Changes:

  • Reads seekable streams directly into a buffer sized to the remaining bytes.
  • Preserves the existing fallback for non-seekable streams.
File Description
src/​Uno.UI/​UI/​Xaml/​Documents/​TextFormatting/​FontDetailsCache.cs Avoids buffer growth and an extra copy when loading seekable font streams.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

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.

Skia startup allocates ~14 MB for font bytes before first frame (FontDetailsCache.LoadFontBytesAsync), +69% vs 6.7

2 participants