perf(favicon): defer PNG save to idle callback to avoid UI blocking
- commit
- be07ff20e6e1abddaa7d536bbfa9b263c0a4c169
- author
- bnema · · 2026-01-06 19:13 UTC
- parent
- 13a022b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +30 -18
M internal/ui/adapter/favicon.go +30 -18
| @@ -192,33 +192,45 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin( | |||
| 192 | 192 | } | |
| 193 | 193 | ||
| 194 | 194 | // saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated. | |
| 195 | + | // Uses glib.IdleAdd to defer disk I/O until GTK main loop is idle, avoiding UI blocking. | |
| 195 | 196 | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | |
| 196 | 197 | log := logging.FromContext(ctx) | |
| 197 | 198 | ||
| 198 | - | // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread | |
| 199 | - | // since GTK texture operations need to happen there | |
| 199 | + | // Check if PNG save is needed before scheduling idle callback | |
| 200 | 200 | pngPath := a.service.DiskPathPNG(domain) | |
| 201 | - | if pngPath != "" && !a.service.HasPNGOnDisk(domain) { | |
| 202 | - | // Ensure cache directory exists before SaveToPng (GTK won't create it) | |
| 201 | + | needsPNGSave := pngPath != "" && !a.service.HasPNGOnDisk(domain) | |
| 202 | + | ||
| 203 | + | if needsPNGSave { | |
| 204 | + | // Ensure cache directory exists (cheap check, do it now) | |
| 203 | 205 | if err := a.service.EnsureCacheDir(); err != nil { | |
| 204 | 206 | log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir") | |
| 205 | - | } else if ok := texture.SaveToPng(pngPath); !ok { | |
| 206 | - | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |
| 207 | - | } else { | |
| 208 | - | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |
| 209 | - | ||
| 210 | - | // Create normalized sized copy for dmenu/fuzzel (async) | |
| 211 | - | // The original PNG is now on disk, so this should succeed | |
| 212 | - | go func() { | |
| 213 | - | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |
| 214 | - | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG") | |
| 215 | - | } else { | |
| 216 | - | log.Debug().Str("domain", domain).Msg("created sized PNG") | |
| 217 | - | } | |
| 218 | - | }() | |
| 207 | + | needsPNGSave = false | |
| 219 | 208 | } | |
| 220 | 209 | } | |
| 221 | 210 | ||
| 211 | + | if needsPNGSave { | |
| 212 | + | // Schedule PNG save for when GTK main loop is idle (non-blocking) | |
| 213 | + | // texture.SaveToPng must run on main thread but we defer it to avoid blocking | |
| 214 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 215 | + | if ok := texture.SaveToPng(pngPath); !ok { | |
| 216 | + | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |
| 217 | + | } else { | |
| 218 | + | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |
| 219 | + | ||
| 220 | + | // Create normalized sized copy for dmenu/fuzzel (async) | |
| 221 | + | go func() { | |
| 222 | + | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |
| 223 | + | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG") | |
| 224 | + | } else { | |
| 225 | + | log.Debug().Str("domain", domain).Msg("created sized PNG") | |
| 226 | + | } | |
| 227 | + | }() | |
| 228 | + | } | |
| 229 | + | return false // don't repeat | |
| 230 | + | }) | |
| 231 | + | glib.IdleAdd(&cb, 0) | |
| 232 | + | } | |
| 233 | + | ||
| 222 | 234 | // Ensure disk cache is populated (async, in background) | |
| 223 | 235 | go a.service.EnsureDiskCache(ctx, domain) | |
| 224 | 236 | } | |
| @@ -192,33 +192,45 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin( | |||||
| 192 | } | 192 | } | ||
| 193 | 193 | ||||
| 194 | // saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated. | 194 | // saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated. | ||
| 195 | + | // Uses glib.IdleAdd to defer disk I/O until GTK main loop is idle, avoiding UI blocking. | |||
| 195 | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | 196 | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | ||
| 196 | log := logging.FromContext(ctx) | 197 | log := logging.FromContext(ctx) | ||
| 197 | 198 | ||||
| 198 | - | // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread | 199 | + | // Check if PNG save is needed before scheduling idle callback |
| 199 | - | // since GTK texture operations need to happen there | |||
| 200 | pngPath := a.service.DiskPathPNG(domain) | 200 | pngPath := a.service.DiskPathPNG(domain) | ||
| 201 | - | if pngPath != "" && !a.service.HasPNGOnDisk(domain) { | 201 | + | needsPNGSave := pngPath != "" && !a.service.HasPNGOnDisk(domain) |
| 202 | - | // Ensure cache directory exists before SaveToPng (GTK won't create it) | 202 | + | |
| 203 | + | if needsPNGSave { | |||
| 204 | + | // Ensure cache directory exists (cheap check, do it now) | |||
| 203 | if err := a.service.EnsureCacheDir(); err != nil { | 205 | if err := a.service.EnsureCacheDir(); err != nil { | ||
| 204 | log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir") | 206 | log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir") | ||
| 205 | - | } else if ok := texture.SaveToPng(pngPath); !ok { | 207 | + | needsPNGSave = false |
| 206 | - | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |||
| 207 | - | } else { | |||
| 208 | - | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |||
| 209 | - | ||||
| 210 | - | // Create normalized sized copy for dmenu/fuzzel (async) | |||
| 211 | - | // The original PNG is now on disk, so this should succeed | |||
| 212 | - | go func() { | |||
| 213 | - | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |||
| 214 | - | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG") | |||
| 215 | - | } else { | |||
| 216 | - | log.Debug().Str("domain", domain).Msg("created sized PNG") | |||
| 217 | - | } | |||
| 218 | - | }() | |||
| 219 | } | 208 | } | ||
| 220 | } | 209 | } | ||
| 221 | 210 | ||||
| 211 | + | if needsPNGSave { | |||
| 212 | + | // Schedule PNG save for when GTK main loop is idle (non-blocking) | |||
| 213 | + | // texture.SaveToPng must run on main thread but we defer it to avoid blocking | |||
| 214 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 215 | + | if ok := texture.SaveToPng(pngPath); !ok { | |||
| 216 | + | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |||
| 217 | + | } else { | |||
| 218 | + | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |||
| 219 | + | ||||
| 220 | + | // Create normalized sized copy for dmenu/fuzzel (async) | |||
| 221 | + | go func() { | |||
| 222 | + | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |||
| 223 | + | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG") | |||
| 224 | + | } else { | |||
| 225 | + | log.Debug().Str("domain", domain).Msg("created sized PNG") | |||
| 226 | + | } | |||
| 227 | + | }() | |||
| 228 | + | } | |||
| 229 | + | return false // don't repeat | |||
| 230 | + | }) | |||
| 231 | + | glib.IdleAdd(&cb, 0) | |||
| 232 | + | } | |||
| 233 | + | ||||
| 222 | // Ensure disk cache is populated (async, in background) | 234 | // Ensure disk cache is populated (async, in background) | ||
| 223 | go a.service.EnsureDiskCache(ctx, domain) | 235 | go a.service.EnsureDiskCache(ctx, domain) | ||
| 224 | } | 236 | } | ||