PERF: Let FreeType read font data from memory instead of through Python - #32064
Conversation
|
How does this intersect with a (hot) disk cache? What is the memory hit (discussed this in person with @ksunden ). |
|
My understanding of how this works is that the disk cache is on the kernel side, and doesn't help with the userspace calls to fetch that data (much less the python overhead). I believe the reason the WSL bridge/network drives are so much slower here is because they can't have a disk cache. mmap forces that cache to happen, and skips the kernel for direct memory reads, so there's no additional memory usage (for local filesystems) or syscall/python overhead (for either). If the file changes underneath while we're running then the values we've already loaded to ram will be stale, but I don't think that's behavior we need to protect for. Measuring it, DejaVuSans is ~738kB on disk, and for the ascii character set mmap lazily loads only ~292kB of this into ram (mostly headers). But again, I think this memory usage is just the disk cache made explicit and not actually additional. It's a free win AFAICT. |
bdf46d9 to
3b5fe33
Compare
|
@QuLogic How do we get the WASM tests to run? |
One could normally just add the cibuildwheel label, but currently the WASM build doesn't run any tests because they are broken, so you will only get a compile check. |
| // Fall back to a copy of the whole file into memory. | ||
| data = self->py_file.attr("read")(); |
There was a problem hiding this comment.
I'm a bit wary of this. While most single-language fonts can be relatively small, fonts that cover large swaths of Unicode, like CJK fonts, may be much bigger. On Google Fonts, the largest font is Chiron Sung HK which is over 50 MB. And if someone configures fallback fonts, they will now all get loaded in their entirety, causing a large increase in memory usage after this.
As a worst case, if we start supporting full family loading (i.e., all weights of a family), then Noto Sans CJK in all its weights takes up 125MiB:
$ du -hsc /usr/share/fonts/google-noto-sans-cjk-fonts/*
19M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Black.ttc
20M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc
18M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-DemiLight.ttc
18M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Light.ttc
18M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Medium.ttc
19M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc
16M /usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Thin.ttc
125M total
There was a problem hiding this comment.
Would it be an option to do the pre-read into memory only for "small" fonts? With a suitable definitions of small, possibly even configurable.
This could still yield a speed up for common cases but prevent excessive memory use.
There was a problem hiding this comment.
I'd be okay with moving this fallback back to the original implementation, I think the remaining slow path of WASI reading fonts over a network is a super edge case and we're getting into a storage vs speed tradeoff regardless.
Pushed that as a new commit, can revert pending discussion.
9f6d4c0 to
d29a0b9
Compare
…h python More targeted error handling
960f6a2 to
a029d55
Compare
PR summary
FT2Fontcurrently re-reads data from the font file on every glyph load, costing 10 lseek and 4 read syscalls per glyph on everyFT2Font.set_textcall. This is still relatively fast on a local filesystem, but imposes huge IO strain on remote filesystems (WSL's /mnt/c bridge to windows in my particular case, but also network mounts).This PR mmaps the font file and hands the buffer to FreeType, so glyph loads skip the Python layer and only touch the filesystem once. Python still handles opening the file for unicode path handling, with a test added for that.
Note that we build freetype with meson's
auto_features=disabled, so itsmmap=autoline was a noop and removed in this PR. It doesn't matter here since we're handing freetype the data instead of a path.Measuring syscalls directly on the font file for 30 glyphs across 10
set_textcalls, we drop from ~2900 lseeks and ~1200 reads to 1 lseek and 1 mmap. When using the local linux filesystem on my machine,set_textis ~15x faster and a savefig on an empty plot is ~1.5x faster. When I cross the WSL filesystem boundary, the speedups are ~1000x forset_textand ~10x for savefig.Before (Across WSL filesystem boundary):
After:

AI Disclosure
Discovered myself, investigated and prototyped with claude code, manually reviewed / edited.
PR checklist