detached 768bf78

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +34 -6
M internal/glyph/glyph.go +34 -6
@@ -13,6 +13,7 @@ import (
1313 "golang.org/x/image/font"
1414 "golang.org/x/image/font/opentype"
1515 "golang.org/x/image/math/fixed"
16+ "golang.org/x/sys/unix"
1617 )
1718
1819 // Style is a set of text attributes. Bold and Italic pick a font file;
@@ -37,6 +38,9 @@ type Paths [4]string
3738 // clipped to a CellW x CellH box. It is used from one goroutine.
3839 type Face struct {
3940 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
4044 // CellW and CellH are the cell size in physical pixels.
4145 CellW, CellH int
4246 ascent fixed.Int26_6
@@ -74,16 +78,35 @@ func FamilyOf(request string) string {
7478 return strings.TrimSpace(string(out))
7579 }
7680
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)
79101 if err != nil {
80102 return nil, fmt.Errorf("glyph: %w", err)
81103 }
82- f, err := opentype.Parse(data)
104+ f.maps = append(f.maps, data)
105+ fnt, err := opentype.Parse(data)
83106 if err != nil {
84107 return nil, fmt.Errorf("glyph: parse %s: %w", path, err)
85108 }
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})
87110 }
88111
89112 // 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) {
105128 f.faces[i] = face
106129 continue
107130 }
108- face, err := loadFace(path, px)
131+ face, err := f.loadFace(path, px)
109132 if err != nil {
110133 f.Close()
111134 return nil, err
@@ -144,7 +167,8 @@ func Load(paths Paths, px float64) (*Face, error) {
144167 return f, nil
145168 }
146169
147-// Close releases the font faces.
170+// Close releases the font faces and unmaps their files. The face must not be
171+// used afterwards.
148172 func (f *Face) Close() {
149173 if f == nil {
150174 return
@@ -157,6 +181,10 @@ func (f *Face) Close() {
157181 }
158182 f.faces[i] = nil
159183 }
184+ for _, m := range f.maps {
185+ _ = unix.Munmap(m)
186+ }
187+ f.maps = nil
160188 }
161189
162190 // 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.Face40 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 int45 CellW, CellH int
42 ascent fixed.Int26_646 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 regular112 // 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] = face128 f.faces[i] = face
106 continue129 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, err134 return nil, err
@@ -144,7 +167,8 @@ func Load(paths Paths, px float64) (*Face, error) {
144 return f, nil167 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 return174 return
@@ -157,6 +181,10 @@ func (f *Face) Close() {
157 }181 }
158 f.faces[i] = nil182 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 (stride190 // Rasterize draws r into dst, which must hold CellW*CellH bytes (stride

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close