perf(glyph): map font files instead of reading them into the heap
- commit
- 768bf7807e62121797f51eeac1e45436a3653254
- author
- brice · · 2026-10-08 05:37 UTC
- parent
- 452b6b4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +34 -6
M internal/glyph/glyph.go +34 -6
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "golang.org/x/image/font" | |
| 14 | 14 | "golang.org/x/image/font/opentype" | |
| 15 | 15 | "golang.org/x/image/math/fixed" | |
| 16 | + | "golang.org/x/sys/unix" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 18 | 19 | // Style is a set of text attributes. Bold and Italic pick a font file; | |
| @@ -37,6 +38,9 @@ type Paths [4]string | |||
| 37 | 38 | // clipped to a CellW x CellH box. It is used from one goroutine. | |
| 38 | 39 | type Face struct { | |
| 39 | 40 | faces [4]font.Face | |
| 41 | + | // maps are the font files, mapped read-only: they stay in the shared page | |
| 42 | + | // cache instead of the Go heap, and only the pages read count as memory. | |
| 43 | + | maps [][]byte | |
| 40 | 44 | // CellW and CellH are the cell size in physical pixels. | |
| 41 | 45 | CellW, CellH int | |
| 42 | 46 | ascent fixed.Int26_6 | |
| @@ -74,16 +78,35 @@ func FamilyOf(request string) string { | |||
| 74 | 78 | return strings.TrimSpace(string(out)) | |
| 75 | 79 | } | |
| 76 | 80 | ||
| 77 | - | func loadFace(path string, px float64) (font.Face, error) { | |
| 78 | - | data, err := os.ReadFile(path) | |
| 81 | + | // mapFile maps a whole file read-only. | |
| 82 | + | func mapFile(path string) ([]byte, error) { | |
| 83 | + | file, err := os.Open(path) | |
| 84 | + | if err != nil { | |
| 85 | + | return nil, err | |
| 86 | + | } | |
| 87 | + | defer file.Close() | |
| 88 | + | st, err := file.Stat() | |
| 89 | + | if err != nil { | |
| 90 | + | return nil, err | |
| 91 | + | } | |
| 92 | + | if st.Size() <= 0 || st.Size() != int64(int(st.Size())) { | |
| 93 | + | return nil, fmt.Errorf("%s: unusable size %d", path, st.Size()) | |
| 94 | + | } | |
| 95 | + | return unix.Mmap(int(file.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // loadFace opens the file at path and appends its mapping to f.maps. | |
| 99 | + | func (f *Face) loadFace(path string, px float64) (font.Face, error) { | |
| 100 | + | data, err := mapFile(path) | |
| 79 | 101 | if err != nil { | |
| 80 | 102 | return nil, fmt.Errorf("glyph: %w", err) | |
| 81 | 103 | } | |
| 82 | - | f, err := opentype.Parse(data) | |
| 104 | + | f.maps = append(f.maps, data) | |
| 105 | + | fnt, err := opentype.Parse(data) | |
| 83 | 106 | if err != nil { | |
| 84 | 107 | return nil, fmt.Errorf("glyph: parse %s: %w", path, err) | |
| 85 | 108 | } | |
| 86 | - | return opentype.NewFace(f, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull}) | |
| 109 | + | return opentype.NewFace(fnt, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull}) | |
| 87 | 110 | } | |
| 88 | 111 | ||
| 89 | 112 | // Load opens the font files at px pixels. The cell size comes from the regular | |
| @@ -105,7 +128,7 @@ func Load(paths Paths, px float64) (*Face, error) { | |||
| 105 | 128 | f.faces[i] = face | |
| 106 | 129 | continue | |
| 107 | 130 | } | |
| 108 | - | face, err := loadFace(path, px) | |
| 131 | + | face, err := f.loadFace(path, px) | |
| 109 | 132 | if err != nil { | |
| 110 | 133 | f.Close() | |
| 111 | 134 | return nil, err | |
| @@ -144,7 +167,8 @@ func Load(paths Paths, px float64) (*Face, error) { | |||
| 144 | 167 | return f, nil | |
| 145 | 168 | } | |
| 146 | 169 | ||
| 147 | - | // Close releases the font faces. | |
| 170 | + | // Close releases the font faces and unmaps their files. The face must not be | |
| 171 | + | // used afterwards. | |
| 148 | 172 | func (f *Face) Close() { | |
| 149 | 173 | if f == nil { | |
| 150 | 174 | return | |
| @@ -157,6 +181,10 @@ func (f *Face) Close() { | |||
| 157 | 181 | } | |
| 158 | 182 | f.faces[i] = nil | |
| 159 | 183 | } | |
| 184 | + | for _, m := range f.maps { | |
| 185 | + | _ = unix.Munmap(m) | |
| 186 | + | } | |
| 187 | + | f.maps = nil | |
| 160 | 188 | } | |
| 161 | 189 | ||
| 162 | 190 | // Rasterize draws r into dst, which must hold CellW*CellH bytes (stride | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "golang.org/x/image/font" | 13 | "golang.org/x/image/font" | ||
| 14 | "golang.org/x/image/font/opentype" | 14 | "golang.org/x/image/font/opentype" | ||
| 15 | "golang.org/x/image/math/fixed" | 15 | "golang.org/x/image/math/fixed" | ||
| 16 | + | "golang.org/x/sys/unix" | |||
| 16 | ) | 17 | ) | ||
| 17 | 18 | ||||
| 18 | // Style is a set of text attributes. Bold and Italic pick a font file; | 19 | // Style is a set of text attributes. Bold and Italic pick a font file; | ||
| @@ -37,6 +38,9 @@ type Paths [4]string | |||||
| 37 | // clipped to a CellW x CellH box. It is used from one goroutine. | 38 | // clipped to a CellW x CellH box. It is used from one goroutine. | ||
| 38 | type Face struct { | 39 | type Face struct { | ||
| 39 | faces [4]font.Face | 40 | faces [4]font.Face | ||
| 41 | + | // maps are the font files, mapped read-only: they stay in the shared page | |||
| 42 | + | // cache instead of the Go heap, and only the pages read count as memory. | |||
| 43 | + | maps [][]byte | |||
| 40 | // CellW and CellH are the cell size in physical pixels. | 44 | // CellW and CellH are the cell size in physical pixels. | ||
| 41 | CellW, CellH int | 45 | CellW, CellH int | ||
| 42 | ascent fixed.Int26_6 | 46 | ascent fixed.Int26_6 | ||
| @@ -74,16 +78,35 @@ func FamilyOf(request string) string { | |||||
| 74 | return strings.TrimSpace(string(out)) | 78 | return strings.TrimSpace(string(out)) | ||
| 75 | } | 79 | } | ||
| 76 | 80 | ||||
| 77 | - | func loadFace(path string, px float64) (font.Face, error) { | 81 | + | // mapFile maps a whole file read-only. |
| 78 | - | data, err := os.ReadFile(path) | 82 | + | func mapFile(path string) ([]byte, error) { |
| 83 | + | file, err := os.Open(path) | |||
| 84 | + | if err != nil { | |||
| 85 | + | return nil, err | |||
| 86 | + | } | |||
| 87 | + | defer file.Close() | |||
| 88 | + | st, err := file.Stat() | |||
| 89 | + | if err != nil { | |||
| 90 | + | return nil, err | |||
| 91 | + | } | |||
| 92 | + | if st.Size() <= 0 || st.Size() != int64(int(st.Size())) { | |||
| 93 | + | return nil, fmt.Errorf("%s: unusable size %d", path, st.Size()) | |||
| 94 | + | } | |||
| 95 | + | return unix.Mmap(int(file.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // loadFace opens the file at path and appends its mapping to f.maps. | |||
| 99 | + | func (f *Face) loadFace(path string, px float64) (font.Face, error) { | |||
| 100 | + | data, err := mapFile(path) | |||
| 79 | if err != nil { | 101 | if err != nil { | ||
| 80 | return nil, fmt.Errorf("glyph: %w", err) | 102 | return nil, fmt.Errorf("glyph: %w", err) | ||
| 81 | } | 103 | } | ||
| 82 | - | f, err := opentype.Parse(data) | 104 | + | f.maps = append(f.maps, data) |
| 105 | + | fnt, err := opentype.Parse(data) | |||
| 83 | if err != nil { | 106 | if err != nil { | ||
| 84 | return nil, fmt.Errorf("glyph: parse %s: %w", path, err) | 107 | return nil, fmt.Errorf("glyph: parse %s: %w", path, err) | ||
| 85 | } | 108 | } | ||
| 86 | - | return opentype.NewFace(f, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull}) | 109 | + | return opentype.NewFace(fnt, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull}) |
| 87 | } | 110 | } | ||
| 88 | 111 | ||||
| 89 | // Load opens the font files at px pixels. The cell size comes from the regular | 112 | // Load opens the font files at px pixels. The cell size comes from the regular | ||
| @@ -105,7 +128,7 @@ func Load(paths Paths, px float64) (*Face, error) { | |||||
| 105 | f.faces[i] = face | 128 | f.faces[i] = face | ||
| 106 | continue | 129 | continue | ||
| 107 | } | 130 | } | ||
| 108 | - | face, err := loadFace(path, px) | 131 | + | face, err := f.loadFace(path, px) |
| 109 | if err != nil { | 132 | if err != nil { | ||
| 110 | f.Close() | 133 | f.Close() | ||
| 111 | return nil, err | 134 | return nil, err | ||
| @@ -144,7 +167,8 @@ func Load(paths Paths, px float64) (*Face, error) { | |||||
| 144 | return f, nil | 167 | return f, nil | ||
| 145 | } | 168 | } | ||
| 146 | 169 | ||||
| 147 | - | // Close releases the font faces. | 170 | + | // Close releases the font faces and unmaps their files. The face must not be |
| 171 | + | // used afterwards. | |||
| 148 | func (f *Face) Close() { | 172 | func (f *Face) Close() { | ||
| 149 | if f == nil { | 173 | if f == nil { | ||
| 150 | return | 174 | return | ||
| @@ -157,6 +181,10 @@ func (f *Face) Close() { | |||||
| 157 | } | 181 | } | ||
| 158 | f.faces[i] = nil | 182 | f.faces[i] = nil | ||
| 159 | } | 183 | } | ||
| 184 | + | for _, m := range f.maps { | |||
| 185 | + | _ = unix.Munmap(m) | |||
| 186 | + | } | |||
| 187 | + | f.maps = nil | |||
| 160 | } | 188 | } | ||
| 161 | 189 | ||||
| 162 | // Rasterize draws r into dst, which must hold CellW*CellH bytes (stride | 190 | // Rasterize draws r into dst, which must hold CellW*CellH bytes (stride | ||