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
3 files · +36 -8
M README.md +1 -1
| @@ -231,7 +231,7 @@ neferbar [-config file] [-display socket] [-pprof addr] [-memstats interval] | |||
| 231 | 231 | |---|---| | |
| 232 | 232 | | `-config` | Config file. Default: `$XDG_CONFIG_HOME/neferbar/config.toml`. | | |
| 233 | 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 | 235 | | `-memstats` | Log allocation counters at this interval. | | |
| 236 | 236 | ||
| 237 | 237 | ## 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 window | 237 | ## Reading the focused window | ||
M cmd/neferbar/main.go +1 -1
| @@ -33,7 +33,7 @@ func main() { | |||
| 33 | 33 | } | |
| 34 | 34 | cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)") | |
| 35 | 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 | 37 | memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)") | |
| 38 | 38 | flag.Parse() | |
| 39 | 39 | ||
| @@ -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 ( | |||
| 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 | ||