perf(threading): move UI operations off GTK main thread to eliminate hitches
- commit
- 8da98d28e115d98a221594113e7215e50274ffd0
- author
- bamen · · 2025-09-16 19:27 UTC
- committer
- bamen · · 2025-09-16 17:19 UTC
- parent
- a732180
- signature
- unsigned
3 files · +86 -41
M internal/app/browser/webview.go +14 -3
| @@ -66,6 +66,7 @@ func (app *BrowserApp) createWebView() error { | |||
| 66 | 66 | DefaultMonospaceFont: app.config.Appearance.MonospaceFont, | |
| 67 | 67 | DefaultFontSize: app.config.Appearance.DefaultFontSize, | |
| 68 | 68 | Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)}, | |
| 69 | + | UseDomZoom: app.config.UseDomZoom, | |
| 69 | 70 | VideoAcceleration: webkit.VideoAccelerationConfig{ | |
| 70 | 71 | EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI, | |
| 71 | 72 | AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU, | |
| @@ -123,13 +124,19 @@ func (app *BrowserApp) setupWebViewIntegration() { | |||
| 123 | 124 | func (app *BrowserApp) setupWebViewHandlers() { | |
| 124 | 125 | // Persist page titles to DB when they change | |
| 125 | 126 | app.webView.RegisterTitleChangedHandler(func(title string) { | |
| 126 | - | ctx := context.Background() | |
| 127 | + | if title == "" { | |
| 128 | + | return | |
| 129 | + | } | |
| 127 | 130 | url := app.webView.GetCurrentURL() | |
| 128 | - | if url != "" && title != "" { | |
| 131 | + | if url == "" { | |
| 132 | + | return | |
| 133 | + | } | |
| 134 | + | go func(url, title string) { | |
| 135 | + | ctx := context.Background() | |
| 129 | 136 | if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil { | |
| 130 | 137 | log.Printf("Warning: failed to update page title: %v", err) | |
| 131 | 138 | } | |
| 132 | - | } | |
| 139 | + | }(url, title) | |
| 133 | 140 | }) | |
| 134 | 141 | ||
| 135 | 142 | // Set WebView reference for message handler and register script messages | |
| @@ -150,6 +157,10 @@ func (app *BrowserApp) setupControllers() { | |||
| 150 | 157 | ) | |
| 151 | 158 | app.clipboardController = control.NewClipboardController(app.webView) | |
| 152 | 159 | ||
| 160 | + | if app.messageHandler != nil && app.navigationController != nil { | |
| 161 | + | app.messageHandler.SetNavigationController(app.navigationController) | |
| 162 | + | } | |
| 163 | + | ||
| 153 | 164 | // Register controller handlers | |
| 154 | 165 | app.zoomController.RegisterHandlers() | |
| 155 | 166 | } | |
| @@ -66,6 +66,7 @@ func (app *BrowserApp) createWebView() error { | |||||
| 66 | DefaultMonospaceFont: app.config.Appearance.MonospaceFont, | 66 | DefaultMonospaceFont: app.config.Appearance.MonospaceFont, | ||
| 67 | DefaultFontSize: app.config.Appearance.DefaultFontSize, | 67 | DefaultFontSize: app.config.Appearance.DefaultFontSize, | ||
| 68 | Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)}, | 68 | Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)}, | ||
| 69 | + | UseDomZoom: app.config.UseDomZoom, | |||
| 69 | VideoAcceleration: webkit.VideoAccelerationConfig{ | 70 | VideoAcceleration: webkit.VideoAccelerationConfig{ | ||
| 70 | EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI, | 71 | EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI, | ||
| 71 | AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU, | 72 | AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU, | ||
| @@ -123,13 +124,19 @@ func (app *BrowserApp) setupWebViewIntegration() { | |||||
| 123 | func (app *BrowserApp) setupWebViewHandlers() { | 124 | func (app *BrowserApp) setupWebViewHandlers() { | ||
| 124 | // Persist page titles to DB when they change | 125 | // Persist page titles to DB when they change | ||
| 125 | app.webView.RegisterTitleChangedHandler(func(title string) { | 126 | app.webView.RegisterTitleChangedHandler(func(title string) { | ||
| 126 | - | ctx := context.Background() | 127 | + | if title == "" { |
| 128 | + | return | |||
| 129 | + | } | |||
| 127 | url := app.webView.GetCurrentURL() | 130 | url := app.webView.GetCurrentURL() | ||
| 128 | - | if url != "" && title != "" { | 131 | + | if url == "" { |
| 132 | + | return | |||
| 133 | + | } | |||
| 134 | + | go func(url, title string) { | |||
| 135 | + | ctx := context.Background() | |||
| 129 | if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil { | 136 | if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil { | ||
| 130 | log.Printf("Warning: failed to update page title: %v", err) | 137 | log.Printf("Warning: failed to update page title: %v", err) | ||
| 131 | } | 138 | } | ||
| 132 | - | } | 139 | + | }(url, title) |
| 133 | }) | 140 | }) | ||
| 134 | 141 | ||||
| 135 | // Set WebView reference for message handler and register script messages | 142 | // Set WebView reference for message handler and register script messages | ||
| @@ -150,6 +157,10 @@ func (app *BrowserApp) setupControllers() { | |||||
| 150 | ) | 157 | ) | ||
| 151 | app.clipboardController = control.NewClipboardController(app.webView) | 158 | app.clipboardController = control.NewClipboardController(app.webView) | ||
| 152 | 159 | ||||
| 160 | + | if app.messageHandler != nil && app.navigationController != nil { | |||
| 161 | + | app.messageHandler.SetNavigationController(app.navigationController) | |||
| 162 | + | } | |||
| 163 | + | ||||
| 153 | // Register controller handlers | 164 | // Register controller handlers | ||
| 154 | app.zoomController.RegisterHandlers() | 165 | app.zoomController.RegisterHandlers() | ||
| 155 | } | 166 | } | ||
M internal/app/control/zoom.go +64 -37
| @@ -48,22 +48,8 @@ func (z *ZoomController) handleURIChange(url string) { | |||
| 48 | 48 | if url == "" { | |
| 49 | 49 | return | |
| 50 | 50 | } | |
| 51 | - | ctx := context.Background() | |
| 52 | 51 | currentDomain := services.ZoomKeyForLog(url) | |
| 53 | - | ||
| 54 | - | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | |
| 55 | - | z.programmaticChange = true | |
| 56 | - | if err := z.webView.SetZoom(zoomLevel); err == nil { | |
| 57 | - | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, currentDomain) | |
| 58 | - | ||
| 59 | - | // Only show toast when entering a new domain (not on first load) | |
| 60 | - | if currentDomain != z.lastZoomDomain && z.lastZoomDomain != "" { | |
| 61 | - | z.showZoomToast(zoomLevel) | |
| 62 | - | } | |
| 63 | - | z.lastZoomDomain = currentDomain | |
| 64 | - | } | |
| 65 | - | z.programmaticChange = false | |
| 66 | - | } | |
| 52 | + | z.loadZoomLevelAsync(url, currentDomain, true) | |
| 67 | 53 | } | |
| 68 | 54 | ||
| 69 | 55 | // handleZoomChange responds to zoom level changes and persists them | |
| @@ -72,13 +58,15 @@ func (z *ZoomController) handleZoomChange(level float64) { | |||
| 72 | 58 | if url == "" { | |
| 73 | 59 | return | |
| 74 | 60 | } | |
| 75 | - | ctx := context.Background() | |
| 76 | - | if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil { | |
| 77 | - | log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err) | |
| 78 | - | return | |
| 79 | - | } | |
| 80 | - | key := services.ZoomKeyForLog(url) | |
| 81 | - | log.Printf("[zoom] saved %.2f for %s", level, key) | |
| 61 | + | go func(url string, level float64) { | |
| 62 | + | ctx := context.Background() | |
| 63 | + | if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil { | |
| 64 | + | log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err) | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | key := services.ZoomKeyForLog(url) | |
| 68 | + | log.Printf("[zoom] saved %.2f for %s", level, key) | |
| 69 | + | }(url, level) | |
| 82 | 70 | ||
| 83 | 71 | // Only show toast for user-initiated zoom changes, not programmatic ones | |
| 84 | 72 | if !z.programmaticChange { | |
| @@ -144,28 +132,67 @@ func (z *ZoomController) ApplyInitialZoom() { | |||
| 144 | 132 | url = "dumb://homepage" | |
| 145 | 133 | } | |
| 146 | 134 | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | |
| 147 | - | z.programmaticChange = true | |
| 148 | - | if err := z.webView.SetZoom(zoomLevel); err != nil { | |
| 149 | - | log.Printf("Warning: failed to set initial zoom: %v", err) | |
| 150 | - | } else { | |
| 151 | - | key := services.ZoomKeyForLog(url) | |
| 152 | - | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key) | |
| 153 | - | } | |
| 154 | - | z.programmaticChange = false | |
| 135 | + | z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, false) | |
| 155 | 136 | } | |
| 156 | 137 | } | |
| 157 | 138 | ||
| 158 | 139 | // ApplyZoomForURL applies zoom for a specific URL (used for navigation) | |
| 159 | 140 | func (z *ZoomController) ApplyZoomForURL(url string) { | |
| 160 | - | ctx := context.Background() | |
| 161 | - | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | |
| 141 | + | if url == "" { | |
| 142 | + | return | |
| 143 | + | } | |
| 144 | + | currentDomain := services.ZoomKeyForLog(url) | |
| 145 | + | z.loadZoomLevelAsync(url, currentDomain, false) | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // ApplyZoomForURLWithLevel applies a known zoom level without hitting the database again. | |
| 149 | + | func (z *ZoomController) ApplyZoomForURLWithLevel(url string, zoomLevel float64, allowToast bool) { | |
| 150 | + | if z == nil || url == "" { | |
| 151 | + | return | |
| 152 | + | } | |
| 153 | + | z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, allowToast) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (z *ZoomController) loadZoomLevelAsync(url, domain string, allowToast bool) { | |
| 157 | + | if z.browserService == nil || z.webView == nil || url == "" { | |
| 158 | + | return | |
| 159 | + | } | |
| 160 | + | go func(url, domain string, allowToast bool) { | |
| 161 | + | ctx := context.Background() | |
| 162 | + | zoomLevel, err := z.browserService.GetZoomLevel(ctx, url) | |
| 163 | + | if err != nil { | |
| 164 | + | return | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | z.applyZoomLevel(url, domain, zoomLevel, allowToast) | |
| 168 | + | }(url, domain, allowToast) | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, allowToast bool) { | |
| 172 | + | if z == nil || z.webView == nil || url == "" { | |
| 173 | + | return | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | z.webView.RunOnMainThread(func() { | |
| 177 | + | if z.webView == nil { | |
| 178 | + | return | |
| 179 | + | } | |
| 180 | + | if z.currentURL != url { | |
| 181 | + | z.currentURL = url | |
| 182 | + | } | |
| 183 | + | ||
| 162 | 184 | z.programmaticChange = true | |
| 185 | + | defer func() { z.programmaticChange = false }() | |
| 186 | + | ||
| 163 | 187 | if err := z.webView.SetZoom(zoomLevel); err != nil { | |
| 164 | 188 | log.Printf("Warning: failed to set zoom: %v", err) | |
| 165 | - | } else { | |
| 166 | - | key := services.ZoomKeyForLog(url) | |
| 167 | - | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key) | |
| 189 | + | return | |
| 168 | 190 | } | |
| 169 | - | z.programmaticChange = false | |
| 170 | - | } | |
| 191 | + | ||
| 192 | + | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain) | |
| 193 | + | if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" { | |
| 194 | + | z.showZoomToast(zoomLevel) | |
| 195 | + | } | |
| 196 | + | z.lastZoomDomain = domain | |
| 197 | + | }) | |
| 171 | 198 | } | |
| @@ -48,22 +48,8 @@ func (z *ZoomController) handleURIChange(url string) { | |||||
| 48 | if url == "" { | 48 | if url == "" { | ||
| 49 | return | 49 | return | ||
| 50 | } | 50 | } | ||
| 51 | - | ctx := context.Background() | |||
| 52 | currentDomain := services.ZoomKeyForLog(url) | 51 | currentDomain := services.ZoomKeyForLog(url) | ||
| 53 | - | 52 | + | z.loadZoomLevelAsync(url, currentDomain, true) | |
| 54 | - | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | |||
| 55 | - | z.programmaticChange = true | |||
| 56 | - | if err := z.webView.SetZoom(zoomLevel); err == nil { | |||
| 57 | - | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, currentDomain) | |||
| 58 | - | ||||
| 59 | - | // Only show toast when entering a new domain (not on first load) | |||
| 60 | - | if currentDomain != z.lastZoomDomain && z.lastZoomDomain != "" { | |||
| 61 | - | z.showZoomToast(zoomLevel) | |||
| 62 | - | } | |||
| 63 | - | z.lastZoomDomain = currentDomain | |||
| 64 | - | } | |||
| 65 | - | z.programmaticChange = false | |||
| 66 | - | } | |||
| 67 | } | 53 | } | ||
| 68 | 54 | ||||
| 69 | // handleZoomChange responds to zoom level changes and persists them | 55 | // handleZoomChange responds to zoom level changes and persists them | ||
| @@ -72,13 +58,15 @@ func (z *ZoomController) handleZoomChange(level float64) { | |||||
| 72 | if url == "" { | 58 | if url == "" { | ||
| 73 | return | 59 | return | ||
| 74 | } | 60 | } | ||
| 75 | - | ctx := context.Background() | 61 | + | go func(url string, level float64) { |
| 76 | - | if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil { | 62 | + | ctx := context.Background() |
| 77 | - | log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err) | 63 | + | if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil { |
| 78 | - | return | 64 | + | log.Printf("[zoom] failed to save level %.2f for %s: %v", level, url, err) |
| 79 | - | } | 65 | + | return |
| 80 | - | key := services.ZoomKeyForLog(url) | 66 | + | } |
| 81 | - | log.Printf("[zoom] saved %.2f for %s", level, key) | 67 | + | key := services.ZoomKeyForLog(url) |
| 68 | + | log.Printf("[zoom] saved %.2f for %s", level, key) | |||
| 69 | + | }(url, level) | |||
| 82 | 70 | ||||
| 83 | // Only show toast for user-initiated zoom changes, not programmatic ones | 71 | // Only show toast for user-initiated zoom changes, not programmatic ones | ||
| 84 | if !z.programmaticChange { | 72 | if !z.programmaticChange { | ||
| @@ -144,28 +132,67 @@ func (z *ZoomController) ApplyInitialZoom() { | |||||
| 144 | url = "dumb://homepage" | 132 | url = "dumb://homepage" | ||
| 145 | } | 133 | } | ||
| 146 | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | 134 | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | ||
| 147 | - | z.programmaticChange = true | 135 | + | z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, false) |
| 148 | - | if err := z.webView.SetZoom(zoomLevel); err != nil { | |||
| 149 | - | log.Printf("Warning: failed to set initial zoom: %v", err) | |||
| 150 | - | } else { | |||
| 151 | - | key := services.ZoomKeyForLog(url) | |||
| 152 | - | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key) | |||
| 153 | - | } | |||
| 154 | - | z.programmaticChange = false | |||
| 155 | } | 136 | } | ||
| 156 | } | 137 | } | ||
| 157 | 138 | ||||
| 158 | // ApplyZoomForURL applies zoom for a specific URL (used for navigation) | 139 | // ApplyZoomForURL applies zoom for a specific URL (used for navigation) | ||
| 159 | func (z *ZoomController) ApplyZoomForURL(url string) { | 140 | func (z *ZoomController) ApplyZoomForURL(url string) { | ||
| 160 | - | ctx := context.Background() | 141 | + | if url == "" { |
| 161 | - | if zoomLevel, err := z.browserService.GetZoomLevel(ctx, url); err == nil { | 142 | + | return |
| 143 | + | } | |||
| 144 | + | currentDomain := services.ZoomKeyForLog(url) | |||
| 145 | + | z.loadZoomLevelAsync(url, currentDomain, false) | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | // ApplyZoomForURLWithLevel applies a known zoom level without hitting the database again. | |||
| 149 | + | func (z *ZoomController) ApplyZoomForURLWithLevel(url string, zoomLevel float64, allowToast bool) { | |||
| 150 | + | if z == nil || url == "" { | |||
| 151 | + | return | |||
| 152 | + | } | |||
| 153 | + | z.applyZoomLevel(url, services.ZoomKeyForLog(url), zoomLevel, allowToast) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func (z *ZoomController) loadZoomLevelAsync(url, domain string, allowToast bool) { | |||
| 157 | + | if z.browserService == nil || z.webView == nil || url == "" { | |||
| 158 | + | return | |||
| 159 | + | } | |||
| 160 | + | go func(url, domain string, allowToast bool) { | |||
| 161 | + | ctx := context.Background() | |||
| 162 | + | zoomLevel, err := z.browserService.GetZoomLevel(ctx, url) | |||
| 163 | + | if err != nil { | |||
| 164 | + | return | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | z.applyZoomLevel(url, domain, zoomLevel, allowToast) | |||
| 168 | + | }(url, domain, allowToast) | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func (z *ZoomController) applyZoomLevel(url, domain string, zoomLevel float64, allowToast bool) { | |||
| 172 | + | if z == nil || z.webView == nil || url == "" { | |||
| 173 | + | return | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | z.webView.RunOnMainThread(func() { | |||
| 177 | + | if z.webView == nil { | |||
| 178 | + | return | |||
| 179 | + | } | |||
| 180 | + | if z.currentURL != url { | |||
| 181 | + | z.currentURL = url | |||
| 182 | + | } | |||
| 183 | + | ||||
| 162 | z.programmaticChange = true | 184 | z.programmaticChange = true | ||
| 185 | + | defer func() { z.programmaticChange = false }() | |||
| 186 | + | ||||
| 163 | if err := z.webView.SetZoom(zoomLevel); err != nil { | 187 | if err := z.webView.SetZoom(zoomLevel); err != nil { | ||
| 164 | log.Printf("Warning: failed to set zoom: %v", err) | 188 | log.Printf("Warning: failed to set zoom: %v", err) | ||
| 165 | - | } else { | 189 | + | return |
| 166 | - | key := services.ZoomKeyForLog(url) | |||
| 167 | - | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, key) | |||
| 168 | } | 190 | } | ||
| 169 | - | z.programmaticChange = false | 191 | + | |
| 170 | - | } | 192 | + | log.Printf("[zoom] loaded %.2f for %s", zoomLevel, domain) |
| 193 | + | if allowToast && domain != z.lastZoomDomain && z.lastZoomDomain != "" { | |||
| 194 | + | z.showZoomToast(zoomLevel) | |||
| 195 | + | } | |||
| 196 | + | z.lastZoomDomain = domain | |||
| 197 | + | }) | |||
| 171 | } | 198 | } | ||
M services/browser_service.go +8 -1
| @@ -215,7 +215,14 @@ func (s *BrowserService) UpdatePageTitle(ctx context.Context, url, title string) | |||
| 215 | 215 | // Update the window title if we have a title updater | |
| 216 | 216 | if s.windowTitleUpdater != nil && title != "" { | |
| 217 | 217 | windowTitle := fmt.Sprintf("Dumber - %s", title) | |
| 218 | - | s.windowTitleUpdater.SetTitle(windowTitle) | |
| 218 | + | updater := s.windowTitleUpdater | |
| 219 | + | if s.webView != nil { | |
| 220 | + | s.webView.RunOnMainThread(func() { | |
| 221 | + | updater.SetTitle(windowTitle) | |
| 222 | + | }) | |
| 223 | + | } else { | |
| 224 | + | updater.SetTitle(windowTitle) | |
| 225 | + | } | |
| 219 | 226 | } | |
| 220 | 227 | ||
| 221 | 228 | return nil | |
| @@ -215,7 +215,14 @@ func (s *BrowserService) UpdatePageTitle(ctx context.Context, url, title string) | |||||
| 215 | // Update the window title if we have a title updater | 215 | // Update the window title if we have a title updater | ||
| 216 | if s.windowTitleUpdater != nil && title != "" { | 216 | if s.windowTitleUpdater != nil && title != "" { | ||
| 217 | windowTitle := fmt.Sprintf("Dumber - %s", title) | 217 | windowTitle := fmt.Sprintf("Dumber - %s", title) | ||
| 218 | - | s.windowTitleUpdater.SetTitle(windowTitle) | 218 | + | updater := s.windowTitleUpdater |
| 219 | + | if s.webView != nil { | |||
| 220 | + | s.webView.RunOnMainThread(func() { | |||
| 221 | + | updater.SetTitle(windowTitle) | |||
| 222 | + | }) | |||
| 223 | + | } else { | |||
| 224 | + | updater.SetTitle(windowTitle) | |||
| 225 | + | } | |||
| 219 | } | 226 | } | ||
| 220 | 227 | ||||
| 221 | return nil | 228 | return nil | ||