detached e6cc2c8

feat(webkit): implement web process memory pressure settings

commit
e6cc2c80e6cc20d2258ea081f2044a848f3acea8
author
brice · · 2025-12-29 17:07 UTC
committer
brice · · 2025-12-29 17:28 UTC
parent
6b96161
signature
unsigned
3 files · +70 -56
M internal/infrastructure/webkit/context.go +19 -30
@@ -74,21 +74,29 @@ func NewWebKitContextWithOptions(ctx context.Context, opts port.WebKitContextOpt
7474 return nil, fmt.Errorf("failed to init network session: %w", err)
7575 }
7676
77- // WebViews in this codebase are created via WebKit's default WebContext.
78- // If we create a separate WebContext here and only register our custom
79- // dumb:// scheme on it, internal pages will break with "The URL can't be shown".
80- //
81- // Additionally, WebKit's "memory-pressure-settings" is a construct-only property,
82- // so setting it after `webkit.NewWebContext()` triggers a GLib critical and has no effect.
83- // Until we can reliably construct a WebContext with memory-pressure-settings AND ensure
84- // all WebViews use that context, we intentionally stick to the default WebContext.
77+ // Create WebContext - use custom constructor if memory pressure settings are configured
8578 if opts.IsWebProcessMemoryConfigured() {
86- log.Warn().Msg("web process memory pressure settings requested but not applied (construct-only WebContext property; would break dumb:// scheme registration)")
79+ memSettings := buildMemoryPressureSettings(opts.WebProcessMemory)
80+ if memSettings != nil {
81+ wkCtx.webContext = webkit.NewWebContextWithMemoryPressureSettings(memSettings)
82+ if wkCtx.webContext != nil {
83+ log.Info().
84+ Int("limit_mb", opts.WebProcessMemory.MemoryLimitMB).
85+ Float64("poll_sec", opts.WebProcessMemory.PollIntervalSec).
86+ Float64("conservative", opts.WebProcessMemory.ConservativeThreshold).
87+ Float64("strict", opts.WebProcessMemory.StrictThreshold).
88+ Float64("kill", opts.WebProcessMemory.KillThreshold).
89+ Msg("created WebContext with memory pressure settings")
90+ }
91+ }
8792 }
8893
89- wkCtx.webContext = webkit.WebContextGetDefault()
94+ // Fall back to default WebContext if custom creation failed or wasn't needed
95+ if wkCtx.webContext == nil {
96+ wkCtx.webContext = webkit.WebContextGetDefault()
97+ }
9098 if wkCtx.webContext == nil {
91- return nil, fmt.Errorf("failed to get default WebContext")
99+ return nil, fmt.Errorf("failed to create or get WebContext")
92100 }
93101
94102 // Set cache model for browser-style caching
@@ -103,25 +111,6 @@ func NewWebKitContextWithOptions(ctx context.Context, opts port.WebKitContextOpt
103111 return wkCtx, nil
104112 }
105113
106-// createWebContextWithMemorySettings creates a WebContext with memory-pressure-settings.
107-// The memory-pressure-settings property is construct-only, so we need to set it at creation time.
108-func createWebContextWithMemorySettings(settings *webkit.MemoryPressureSettings) *webkit.WebContext {
109- if settings == nil {
110- return nil
111- }
112-
113- // Create a new WebContext and set memory pressure settings
114- // Note: memory-pressure-settings is construct-only, but we try setting it via property
115- // as an alternative to g_object_new_with_properties which may not work reliably
116- ctx := webkit.NewWebContext()
117- if ctx != nil {
118- // Try to set the property - this may not work for construct-only properties
119- // but it's the best we can do without lower-level GObject access
120- ctx.SetPropertyMemoryPressureSettings(settings.GoPointer())
121- }
122- return ctx
123-}
124-
125114 // initNetworkSession creates and configures the persistent network session.
126115 func (c *WebKitContext) initNetworkSession() error {
127116 // Create persistent network session
@@ -74,21 +74,29 @@ func NewWebKitContextWithOptions(ctx context.Context, opts port.WebKitContextOpt
74 return nil, fmt.Errorf("failed to init network session: %w", err)74 return nil, fmt.Errorf("failed to init network session: %w", err)
75 }75 }
76 76
77- // WebViews in this codebase are created via WebKit's default WebContext.77+ // Create WebContext - use custom constructor if memory pressure settings are configured
78- // If we create a separate WebContext here and only register our custom
79- // dumb:// scheme on it, internal pages will break with "The URL can't be shown".
80- //
81- // Additionally, WebKit's "memory-pressure-settings" is a construct-only property,
82- // so setting it after `webkit.NewWebContext()` triggers a GLib critical and has no effect.
83- // Until we can reliably construct a WebContext with memory-pressure-settings AND ensure
84- // all WebViews use that context, we intentionally stick to the default WebContext.
85 if opts.IsWebProcessMemoryConfigured() {78 if opts.IsWebProcessMemoryConfigured() {
86- log.Warn().Msg("web process memory pressure settings requested but not applied (construct-only WebContext property; would break dumb:// scheme registration)")79+ memSettings := buildMemoryPressureSettings(opts.WebProcessMemory)
80+ if memSettings != nil {
81+ wkCtx.webContext = webkit.NewWebContextWithMemoryPressureSettings(memSettings)
82+ if wkCtx.webContext != nil {
83+ log.Info().
84+ Int("limit_mb", opts.WebProcessMemory.MemoryLimitMB).
85+ Float64("poll_sec", opts.WebProcessMemory.PollIntervalSec).
86+ Float64("conservative", opts.WebProcessMemory.ConservativeThreshold).
87+ Float64("strict", opts.WebProcessMemory.StrictThreshold).
88+ Float64("kill", opts.WebProcessMemory.KillThreshold).
89+ Msg("created WebContext with memory pressure settings")
90+ }
91+ }
87 }92 }
88 93
89- wkCtx.webContext = webkit.WebContextGetDefault()94+ // Fall back to default WebContext if custom creation failed or wasn't needed
95+ if wkCtx.webContext == nil {
96+ wkCtx.webContext = webkit.WebContextGetDefault()
97+ }
90 if wkCtx.webContext == nil {98 if wkCtx.webContext == nil {
91- return nil, fmt.Errorf("failed to get default WebContext")99+ return nil, fmt.Errorf("failed to create or get WebContext")
92 }100 }
93 101
94 // Set cache model for browser-style caching102 // Set cache model for browser-style caching
@@ -103,25 +111,6 @@ func NewWebKitContextWithOptions(ctx context.Context, opts port.WebKitContextOpt
103 return wkCtx, nil111 return wkCtx, nil
104 }112 }
105 113
106-// createWebContextWithMemorySettings creates a WebContext with memory-pressure-settings.
107-// The memory-pressure-settings property is construct-only, so we need to set it at creation time.
108-func createWebContextWithMemorySettings(settings *webkit.MemoryPressureSettings) *webkit.WebContext {
109- if settings == nil {
110- return nil
111- }
112-
113- // Create a new WebContext and set memory pressure settings
114- // Note: memory-pressure-settings is construct-only, but we try setting it via property
115- // as an alternative to g_object_new_with_properties which may not work reliably
116- ctx := webkit.NewWebContext()
117- if ctx != nil {
118- // Try to set the property - this may not work for construct-only properties
119- // but it's the best we can do without lower-level GObject access
120- ctx.SetPropertyMemoryPressureSettings(settings.GoPointer())
121- }
122- return ctx
123-}
124-
125 // initNetworkSession creates and configures the persistent network session.114 // initNetworkSession creates and configures the persistent network session.
126 func (c *WebKitContext) initNetworkSession() error {115 func (c *WebKitContext) initNetworkSession() error {
127 // Create persistent network session116 // Create persistent network session
M internal/infrastructure/webkit/scheme_handler.go +43 -8
@@ -91,10 +91,20 @@ type configHardwarePayload struct {
9191
9292 type configPerformancePayload struct {
9393 Profile string `json:"profile"`
94+ Custom configCustomPerformance `json:"custom"`
9495 Resolved configResolvedPerformance `json:"resolved"`
9596 Hardware configHardwarePayload `json:"hardware"`
9697 }
9798
99+// configCustomPerformance holds user-editable fields for custom profile.
100+type configCustomPerformance struct {
101+ SkiaCPUThreads int `json:"skia_cpu_threads"`
102+ SkiaGPUThreads int `json:"skia_gpu_threads"`
103+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
104+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
105+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
106+}
107+
98108 // configResolvedPerformance shows the actual values that will be applied at startup.
99109 type configResolvedPerformance struct {
100110 SkiaCPUThreads int `json:"skia_cpu_threads"`
@@ -199,6 +209,13 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo
199209 },
200210 Performance: configPerformancePayload{
201211 Profile: string(cfg.Performance.Profile),
212+ Custom: configCustomPerformance{
213+ SkiaCPUThreads: cfg.Performance.SkiaCPUPaintingThreads,
214+ SkiaGPUThreads: cfg.Performance.SkiaGPUPaintingThreads,
215+ WebProcessMemoryMB: cfg.Performance.WebProcessMemoryLimitMB,
216+ NetworkProcessMemoryMB: cfg.Performance.NetworkProcessMemoryLimitMB,
217+ WebViewPoolPrewarm: cfg.Performance.WebViewPoolPrewarmCount,
218+ },
202219 Resolved: configResolvedPerformance{
203220 SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
204221 SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
@@ -494,6 +511,8 @@ func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response
494511 }
495512
496513 // RegisterWithContext registers the dumb:// scheme with a WebKitContext.
514+// The scheme is always registered on the default WebContext to ensure WebViews
515+// (which use the default WebContext) can load dumb:// URLs.
497516 func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {
498517 if wkCtx == nil || wkCtx.Context() == nil {
499518 h.logger.Error().Msg("cannot register scheme: context is nil")
@@ -504,15 +523,31 @@ func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {
504523 h.HandleRequest(reqPtr)
505524 })
506525
507- wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)
526+ // Always register on the default WebContext since WebViews use it
527+ defaultCtx := webkit.WebContextGetDefault()
528+ if defaultCtx != nil {
529+ defaultCtx.RegisterUriScheme("dumb", &callback, 0, nil)
530+
531+ // Mark scheme as local, secure, and CORS-enabled for proper security policies
532+ if secMgr := defaultCtx.GetSecurityManager(); secMgr != nil {
533+ secMgr.RegisterUriSchemeAsLocal("dumb")
534+ secMgr.RegisterUriSchemeAsSecure("dumb")
535+ secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
536+ }
537+ h.logger.Debug().Msg("dumb:// scheme registered on default WebContext")
538+ }
508539
509- // Mark scheme as local, secure, and CORS-enabled for proper security policies
510- secMgr := wkCtx.Context().GetSecurityManager()
511- if secMgr != nil {
512- secMgr.RegisterUriSchemeAsLocal("dumb")
513- secMgr.RegisterUriSchemeAsSecure("dumb")
514- secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
515- h.logger.Debug().Msg("dumb:// scheme registered as local, secure, and CORS-enabled")
540+ // Also register on custom WebContext if different from default
541+ customCtx := wkCtx.Context()
542+ if customCtx.GoPointer() != 0 && (defaultCtx == nil || customCtx.GoPointer() != defaultCtx.GoPointer()) {
543+ wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)
544+
545+ if secMgr := customCtx.GetSecurityManager(); secMgr != nil {
546+ secMgr.RegisterUriSchemeAsLocal("dumb")
547+ secMgr.RegisterUriSchemeAsSecure("dumb")
548+ secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
549+ }
550+ h.logger.Debug().Msg("dumb:// scheme also registered on custom WebContext")
516551 }
517552
518553 h.logger.Info().Msg("dumb:// scheme registered")
@@ -91,10 +91,20 @@ type configHardwarePayload struct {
91 91
92 type configPerformancePayload struct {92 type configPerformancePayload struct {
93 Profile string `json:"profile"`93 Profile string `json:"profile"`
94+ Custom configCustomPerformance `json:"custom"`
94 Resolved configResolvedPerformance `json:"resolved"`95 Resolved configResolvedPerformance `json:"resolved"`
95 Hardware configHardwarePayload `json:"hardware"`96 Hardware configHardwarePayload `json:"hardware"`
96 }97 }
97 98
99+// configCustomPerformance holds user-editable fields for custom profile.
100+type configCustomPerformance struct {
101+ SkiaCPUThreads int `json:"skia_cpu_threads"`
102+ SkiaGPUThreads int `json:"skia_gpu_threads"`
103+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
104+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
105+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
106+}
107+
98 // configResolvedPerformance shows the actual values that will be applied at startup.108 // configResolvedPerformance shows the actual values that will be applied at startup.
99 type configResolvedPerformance struct {109 type configResolvedPerformance struct {
100 SkiaCPUThreads int `json:"skia_cpu_threads"`110 SkiaCPUThreads int `json:"skia_cpu_threads"`
@@ -199,6 +209,13 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo
199 },209 },
200 Performance: configPerformancePayload{210 Performance: configPerformancePayload{
201 Profile: string(cfg.Performance.Profile),211 Profile: string(cfg.Performance.Profile),
212+ Custom: configCustomPerformance{
213+ SkiaCPUThreads: cfg.Performance.SkiaCPUPaintingThreads,
214+ SkiaGPUThreads: cfg.Performance.SkiaGPUPaintingThreads,
215+ WebProcessMemoryMB: cfg.Performance.WebProcessMemoryLimitMB,
216+ NetworkProcessMemoryMB: cfg.Performance.NetworkProcessMemoryLimitMB,
217+ WebViewPoolPrewarm: cfg.Performance.WebViewPoolPrewarmCount,
218+ },
202 Resolved: configResolvedPerformance{219 Resolved: configResolvedPerformance{
203 SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,220 SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
204 SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,221 SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
@@ -494,6 +511,8 @@ func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response
494 }511 }
495 512
496 // RegisterWithContext registers the dumb:// scheme with a WebKitContext.513 // RegisterWithContext registers the dumb:// scheme with a WebKitContext.
514+// The scheme is always registered on the default WebContext to ensure WebViews
515+// (which use the default WebContext) can load dumb:// URLs.
497 func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {516 func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {
498 if wkCtx == nil || wkCtx.Context() == nil {517 if wkCtx == nil || wkCtx.Context() == nil {
499 h.logger.Error().Msg("cannot register scheme: context is nil")518 h.logger.Error().Msg("cannot register scheme: context is nil")
@@ -504,15 +523,31 @@ func (h *DumbSchemeHandler) RegisterWithContext(wkCtx *WebKitContext) {
504 h.HandleRequest(reqPtr)523 h.HandleRequest(reqPtr)
505 })524 })
506 525
507- wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)526+ // Always register on the default WebContext since WebViews use it
527+ defaultCtx := webkit.WebContextGetDefault()
528+ if defaultCtx != nil {
529+ defaultCtx.RegisterUriScheme("dumb", &callback, 0, nil)
530+
531+ // Mark scheme as local, secure, and CORS-enabled for proper security policies
532+ if secMgr := defaultCtx.GetSecurityManager(); secMgr != nil {
533+ secMgr.RegisterUriSchemeAsLocal("dumb")
534+ secMgr.RegisterUriSchemeAsSecure("dumb")
535+ secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
536+ }
537+ h.logger.Debug().Msg("dumb:// scheme registered on default WebContext")
538+ }
508 539
509- // Mark scheme as local, secure, and CORS-enabled for proper security policies540+ // Also register on custom WebContext if different from default
510- secMgr := wkCtx.Context().GetSecurityManager()541+ customCtx := wkCtx.Context()
511- if secMgr != nil {542+ if customCtx.GoPointer() != 0 && (defaultCtx == nil || customCtx.GoPointer() != defaultCtx.GoPointer()) {
512- secMgr.RegisterUriSchemeAsLocal("dumb")543+ wkCtx.Context().RegisterUriScheme("dumb", &callback, 0, nil)
513- secMgr.RegisterUriSchemeAsSecure("dumb")544+
514- secMgr.RegisterUriSchemeAsCorsEnabled("dumb")545+ if secMgr := customCtx.GetSecurityManager(); secMgr != nil {
515- h.logger.Debug().Msg("dumb:// scheme registered as local, secure, and CORS-enabled")546+ secMgr.RegisterUriSchemeAsLocal("dumb")
547+ secMgr.RegisterUriSchemeAsSecure("dumb")
548+ secMgr.RegisterUriSchemeAsCorsEnabled("dumb")
549+ }
550+ h.logger.Debug().Msg("dumb:// scheme also registered on custom WebContext")
516 }551 }
517 552
518 h.logger.Info().Msg("dumb:// scheme registered")553 h.logger.Info().Msg("dumb:// scheme registered")
M internal/infrastructure/webkit/webview.go +8 -18
@@ -248,25 +248,15 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan
248248 return nil, fmt.Errorf("webkit context not initialized")
249249 }
250250
251- // Use NetworkSession-aware constructor for persistent cookie/storage.
252- inner := webkit.NewWebViewWithNetworkSession(wkCtx.NetworkSession())
251+ // Use options constructor to pass both WebContext and NetworkSession.
252+ // This ensures WebViews use our custom WebContext (with memory pressure settings)
253+ // and the persistent NetworkSession for cookie/storage.
254+ inner := webkit.NewWebViewWithOptions(&webkit.WebViewOptions{
255+ WebContext: wkCtx.Context(),
256+ NetworkSession: wkCtx.NetworkSession(),
257+ })
253258 if inner == nil {
254- return nil, fmt.Errorf("failed to create webkit webview with network session")
255- }
256-
257- // Sanity check: our internal dumb:// scheme must be registered on the same WebContext
258- // that the WebView is actually using.
259- if expectedCtx := wkCtx.Context(); expectedCtx != nil {
260- if actualCtx := inner.GetContext(); actualCtx != nil {
261- expectedPtr := expectedCtx.GoPointer()
262- actualPtr := actualCtx.GoPointer()
263- if expectedPtr != 0 && actualPtr != 0 && expectedPtr != actualPtr {
264- log.Warn().
265- Uint64("expected_web_context", uint64(expectedPtr)).
266- Uint64("actual_web_context", uint64(actualPtr)).
267- Msg("webview WebContext mismatch; internal dumb:// pages may fail")
268- }
269- }
259+ return nil, fmt.Errorf("failed to create webkit webview with options")
270260 }
271261
272262 // Set background color IMMEDIATELY after creation, before any other operations.
@@ -248,25 +248,15 @@ func NewWebView(ctx context.Context, wkCtx *WebKitContext, settings *SettingsMan
248 return nil, fmt.Errorf("webkit context not initialized")248 return nil, fmt.Errorf("webkit context not initialized")
249 }249 }
250 250
251- // Use NetworkSession-aware constructor for persistent cookie/storage.251+ // Use options constructor to pass both WebContext and NetworkSession.
252- inner := webkit.NewWebViewWithNetworkSession(wkCtx.NetworkSession())252+ // This ensures WebViews use our custom WebContext (with memory pressure settings)
253+ // and the persistent NetworkSession for cookie/storage.
254+ inner := webkit.NewWebViewWithOptions(&webkit.WebViewOptions{
255+ WebContext: wkCtx.Context(),
256+ NetworkSession: wkCtx.NetworkSession(),
257+ })
253 if inner == nil {258 if inner == nil {
254- return nil, fmt.Errorf("failed to create webkit webview with network session")259+ return nil, fmt.Errorf("failed to create webkit webview with options")
255- }
256-
257- // Sanity check: our internal dumb:// scheme must be registered on the same WebContext
258- // that the WebView is actually using.
259- if expectedCtx := wkCtx.Context(); expectedCtx != nil {
260- if actualCtx := inner.GetContext(); actualCtx != nil {
261- expectedPtr := expectedCtx.GoPointer()
262- actualPtr := actualCtx.GoPointer()
263- if expectedPtr != 0 && actualPtr != 0 && expectedPtr != actualPtr {
264- log.Warn().
265- Uint64("expected_web_context", uint64(expectedPtr)).
266- Uint64("actual_web_context", uint64(actualPtr)).
267- Msg("webview WebContext mismatch; internal dumb:// pages may fail")
268- }
269- }
270 }260 }
271 261
272 // Set background color IMMEDIATELY after creation, before any other operations.262 // Set background color IMMEDIATELY after creation, before any other operations.

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 8

Tags 42

↑↓ moveenter openesc close