detached 9c553cf

Merge pull request 'feat(bar): NEFERBAR_PPROF and mapped font files' (#1) from feat/pprof-env into main

Reviewed-on: https://git.bnema.dev/bnema/neferbar/pulls/1
commit
9c553cf1f36cea61fbc2ad566393ae6d759ab24c
author
bnema · · 2026-10-08 05:39 UTC
merge of
cfe73b7 + 768bf78 · diff against the first parent
signature
unsigned
3 files · +36 -8
M README.md +1 -1
@@ -231,7 +231,7 @@ neferbar [-config file] [-display socket] [-pprof addr] [-memstats interval]
231231 |---|---|
232232 | `-config` | Config file. Default: `$XDG_CONFIG_HOME/neferbar/config.toml`. |
233233 | `-display` | Wayland socket name or path. Default: `$NEFERBAR_DISPLAY`, then `$WAYLAND_DISPLAY`. |
234-| `-pprof` | Serve Go profiling on a loopback address, such as `localhost:6060`. |
234+| `-pprof` | Serve Go profiling on a loopback address, such as `localhost:6060`. Default: `$NEFERBAR_PPROF`, so a bar started by the compositor can be profiled. |
235235 | `-memstats` | Log allocation counters at this interval. |
236236
237237 ## Reading the focused window
@@ -231,7 +231,7 @@ neferbar [-config file] [-display socket] [-pprof addr] [-memstats interval]
231 |---|---|231 |---|---|
232 | `-config` | Config file. Default: `$XDG_CONFIG_HOME/neferbar/config.toml`. |232 | `-config` | Config file. Default: `$XDG_CONFIG_HOME/neferbar/config.toml`. |
233 | `-display` | Wayland socket name or path. Default: `$NEFERBAR_DISPLAY`, then `$WAYLAND_DISPLAY`. |233 | `-display` | Wayland socket name or path. Default: `$NEFERBAR_DISPLAY`, then `$WAYLAND_DISPLAY`. |
234-| `-pprof` | Serve Go profiling on a loopback address, such as `localhost:6060`. |234+| `-pprof` | Serve Go profiling on a loopback address, such as `localhost:6060`. Default: `$NEFERBAR_PPROF`, so a bar started by the compositor can be profiled. |
235 | `-memstats` | Log allocation counters at this interval. |235 | `-memstats` | Log allocation counters at this interval. |
236 236
237 ## Reading the focused window237 ## Reading the focused window
M cmd/neferbar/main.go +1 -1
@@ -33,7 +33,7 @@ func main() {
3333 }
3434 cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)")
3535 display := flag.String("display", os.Getenv("NEFERBAR_DISPLAY"), "Wayland socket name or absolute path (default: $NEFERBAR_DISPLAY, then $WAYLAND_DISPLAY)")
36- pprofAddr := flag.String("pprof", "", "serve pprof on this loopback address, e.g. localhost:6060")
36+ pprofAddr := flag.String("pprof", os.Getenv("NEFERBAR_PPROF"), "serve pprof on this loopback address, e.g. localhost:6060 (default: $NEFERBAR_PPROF)")
3737 memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)")
3838 flag.Parse()
3939
@@ -33,7 +33,7 @@ func main() {
33 }33 }
34 cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)")34 cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)")
35 display := flag.String("display", os.Getenv("NEFERBAR_DISPLAY"), "Wayland socket name or absolute path (default: $NEFERBAR_DISPLAY, then $WAYLAND_DISPLAY)")35 display := flag.String("display", os.Getenv("NEFERBAR_DISPLAY"), "Wayland socket name or absolute path (default: $NEFERBAR_DISPLAY, then $WAYLAND_DISPLAY)")
36- pprofAddr := flag.String("pprof", "", "serve pprof on this loopback address, e.g. localhost:6060")36+ pprofAddr := flag.String("pprof", os.Getenv("NEFERBAR_PPROF"), "serve pprof on this loopback address, e.g. localhost:6060 (default: $NEFERBAR_PPROF)")
37 memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)")37 memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)")
38 flag.Parse()38 flag.Parse()
39 39
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