detached cf39998

feat(config): add performance profile system for WebKitGTK tuning

commit
cf3999830e7cd9df51d5f4c734ca4ca66a3c7270
author
brice · · 2025-12-28 20:33 UTC
committer
brice · · 2025-12-29 17:28 UTC
parent
cd437e3
signature
unsigned
8 files · +621 -5
M internal/bootstrap/webkit_stack.go +64 -4
@@ -32,6 +32,15 @@ func BuildWebKitStack(
3232 themeManager *theme.Manager,
3333 logger zerolog.Logger,
3434 ) WebKitStack {
35+ // Resolve performance profile to get actual settings
36+ perfSettings := config.ResolvePerformanceProfile(&cfg.Performance)
37+ logger.Info().
38+ Str("profile", string(cfg.Performance.Profile)).
39+ Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).
40+ Int("skia_gpu_threads", perfSettings.SkiaGPUPaintingThreads).
41+ Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).
42+ Msg("resolved performance profile")
43+
3544 // CRITICAL: Configure rendering environment BEFORE WebKit/GTK initialization.
3645 // Environment variables must be set before GTK/WebKit/GStreamer initializes.
3746 renderEnv := env.NewManager()
@@ -58,6 +67,11 @@ func BuildWebKitStack(
5867 ShowFPS: cfg.Rendering.ShowFPS,
5968 SampleMemory: cfg.Rendering.SampleMemory,
6069 DebugFrames: cfg.Rendering.DebugFrames,
70+
71+ // Skia rendering thread settings (from resolved profile)
72+ SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
73+ SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
74+ SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
6175 }
6276 if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
6377 logger.Warn().Err(err).Msg("failed to apply rendering environment")
@@ -67,7 +81,35 @@ func BuildWebKitStack(
6781 Interface("vars", renderEnv.GetAppliedVars()).
6882 Msg("rendering environment configured")
6983
70- wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir)
84+ // Build WebKitContext options with memory pressure settings
85+ wkOpts := port.WebKitContextOptions{
86+ DataDir: dataDir,
87+ CacheDir: cacheDir,
88+ }
89+
90+ // Configure web process memory pressure if any setting is configured
91+ if hasWebProcessMemoryConfig(&perfSettings) {
92+ wkOpts.WebProcessMemory = &port.MemoryPressureConfig{
93+ MemoryLimitMB: perfSettings.WebProcessMemoryLimitMB,
94+ PollIntervalSec: perfSettings.WebProcessMemoryPollIntervalSec,
95+ ConservativeThreshold: perfSettings.WebProcessMemoryConservativeThreshold,
96+ StrictThreshold: perfSettings.WebProcessMemoryStrictThreshold,
97+ KillThreshold: perfSettings.WebProcessMemoryKillThreshold,
98+ }
99+ }
100+
101+ // Configure network process memory pressure if any setting is configured
102+ if hasNetworkProcessMemoryConfig(&perfSettings) {
103+ wkOpts.NetworkProcessMemory = &port.MemoryPressureConfig{
104+ MemoryLimitMB: perfSettings.NetworkProcessMemoryLimitMB,
105+ PollIntervalSec: perfSettings.NetworkProcessMemoryPollIntervalSec,
106+ ConservativeThreshold: perfSettings.NetworkProcessMemoryConservativeThreshold,
107+ StrictThreshold: perfSettings.NetworkProcessMemoryStrictThreshold,
108+ KillThreshold: perfSettings.NetworkProcessMemoryKillThreshold,
109+ }
110+ }
111+
112+ wkCtx, err := webkit.NewWebKitContextWithOptions(ctx, wkOpts)
71113 if err != nil {
72114 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
73115 }
@@ -106,9 +148,9 @@ func BuildWebKitStack(
106148
107149 messageRouter := webkit.NewMessageRouter(ctx)
108150 poolCfg := webkit.DefaultPoolConfig()
109- // Override prewarm count from config if set
110- if cfg.Performance.WebViewPoolPrewarmCount > 0 {
111- poolCfg.PrewarmCount = cfg.Performance.WebViewPoolPrewarmCount
151+ // Override prewarm count from resolved profile settings
152+ if perfSettings.WebViewPoolPrewarmCount > 0 {
153+ poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount
112154 }
113155 pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
114156 // Ensure prewarmed WebViews pick up the theme background color.
@@ -135,3 +177,21 @@ func BuildWebKitStack(
135177 FilterManager: filterManager,
136178 }
137179 }
180+
181+// hasWebProcessMemoryConfig returns true if any web process memory setting is configured.
182+func hasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
183+ return p.WebProcessMemoryLimitMB > 0 ||
184+ p.WebProcessMemoryPollIntervalSec > 0 ||
185+ p.WebProcessMemoryConservativeThreshold > 0 ||
186+ p.WebProcessMemoryStrictThreshold > 0 ||
187+ p.WebProcessMemoryKillThreshold >= 0
188+}
189+
190+// hasNetworkProcessMemoryConfig returns true if any network process memory setting is configured.
191+func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
192+ return p.NetworkProcessMemoryLimitMB > 0 ||
193+ p.NetworkProcessMemoryPollIntervalSec > 0 ||
194+ p.NetworkProcessMemoryConservativeThreshold > 0 ||
195+ p.NetworkProcessMemoryStrictThreshold > 0 ||
196+ p.NetworkProcessMemoryKillThreshold >= 0
197+}
@@ -32,6 +32,15 @@ func BuildWebKitStack(
32 themeManager *theme.Manager,32 themeManager *theme.Manager,
33 logger zerolog.Logger,33 logger zerolog.Logger,
34 ) WebKitStack {34 ) WebKitStack {
35+ // Resolve performance profile to get actual settings
36+ perfSettings := config.ResolvePerformanceProfile(&cfg.Performance)
37+ logger.Info().
38+ Str("profile", string(cfg.Performance.Profile)).
39+ Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).
40+ Int("skia_gpu_threads", perfSettings.SkiaGPUPaintingThreads).
41+ Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).
42+ Msg("resolved performance profile")
43+
35 // CRITICAL: Configure rendering environment BEFORE WebKit/GTK initialization.44 // CRITICAL: Configure rendering environment BEFORE WebKit/GTK initialization.
36 // Environment variables must be set before GTK/WebKit/GStreamer initializes.45 // Environment variables must be set before GTK/WebKit/GStreamer initializes.
37 renderEnv := env.NewManager()46 renderEnv := env.NewManager()
@@ -58,6 +67,11 @@ func BuildWebKitStack(
58 ShowFPS: cfg.Rendering.ShowFPS,67 ShowFPS: cfg.Rendering.ShowFPS,
59 SampleMemory: cfg.Rendering.SampleMemory,68 SampleMemory: cfg.Rendering.SampleMemory,
60 DebugFrames: cfg.Rendering.DebugFrames,69 DebugFrames: cfg.Rendering.DebugFrames,
70+
71+ // Skia rendering thread settings (from resolved profile)
72+ SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
73+ SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
74+ SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
61 }75 }
62 if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {76 if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
63 logger.Warn().Err(err).Msg("failed to apply rendering environment")77 logger.Warn().Err(err).Msg("failed to apply rendering environment")
@@ -67,7 +81,35 @@ func BuildWebKitStack(
67 Interface("vars", renderEnv.GetAppliedVars()).81 Interface("vars", renderEnv.GetAppliedVars()).
68 Msg("rendering environment configured")82 Msg("rendering environment configured")
69 83
70- wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir)84+ // Build WebKitContext options with memory pressure settings
85+ wkOpts := port.WebKitContextOptions{
86+ DataDir: dataDir,
87+ CacheDir: cacheDir,
88+ }
89+
90+ // Configure web process memory pressure if any setting is configured
91+ if hasWebProcessMemoryConfig(&perfSettings) {
92+ wkOpts.WebProcessMemory = &port.MemoryPressureConfig{
93+ MemoryLimitMB: perfSettings.WebProcessMemoryLimitMB,
94+ PollIntervalSec: perfSettings.WebProcessMemoryPollIntervalSec,
95+ ConservativeThreshold: perfSettings.WebProcessMemoryConservativeThreshold,
96+ StrictThreshold: perfSettings.WebProcessMemoryStrictThreshold,
97+ KillThreshold: perfSettings.WebProcessMemoryKillThreshold,
98+ }
99+ }
100+
101+ // Configure network process memory pressure if any setting is configured
102+ if hasNetworkProcessMemoryConfig(&perfSettings) {
103+ wkOpts.NetworkProcessMemory = &port.MemoryPressureConfig{
104+ MemoryLimitMB: perfSettings.NetworkProcessMemoryLimitMB,
105+ PollIntervalSec: perfSettings.NetworkProcessMemoryPollIntervalSec,
106+ ConservativeThreshold: perfSettings.NetworkProcessMemoryConservativeThreshold,
107+ StrictThreshold: perfSettings.NetworkProcessMemoryStrictThreshold,
108+ KillThreshold: perfSettings.NetworkProcessMemoryKillThreshold,
109+ }
110+ }
111+
112+ wkCtx, err := webkit.NewWebKitContextWithOptions(ctx, wkOpts)
71 if err != nil {113 if err != nil {
72 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")114 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
73 }115 }
@@ -106,9 +148,9 @@ func BuildWebKitStack(
106 148
107 messageRouter := webkit.NewMessageRouter(ctx)149 messageRouter := webkit.NewMessageRouter(ctx)
108 poolCfg := webkit.DefaultPoolConfig()150 poolCfg := webkit.DefaultPoolConfig()
109- // Override prewarm count from config if set151+ // Override prewarm count from resolved profile settings
110- if cfg.Performance.WebViewPoolPrewarmCount > 0 {152+ if perfSettings.WebViewPoolPrewarmCount > 0 {
111- poolCfg.PrewarmCount = cfg.Performance.WebViewPoolPrewarmCount153+ poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount
112 }154 }
113 pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)155 pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
114 // Ensure prewarmed WebViews pick up the theme background color.156 // Ensure prewarmed WebViews pick up the theme background color.
@@ -135,3 +177,21 @@ func BuildWebKitStack(
135 FilterManager: filterManager,177 FilterManager: filterManager,
136 }178 }
137 }179 }
180+
181+// hasWebProcessMemoryConfig returns true if any web process memory setting is configured.
182+func hasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
183+ return p.WebProcessMemoryLimitMB > 0 ||
184+ p.WebProcessMemoryPollIntervalSec > 0 ||
185+ p.WebProcessMemoryConservativeThreshold > 0 ||
186+ p.WebProcessMemoryStrictThreshold > 0 ||
187+ p.WebProcessMemoryKillThreshold >= 0
188+}
189+
190+// hasNetworkProcessMemoryConfig returns true if any network process memory setting is configured.
191+func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
192+ return p.NetworkProcessMemoryLimitMB > 0 ||
193+ p.NetworkProcessMemoryPollIntervalSec > 0 ||
194+ p.NetworkProcessMemoryConservativeThreshold > 0 ||
195+ p.NetworkProcessMemoryStrictThreshold > 0 ||
196+ p.NetworkProcessMemoryKillThreshold >= 0
197+}
M internal/infrastructure/config/defaults.go +25 -0
@@ -65,6 +65,13 @@ const (
6565 // Performance defaults
6666 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
6767 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
68+
69+ // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
70+ defaultSkiaCPUPaintingThreads = 0
71+ defaultSkiaGPUPaintingThreads = -1 // -1 means unset; 0 would disable GPU tile painting
72+
73+ // Memory pressure defaults (-1 = unset for kill threshold, 0 = unset for others)
74+ defaultMemoryPressureKillThreshold = -1.0
6875 )
6976
7077 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -282,8 +289,26 @@ func DefaultConfig() *Config {
282289 Prefix: "",
283290 },
284291 Performance: PerformanceConfig{
292+ Profile: ProfileDefault, // Use WebKit defaults, no tuning
285293 ZoomCacheSize: defaultZoomCacheSize,
286294 WebViewPoolPrewarmCount: defaultWebViewPoolPrewarmCount,
295+ // Skia threading - balanced defaults (unset = use WebKit defaults)
296+ // These only apply when Profile is "custom"
297+ SkiaCPUPaintingThreads: defaultSkiaCPUPaintingThreads,
298+ SkiaGPUPaintingThreads: defaultSkiaGPUPaintingThreads,
299+ SkiaEnableCPURendering: false,
300+ // Web process memory pressure - all unset by default
301+ WebProcessMemoryLimitMB: 0,
302+ WebProcessMemoryPollIntervalSec: 0,
303+ WebProcessMemoryConservativeThreshold: 0,
304+ WebProcessMemoryStrictThreshold: 0,
305+ WebProcessMemoryKillThreshold: defaultMemoryPressureKillThreshold,
306+ // Network process memory pressure - all unset by default
307+ NetworkProcessMemoryLimitMB: 0,
308+ NetworkProcessMemoryPollIntervalSec: 0,
309+ NetworkProcessMemoryConservativeThreshold: 0,
310+ NetworkProcessMemoryStrictThreshold: 0,
311+ NetworkProcessMemoryKillThreshold: defaultMemoryPressureKillThreshold,
287312 },
288313 Update: UpdateConfig{
289314 EnableOnStartup: true, // Check for updates on startup by default
@@ -65,6 +65,13 @@ const (
65 // Performance defaults65 // Performance defaults
66 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)66 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
67 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup67 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
68+
69+ // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
70+ defaultSkiaCPUPaintingThreads = 0
71+ defaultSkiaGPUPaintingThreads = -1 // -1 means unset; 0 would disable GPU tile painting
72+
73+ // Memory pressure defaults (-1 = unset for kill threshold, 0 = unset for others)
74+ defaultMemoryPressureKillThreshold = -1.0
68 )75 )
69 76
70 // getDefaultLogDir returns the default log directory, falls back to empty string on error77 // getDefaultLogDir returns the default log directory, falls back to empty string on error
@@ -282,8 +289,26 @@ func DefaultConfig() *Config {
282 Prefix: "",289 Prefix: "",
283 },290 },
284 Performance: PerformanceConfig{291 Performance: PerformanceConfig{
292+ Profile: ProfileDefault, // Use WebKit defaults, no tuning
285 ZoomCacheSize: defaultZoomCacheSize,293 ZoomCacheSize: defaultZoomCacheSize,
286 WebViewPoolPrewarmCount: defaultWebViewPoolPrewarmCount,294 WebViewPoolPrewarmCount: defaultWebViewPoolPrewarmCount,
295+ // Skia threading - balanced defaults (unset = use WebKit defaults)
296+ // These only apply when Profile is "custom"
297+ SkiaCPUPaintingThreads: defaultSkiaCPUPaintingThreads,
298+ SkiaGPUPaintingThreads: defaultSkiaGPUPaintingThreads,
299+ SkiaEnableCPURendering: false,
300+ // Web process memory pressure - all unset by default
301+ WebProcessMemoryLimitMB: 0,
302+ WebProcessMemoryPollIntervalSec: 0,
303+ WebProcessMemoryConservativeThreshold: 0,
304+ WebProcessMemoryStrictThreshold: 0,
305+ WebProcessMemoryKillThreshold: defaultMemoryPressureKillThreshold,
306+ // Network process memory pressure - all unset by default
307+ NetworkProcessMemoryLimitMB: 0,
308+ NetworkProcessMemoryPollIntervalSec: 0,
309+ NetworkProcessMemoryConservativeThreshold: 0,
310+ NetworkProcessMemoryStrictThreshold: 0,
311+ NetworkProcessMemoryKillThreshold: defaultMemoryPressureKillThreshold,
287 },312 },
288 Update: UpdateConfig{313 Update: UpdateConfig{
289 EnableOnStartup: true, // Check for updates on startup by default314 EnableOnStartup: true, // Check for updates on startup by default
M internal/infrastructure/config/loader.go +38 -0
@@ -212,6 +212,20 @@ func normalizeConfig(config *Config) {
212212 }
213213
214214 config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)
215+ normalizePerformanceProfile(config)
216+}
217+
218+func normalizePerformanceProfile(config *Config) {
219+ switch strings.ToLower(string(config.Performance.Profile)) {
220+ case "", string(ProfileDefault):
221+ config.Performance.Profile = ProfileDefault
222+ case string(ProfileLite):
223+ config.Performance.Profile = ProfileLite
224+ case string(ProfileMax):
225+ config.Performance.Profile = ProfileMax
226+ case string(ProfileCustom):
227+ config.Performance.Profile = ProfileCustom
228+ }
215229 }
216230
217231 // Get returns the current configuration (thread-safe).
@@ -316,6 +330,7 @@ func (m *Manager) setDefaults() {
316330 m.setRuntimeDefaults(defaults)
317331 m.setSessionDefaults(defaults)
318332 m.setUpdateDefaults(defaults)
333+ m.setPerformanceDefaults(defaults)
319334 }
320335
321336 func (m *Manager) setHistoryDefaults(defaults *Config) {
@@ -461,6 +476,29 @@ func (m *Manager) setUpdateDefaults(defaults *Config) {
461476 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
462477 }
463478
479+func (m *Manager) setPerformanceDefaults(defaults *Config) {
480+ m.viper.SetDefault("performance.profile", string(defaults.Performance.Profile))
481+ m.viper.SetDefault("performance.zoom_cache_size", defaults.Performance.ZoomCacheSize)
482+ m.viper.SetDefault("performance.webview_pool_prewarm_count", defaults.Performance.WebViewPoolPrewarmCount)
483+ // Skia threading (only used when profile = "custom")
484+ m.viper.SetDefault("performance.skia_cpu_painting_threads", defaults.Performance.SkiaCPUPaintingThreads)
485+ m.viper.SetDefault("performance.skia_gpu_painting_threads", defaults.Performance.SkiaGPUPaintingThreads)
486+ m.viper.SetDefault("performance.skia_enable_cpu_rendering", defaults.Performance.SkiaEnableCPURendering)
487+ // Web process memory pressure
488+ m.viper.SetDefault("performance.web_process_memory_limit_mb", defaults.Performance.WebProcessMemoryLimitMB)
489+ m.viper.SetDefault("performance.web_process_memory_poll_interval_sec", defaults.Performance.WebProcessMemoryPollIntervalSec)
490+ m.viper.SetDefault("performance.web_process_memory_conservative_threshold", defaults.Performance.WebProcessMemoryConservativeThreshold)
491+ m.viper.SetDefault("performance.web_process_memory_strict_threshold", defaults.Performance.WebProcessMemoryStrictThreshold)
492+ m.viper.SetDefault("performance.web_process_memory_kill_threshold", defaults.Performance.WebProcessMemoryKillThreshold)
493+ // Network process memory pressure
494+ m.viper.SetDefault("performance.network_process_memory_limit_mb", defaults.Performance.NetworkProcessMemoryLimitMB)
495+ m.viper.SetDefault("performance.network_process_memory_poll_interval_sec", defaults.Performance.NetworkProcessMemoryPollIntervalSec)
496+ netConservativeThreshold := defaults.Performance.NetworkProcessMemoryConservativeThreshold
497+ m.viper.SetDefault("performance.network_process_memory_conservative_threshold", netConservativeThreshold)
498+ m.viper.SetDefault("performance.network_process_memory_strict_threshold", defaults.Performance.NetworkProcessMemoryStrictThreshold)
499+ m.viper.SetDefault("performance.network_process_memory_kill_threshold", defaults.Performance.NetworkProcessMemoryKillThreshold)
500+}
501+
464502 // New returns a new default configuration instance.
465503 // This is a convenience function for getting default config without the full manager.
466504 func New() *Config {
@@ -212,6 +212,20 @@ func normalizeConfig(config *Config) {
212 }212 }
213 213
214 config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)214 config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)
215+ normalizePerformanceProfile(config)
216+}
217+
218+func normalizePerformanceProfile(config *Config) {
219+ switch strings.ToLower(string(config.Performance.Profile)) {
220+ case "", string(ProfileDefault):
221+ config.Performance.Profile = ProfileDefault
222+ case string(ProfileLite):
223+ config.Performance.Profile = ProfileLite
224+ case string(ProfileMax):
225+ config.Performance.Profile = ProfileMax
226+ case string(ProfileCustom):
227+ config.Performance.Profile = ProfileCustom
228+ }
215 }229 }
216 230
217 // Get returns the current configuration (thread-safe).231 // Get returns the current configuration (thread-safe).
@@ -316,6 +330,7 @@ func (m *Manager) setDefaults() {
316 m.setRuntimeDefaults(defaults)330 m.setRuntimeDefaults(defaults)
317 m.setSessionDefaults(defaults)331 m.setSessionDefaults(defaults)
318 m.setUpdateDefaults(defaults)332 m.setUpdateDefaults(defaults)
333+ m.setPerformanceDefaults(defaults)
319 }334 }
320 335
321 func (m *Manager) setHistoryDefaults(defaults *Config) {336 func (m *Manager) setHistoryDefaults(defaults *Config) {
@@ -461,6 +476,29 @@ func (m *Manager) setUpdateDefaults(defaults *Config) {
461 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)476 m.viper.SetDefault("update.notify_on_new_settings", defaults.Update.NotifyOnNewSettings)
462 }477 }
463 478
479+func (m *Manager) setPerformanceDefaults(defaults *Config) {
480+ m.viper.SetDefault("performance.profile", string(defaults.Performance.Profile))
481+ m.viper.SetDefault("performance.zoom_cache_size", defaults.Performance.ZoomCacheSize)
482+ m.viper.SetDefault("performance.webview_pool_prewarm_count", defaults.Performance.WebViewPoolPrewarmCount)
483+ // Skia threading (only used when profile = "custom")
484+ m.viper.SetDefault("performance.skia_cpu_painting_threads", defaults.Performance.SkiaCPUPaintingThreads)
485+ m.viper.SetDefault("performance.skia_gpu_painting_threads", defaults.Performance.SkiaGPUPaintingThreads)
486+ m.viper.SetDefault("performance.skia_enable_cpu_rendering", defaults.Performance.SkiaEnableCPURendering)
487+ // Web process memory pressure
488+ m.viper.SetDefault("performance.web_process_memory_limit_mb", defaults.Performance.WebProcessMemoryLimitMB)
489+ m.viper.SetDefault("performance.web_process_memory_poll_interval_sec", defaults.Performance.WebProcessMemoryPollIntervalSec)
490+ m.viper.SetDefault("performance.web_process_memory_conservative_threshold", defaults.Performance.WebProcessMemoryConservativeThreshold)
491+ m.viper.SetDefault("performance.web_process_memory_strict_threshold", defaults.Performance.WebProcessMemoryStrictThreshold)
492+ m.viper.SetDefault("performance.web_process_memory_kill_threshold", defaults.Performance.WebProcessMemoryKillThreshold)
493+ // Network process memory pressure
494+ m.viper.SetDefault("performance.network_process_memory_limit_mb", defaults.Performance.NetworkProcessMemoryLimitMB)
495+ m.viper.SetDefault("performance.network_process_memory_poll_interval_sec", defaults.Performance.NetworkProcessMemoryPollIntervalSec)
496+ netConservativeThreshold := defaults.Performance.NetworkProcessMemoryConservativeThreshold
497+ m.viper.SetDefault("performance.network_process_memory_conservative_threshold", netConservativeThreshold)
498+ m.viper.SetDefault("performance.network_process_memory_strict_threshold", defaults.Performance.NetworkProcessMemoryStrictThreshold)
499+ m.viper.SetDefault("performance.network_process_memory_kill_threshold", defaults.Performance.NetworkProcessMemoryKillThreshold)
500+}
501+
464 // New returns a new default configuration instance.502 // New returns a new default configuration instance.
465 // This is a convenience function for getting default config without the full manager.503 // This is a convenience function for getting default config without the full manager.
466 func New() *Config {504 func New() *Config {
A internal/infrastructure/config/profile.go +172 -0
@@ -0,0 +1,172 @@
1+package config
2+
3+import "runtime"
4+
5+// ResolvedPerformanceSettings contains the computed performance settings
6+// after profile resolution. These are the actual values to apply.
7+type ResolvedPerformanceSettings struct {
8+ // Skia threading
9+ SkiaCPUPaintingThreads int
10+ SkiaGPUPaintingThreads int
11+ SkiaEnableCPURendering bool
12+
13+ // Web process memory pressure
14+ WebProcessMemoryLimitMB int
15+ WebProcessMemoryPollIntervalSec float64
16+ WebProcessMemoryConservativeThreshold float64
17+ WebProcessMemoryStrictThreshold float64
18+ WebProcessMemoryKillThreshold float64
19+
20+ // Network process memory pressure
21+ NetworkProcessMemoryLimitMB int
22+ NetworkProcessMemoryPollIntervalSec float64
23+ NetworkProcessMemoryConservativeThreshold float64
24+ NetworkProcessMemoryStrictThreshold float64
25+ NetworkProcessMemoryKillThreshold float64
26+
27+ // WebView pool
28+ WebViewPoolPrewarmCount int
29+}
30+
31+// ResolvePerformanceProfile computes the actual performance settings based on
32+// the selected profile. For ProfileCustom, it returns the user's configured values.
33+// For other profiles, it computes appropriate values.
34+func ResolvePerformanceProfile(cfg *PerformanceConfig) ResolvedPerformanceSettings {
35+ switch cfg.Profile {
36+ case ProfileLite:
37+ return resolveLiteProfile()
38+ case ProfileMax:
39+ return resolveMaxProfile()
40+ case ProfileCustom:
41+ return resolveCustomProfile(cfg)
42+ default: // ProfileDefault or empty
43+ return resolveDefaultProfile()
44+ }
45+}
46+
47+// resolveDefaultProfile returns WebKit defaults (no tuning).
48+func resolveDefaultProfile() ResolvedPerformanceSettings {
49+ return ResolvedPerformanceSettings{
50+ // All zeros/negatives mean "unset, use WebKit defaults"
51+ SkiaCPUPaintingThreads: 0,
52+ SkiaGPUPaintingThreads: -1,
53+ SkiaEnableCPURendering: false,
54+
55+ WebProcessMemoryLimitMB: 0,
56+ WebProcessMemoryPollIntervalSec: 0,
57+ WebProcessMemoryConservativeThreshold: 0,
58+ WebProcessMemoryStrictThreshold: 0,
59+ WebProcessMemoryKillThreshold: -1,
60+
61+ NetworkProcessMemoryLimitMB: 0,
62+ NetworkProcessMemoryPollIntervalSec: 0,
63+ NetworkProcessMemoryConservativeThreshold: 0,
64+ NetworkProcessMemoryStrictThreshold: 0,
65+ NetworkProcessMemoryKillThreshold: -1,
66+
67+ WebViewPoolPrewarmCount: 4,
68+ }
69+}
70+
71+// resolveLiteProfile returns settings optimized for low-RAM systems.
72+func resolveLiteProfile() ResolvedPerformanceSettings {
73+ return ResolvedPerformanceSettings{
74+ SkiaCPUPaintingThreads: 2,
75+ SkiaGPUPaintingThreads: -1, // unset
76+ SkiaEnableCPURendering: false,
77+
78+ WebProcessMemoryLimitMB: 512,
79+ WebProcessMemoryPollIntervalSec: 0, // use WebKit default (30s)
80+ WebProcessMemoryConservativeThreshold: 0.25,
81+ WebProcessMemoryStrictThreshold: 0.4,
82+ WebProcessMemoryKillThreshold: 0.8,
83+
84+ NetworkProcessMemoryLimitMB: 256,
85+ NetworkProcessMemoryPollIntervalSec: 0,
86+ NetworkProcessMemoryConservativeThreshold: 0.25,
87+ NetworkProcessMemoryStrictThreshold: 0.4,
88+ NetworkProcessMemoryKillThreshold: 0.8,
89+
90+ WebViewPoolPrewarmCount: 2,
91+ }
92+}
93+
94+// resolveMaxProfile returns settings optimized for maximum responsiveness.
95+func resolveMaxProfile() ResolvedPerformanceSettings {
96+ // Use half of available CPUs, minimum 4
97+ cpuThreads := runtime.NumCPU() / 2
98+ if cpuThreads < 4 {
99+ cpuThreads = 4
100+ }
101+
102+ return ResolvedPerformanceSettings{
103+ SkiaCPUPaintingThreads: cpuThreads,
104+ SkiaGPUPaintingThreads: 2,
105+ SkiaEnableCPURendering: false,
106+
107+ WebProcessMemoryLimitMB: 2048,
108+ WebProcessMemoryPollIntervalSec: 0, // use WebKit default
109+ WebProcessMemoryConservativeThreshold: 0.5,
110+ WebProcessMemoryStrictThreshold: 0.7,
111+ WebProcessMemoryKillThreshold: -1, // never kill
112+
113+ NetworkProcessMemoryLimitMB: 512,
114+ NetworkProcessMemoryPollIntervalSec: 0,
115+ NetworkProcessMemoryConservativeThreshold: 0.5,
116+ NetworkProcessMemoryStrictThreshold: 0.7,
117+ NetworkProcessMemoryKillThreshold: -1,
118+
119+ WebViewPoolPrewarmCount: 8,
120+ }
121+}
122+
123+// resolveCustomProfile returns the user's configured values directly.
124+func resolveCustomProfile(cfg *PerformanceConfig) ResolvedPerformanceSettings {
125+ return ResolvedPerformanceSettings{
126+ SkiaCPUPaintingThreads: cfg.SkiaCPUPaintingThreads,
127+ SkiaGPUPaintingThreads: cfg.SkiaGPUPaintingThreads,
128+ SkiaEnableCPURendering: cfg.SkiaEnableCPURendering,
129+
130+ WebProcessMemoryLimitMB: cfg.WebProcessMemoryLimitMB,
131+ WebProcessMemoryPollIntervalSec: cfg.WebProcessMemoryPollIntervalSec,
132+ WebProcessMemoryConservativeThreshold: cfg.WebProcessMemoryConservativeThreshold,
133+ WebProcessMemoryStrictThreshold: cfg.WebProcessMemoryStrictThreshold,
134+ WebProcessMemoryKillThreshold: cfg.WebProcessMemoryKillThreshold,
135+
136+ NetworkProcessMemoryLimitMB: cfg.NetworkProcessMemoryLimitMB,
137+ NetworkProcessMemoryPollIntervalSec: cfg.NetworkProcessMemoryPollIntervalSec,
138+ NetworkProcessMemoryConservativeThreshold: cfg.NetworkProcessMemoryConservativeThreshold,
139+ NetworkProcessMemoryStrictThreshold: cfg.NetworkProcessMemoryStrictThreshold,
140+ NetworkProcessMemoryKillThreshold: cfg.NetworkProcessMemoryKillThreshold,
141+
142+ WebViewPoolPrewarmCount: cfg.WebViewPoolPrewarmCount,
143+ }
144+}
145+
146+// HasCustomPerformanceFields returns true if any individual performance tuning
147+// fields are set to non-default values. Used for validation.
148+func HasCustomPerformanceFields(cfg *PerformanceConfig) bool {
149+ return cfg.SkiaCPUPaintingThreads != 0 ||
150+ cfg.SkiaGPUPaintingThreads != -1 ||
151+ cfg.SkiaEnableCPURendering ||
152+ cfg.WebProcessMemoryLimitMB != 0 ||
153+ cfg.WebProcessMemoryPollIntervalSec != 0 ||
154+ cfg.WebProcessMemoryConservativeThreshold != 0 ||
155+ cfg.WebProcessMemoryStrictThreshold != 0 ||
156+ cfg.WebProcessMemoryKillThreshold != -1 ||
157+ cfg.NetworkProcessMemoryLimitMB != 0 ||
158+ cfg.NetworkProcessMemoryPollIntervalSec != 0 ||
159+ cfg.NetworkProcessMemoryConservativeThreshold != 0 ||
160+ cfg.NetworkProcessMemoryStrictThreshold != 0 ||
161+ cfg.NetworkProcessMemoryKillThreshold != -1
162+}
163+
164+// IsValidPerformanceProfile returns true if the profile name is recognized.
165+func IsValidPerformanceProfile(profile PerformanceProfile) bool {
166+ switch profile {
167+ case ProfileDefault, ProfileLite, ProfileMax, ProfileCustom, "":
168+ return true
169+ default:
170+ return false
171+ }
172+}
@@ -0,0 +1,172 @@
1+package config
2+
3+import "runtime"
4+
5+// ResolvedPerformanceSettings contains the computed performance settings
6+// after profile resolution. These are the actual values to apply.
7+type ResolvedPerformanceSettings struct {
8+ // Skia threading
9+ SkiaCPUPaintingThreads int
10+ SkiaGPUPaintingThreads int
11+ SkiaEnableCPURendering bool
12+
13+ // Web process memory pressure
14+ WebProcessMemoryLimitMB int
15+ WebProcessMemoryPollIntervalSec float64
16+ WebProcessMemoryConservativeThreshold float64
17+ WebProcessMemoryStrictThreshold float64
18+ WebProcessMemoryKillThreshold float64
19+
20+ // Network process memory pressure
21+ NetworkProcessMemoryLimitMB int
22+ NetworkProcessMemoryPollIntervalSec float64
23+ NetworkProcessMemoryConservativeThreshold float64
24+ NetworkProcessMemoryStrictThreshold float64
25+ NetworkProcessMemoryKillThreshold float64
26+
27+ // WebView pool
28+ WebViewPoolPrewarmCount int
29+}
30+
31+// ResolvePerformanceProfile computes the actual performance settings based on
32+// the selected profile. For ProfileCustom, it returns the user's configured values.
33+// For other profiles, it computes appropriate values.
34+func ResolvePerformanceProfile(cfg *PerformanceConfig) ResolvedPerformanceSettings {
35+ switch cfg.Profile {
36+ case ProfileLite:
37+ return resolveLiteProfile()
38+ case ProfileMax:
39+ return resolveMaxProfile()
40+ case ProfileCustom:
41+ return resolveCustomProfile(cfg)
42+ default: // ProfileDefault or empty
43+ return resolveDefaultProfile()
44+ }
45+}
46+
47+// resolveDefaultProfile returns WebKit defaults (no tuning).
48+func resolveDefaultProfile() ResolvedPerformanceSettings {
49+ return ResolvedPerformanceSettings{
50+ // All zeros/negatives mean "unset, use WebKit defaults"
51+ SkiaCPUPaintingThreads: 0,
52+ SkiaGPUPaintingThreads: -1,
53+ SkiaEnableCPURendering: false,
54+
55+ WebProcessMemoryLimitMB: 0,
56+ WebProcessMemoryPollIntervalSec: 0,
57+ WebProcessMemoryConservativeThreshold: 0,
58+ WebProcessMemoryStrictThreshold: 0,
59+ WebProcessMemoryKillThreshold: -1,
60+
61+ NetworkProcessMemoryLimitMB: 0,
62+ NetworkProcessMemoryPollIntervalSec: 0,
63+ NetworkProcessMemoryConservativeThreshold: 0,
64+ NetworkProcessMemoryStrictThreshold: 0,
65+ NetworkProcessMemoryKillThreshold: -1,
66+
67+ WebViewPoolPrewarmCount: 4,
68+ }
69+}
70+
71+// resolveLiteProfile returns settings optimized for low-RAM systems.
72+func resolveLiteProfile() ResolvedPerformanceSettings {
73+ return ResolvedPerformanceSettings{
74+ SkiaCPUPaintingThreads: 2,
75+ SkiaGPUPaintingThreads: -1, // unset
76+ SkiaEnableCPURendering: false,
77+
78+ WebProcessMemoryLimitMB: 512,
79+ WebProcessMemoryPollIntervalSec: 0, // use WebKit default (30s)
80+ WebProcessMemoryConservativeThreshold: 0.25,
81+ WebProcessMemoryStrictThreshold: 0.4,
82+ WebProcessMemoryKillThreshold: 0.8,
83+
84+ NetworkProcessMemoryLimitMB: 256,
85+ NetworkProcessMemoryPollIntervalSec: 0,
86+ NetworkProcessMemoryConservativeThreshold: 0.25,
87+ NetworkProcessMemoryStrictThreshold: 0.4,
88+ NetworkProcessMemoryKillThreshold: 0.8,
89+
90+ WebViewPoolPrewarmCount: 2,
91+ }
92+}
93+
94+// resolveMaxProfile returns settings optimized for maximum responsiveness.
95+func resolveMaxProfile() ResolvedPerformanceSettings {
96+ // Use half of available CPUs, minimum 4
97+ cpuThreads := runtime.NumCPU() / 2
98+ if cpuThreads < 4 {
99+ cpuThreads = 4
100+ }
101+
102+ return ResolvedPerformanceSettings{
103+ SkiaCPUPaintingThreads: cpuThreads,
104+ SkiaGPUPaintingThreads: 2,
105+ SkiaEnableCPURendering: false,
106+
107+ WebProcessMemoryLimitMB: 2048,
108+ WebProcessMemoryPollIntervalSec: 0, // use WebKit default
109+ WebProcessMemoryConservativeThreshold: 0.5,
110+ WebProcessMemoryStrictThreshold: 0.7,
111+ WebProcessMemoryKillThreshold: -1, // never kill
112+
113+ NetworkProcessMemoryLimitMB: 512,
114+ NetworkProcessMemoryPollIntervalSec: 0,
115+ NetworkProcessMemoryConservativeThreshold: 0.5,
116+ NetworkProcessMemoryStrictThreshold: 0.7,
117+ NetworkProcessMemoryKillThreshold: -1,
118+
119+ WebViewPoolPrewarmCount: 8,
120+ }
121+}
122+
123+// resolveCustomProfile returns the user's configured values directly.
124+func resolveCustomProfile(cfg *PerformanceConfig) ResolvedPerformanceSettings {
125+ return ResolvedPerformanceSettings{
126+ SkiaCPUPaintingThreads: cfg.SkiaCPUPaintingThreads,
127+ SkiaGPUPaintingThreads: cfg.SkiaGPUPaintingThreads,
128+ SkiaEnableCPURendering: cfg.SkiaEnableCPURendering,
129+
130+ WebProcessMemoryLimitMB: cfg.WebProcessMemoryLimitMB,
131+ WebProcessMemoryPollIntervalSec: cfg.WebProcessMemoryPollIntervalSec,
132+ WebProcessMemoryConservativeThreshold: cfg.WebProcessMemoryConservativeThreshold,
133+ WebProcessMemoryStrictThreshold: cfg.WebProcessMemoryStrictThreshold,
134+ WebProcessMemoryKillThreshold: cfg.WebProcessMemoryKillThreshold,
135+
136+ NetworkProcessMemoryLimitMB: cfg.NetworkProcessMemoryLimitMB,
137+ NetworkProcessMemoryPollIntervalSec: cfg.NetworkProcessMemoryPollIntervalSec,
138+ NetworkProcessMemoryConservativeThreshold: cfg.NetworkProcessMemoryConservativeThreshold,
139+ NetworkProcessMemoryStrictThreshold: cfg.NetworkProcessMemoryStrictThreshold,
140+ NetworkProcessMemoryKillThreshold: cfg.NetworkProcessMemoryKillThreshold,
141+
142+ WebViewPoolPrewarmCount: cfg.WebViewPoolPrewarmCount,
143+ }
144+}
145+
146+// HasCustomPerformanceFields returns true if any individual performance tuning
147+// fields are set to non-default values. Used for validation.
148+func HasCustomPerformanceFields(cfg *PerformanceConfig) bool {
149+ return cfg.SkiaCPUPaintingThreads != 0 ||
150+ cfg.SkiaGPUPaintingThreads != -1 ||
151+ cfg.SkiaEnableCPURendering ||
152+ cfg.WebProcessMemoryLimitMB != 0 ||
153+ cfg.WebProcessMemoryPollIntervalSec != 0 ||
154+ cfg.WebProcessMemoryConservativeThreshold != 0 ||
155+ cfg.WebProcessMemoryStrictThreshold != 0 ||
156+ cfg.WebProcessMemoryKillThreshold != -1 ||
157+ cfg.NetworkProcessMemoryLimitMB != 0 ||
158+ cfg.NetworkProcessMemoryPollIntervalSec != 0 ||
159+ cfg.NetworkProcessMemoryConservativeThreshold != 0 ||
160+ cfg.NetworkProcessMemoryStrictThreshold != 0 ||
161+ cfg.NetworkProcessMemoryKillThreshold != -1
162+}
163+
164+// IsValidPerformanceProfile returns true if the profile name is recognized.
165+func IsValidPerformanceProfile(profile PerformanceProfile) bool {
166+ switch profile {
167+ case ProfileDefault, ProfileLite, ProfileMax, ProfileCustom, "":
168+ return true
169+ default:
170+ return false
171+ }
172+}
A internal/infrastructure/config/profile_test.go +211 -0
@@ -0,0 +1,211 @@
1+package config
2+
3+import (
4+ "runtime"
5+ "testing"
6+)
7+
8+func TestResolvePerformanceProfile_Default(t *testing.T) {
9+ cfg := &PerformanceConfig{
10+ Profile: ProfileDefault,
11+ }
12+
13+ result := ResolvePerformanceProfile(cfg)
14+
15+ // Default should have no tuning (unset values)
16+ if result.SkiaCPUPaintingThreads != 0 {
17+ t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
18+ }
19+ if result.SkiaGPUPaintingThreads != -1 {
20+ t.Errorf("expected SkiaGPUPaintingThreads=-1, got %d", result.SkiaGPUPaintingThreads)
21+ }
22+ if result.WebProcessMemoryLimitMB != 0 {
23+ t.Errorf("expected WebProcessMemoryLimitMB=0, got %d", result.WebProcessMemoryLimitMB)
24+ }
25+ if result.WebViewPoolPrewarmCount != 4 {
26+ t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
27+ }
28+}
29+
30+func TestResolvePerformanceProfile_Empty(t *testing.T) {
31+ cfg := &PerformanceConfig{
32+ Profile: "", // Empty should behave like default
33+ }
34+
35+ result := ResolvePerformanceProfile(cfg)
36+
37+ if result.SkiaCPUPaintingThreads != 0 {
38+ t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
39+ }
40+ if result.WebViewPoolPrewarmCount != 4 {
41+ t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
42+ }
43+}
44+
45+func TestResolvePerformanceProfile_Lite(t *testing.T) {
46+ cfg := &PerformanceConfig{
47+ Profile: ProfileLite,
48+ }
49+
50+ result := ResolvePerformanceProfile(cfg)
51+
52+ // Lite should have reduced resource usage
53+ if result.SkiaCPUPaintingThreads != 2 {
54+ t.Errorf("expected SkiaCPUPaintingThreads=2, got %d", result.SkiaCPUPaintingThreads)
55+ }
56+ if result.WebProcessMemoryLimitMB != 512 {
57+ t.Errorf("expected WebProcessMemoryLimitMB=512, got %d", result.WebProcessMemoryLimitMB)
58+ }
59+ if result.NetworkProcessMemoryLimitMB != 256 {
60+ t.Errorf("expected NetworkProcessMemoryLimitMB=256, got %d", result.NetworkProcessMemoryLimitMB)
61+ }
62+ if result.WebViewPoolPrewarmCount != 2 {
63+ t.Errorf("expected WebViewPoolPrewarmCount=2, got %d", result.WebViewPoolPrewarmCount)
64+ }
65+ if result.WebProcessMemoryKillThreshold != 0.8 {
66+ t.Errorf("expected WebProcessMemoryKillThreshold=0.8, got %f", result.WebProcessMemoryKillThreshold)
67+ }
68+}
69+
70+func TestResolvePerformanceProfile_Max(t *testing.T) {
71+ cfg := &PerformanceConfig{
72+ Profile: ProfileMax,
73+ }
74+
75+ result := ResolvePerformanceProfile(cfg)
76+
77+ // Max should have high resource allocation
78+ expectedCPUThreads := runtime.NumCPU() / 2
79+ if expectedCPUThreads < 4 {
80+ expectedCPUThreads = 4
81+ }
82+ if result.SkiaCPUPaintingThreads != expectedCPUThreads {
83+ t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)
84+ }
85+ if result.SkiaGPUPaintingThreads != 2 {
86+ t.Errorf("expected SkiaGPUPaintingThreads=2, got %d", result.SkiaGPUPaintingThreads)
87+ }
88+ if result.WebProcessMemoryLimitMB != 2048 {
89+ t.Errorf("expected WebProcessMemoryLimitMB=2048, got %d", result.WebProcessMemoryLimitMB)
90+ }
91+ if result.WebViewPoolPrewarmCount != 8 {
92+ t.Errorf("expected WebViewPoolPrewarmCount=8, got %d", result.WebViewPoolPrewarmCount)
93+ }
94+ if result.WebProcessMemoryKillThreshold != -1 {
95+ t.Errorf("expected WebProcessMemoryKillThreshold=-1 (never kill), got %f", result.WebProcessMemoryKillThreshold)
96+ }
97+}
98+
99+func TestResolvePerformanceProfile_Custom(t *testing.T) {
100+ cfg := &PerformanceConfig{
101+ Profile: ProfileCustom,
102+ SkiaCPUPaintingThreads: 6,
103+ SkiaGPUPaintingThreads: 3,
104+ SkiaEnableCPURendering: true,
105+ WebProcessMemoryLimitMB: 1024,
106+ WebProcessMemoryConservativeThreshold: 0.4,
107+ WebViewPoolPrewarmCount: 5,
108+ }
109+
110+ result := ResolvePerformanceProfile(cfg)
111+
112+ // Custom should use user-specified values
113+ if result.SkiaCPUPaintingThreads != 6 {
114+ t.Errorf("expected SkiaCPUPaintingThreads=6, got %d", result.SkiaCPUPaintingThreads)
115+ }
116+ if result.SkiaGPUPaintingThreads != 3 {
117+ t.Errorf("expected SkiaGPUPaintingThreads=3, got %d", result.SkiaGPUPaintingThreads)
118+ }
119+ if !result.SkiaEnableCPURendering {
120+ t.Error("expected SkiaEnableCPURendering=true")
121+ }
122+ if result.WebProcessMemoryLimitMB != 1024 {
123+ t.Errorf("expected WebProcessMemoryLimitMB=1024, got %d", result.WebProcessMemoryLimitMB)
124+ }
125+ if result.WebViewPoolPrewarmCount != 5 {
126+ t.Errorf("expected WebViewPoolPrewarmCount=5, got %d", result.WebViewPoolPrewarmCount)
127+ }
128+}
129+
130+func TestHasCustomPerformanceFields(t *testing.T) {
131+ tests := []struct {
132+ name string
133+ cfg PerformanceConfig
134+ expected bool
135+ }{
136+ {
137+ name: "all defaults - no custom fields",
138+ cfg: PerformanceConfig{
139+ SkiaCPUPaintingThreads: 0,
140+ SkiaGPUPaintingThreads: -1,
141+ SkiaEnableCPURendering: false,
142+ WebProcessMemoryKillThreshold: -1,
143+ NetworkProcessMemoryKillThreshold: -1,
144+ },
145+ expected: false,
146+ },
147+ {
148+ name: "skia cpu threads set",
149+ cfg: PerformanceConfig{
150+ SkiaCPUPaintingThreads: 4,
151+ SkiaGPUPaintingThreads: -1,
152+ WebProcessMemoryKillThreshold: -1,
153+ NetworkProcessMemoryKillThreshold: -1,
154+ },
155+ expected: true,
156+ },
157+ {
158+ name: "memory limit set",
159+ cfg: PerformanceConfig{
160+ SkiaGPUPaintingThreads: -1,
161+ WebProcessMemoryLimitMB: 1024,
162+ WebProcessMemoryKillThreshold: -1,
163+ NetworkProcessMemoryKillThreshold: -1,
164+ },
165+ expected: true,
166+ },
167+ {
168+ name: "kill threshold changed from -1",
169+ cfg: PerformanceConfig{
170+ SkiaGPUPaintingThreads: -1,
171+ WebProcessMemoryKillThreshold: 0.9,
172+ NetworkProcessMemoryKillThreshold: -1,
173+ },
174+ expected: true,
175+ },
176+ }
177+
178+ for _, tt := range tests {
179+ t.Run(tt.name, func(t *testing.T) {
180+ result := HasCustomPerformanceFields(&tt.cfg)
181+ if result != tt.expected {
182+ t.Errorf("expected %v, got %v", tt.expected, result)
183+ }
184+ })
185+ }
186+}
187+
188+func TestIsValidPerformanceProfile(t *testing.T) {
189+ tests := []struct {
190+ profile PerformanceProfile
191+ expected bool
192+ }{
193+ {ProfileDefault, true},
194+ {ProfileLite, true},
195+ {ProfileMax, true},
196+ {ProfileCustom, true},
197+ {"", true}, // Empty is valid (treated as default)
198+ {"invalid", false},
199+ {"LITE", false}, // Case sensitive
200+ {"Maximum", false},
201+ }
202+
203+ for _, tt := range tests {
204+ t.Run(string(tt.profile), func(t *testing.T) {
205+ result := IsValidPerformanceProfile(tt.profile)
206+ if result != tt.expected {
207+ t.Errorf("IsValidPerformanceProfile(%q): expected %v, got %v", tt.profile, tt.expected, result)
208+ }
209+ })
210+ }
211+}
@@ -0,0 +1,211 @@
1+package config
2+
3+import (
4+ "runtime"
5+ "testing"
6+)
7+
8+func TestResolvePerformanceProfile_Default(t *testing.T) {
9+ cfg := &PerformanceConfig{
10+ Profile: ProfileDefault,
11+ }
12+
13+ result := ResolvePerformanceProfile(cfg)
14+
15+ // Default should have no tuning (unset values)
16+ if result.SkiaCPUPaintingThreads != 0 {
17+ t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
18+ }
19+ if result.SkiaGPUPaintingThreads != -1 {
20+ t.Errorf("expected SkiaGPUPaintingThreads=-1, got %d", result.SkiaGPUPaintingThreads)
21+ }
22+ if result.WebProcessMemoryLimitMB != 0 {
23+ t.Errorf("expected WebProcessMemoryLimitMB=0, got %d", result.WebProcessMemoryLimitMB)
24+ }
25+ if result.WebViewPoolPrewarmCount != 4 {
26+ t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
27+ }
28+}
29+
30+func TestResolvePerformanceProfile_Empty(t *testing.T) {
31+ cfg := &PerformanceConfig{
32+ Profile: "", // Empty should behave like default
33+ }
34+
35+ result := ResolvePerformanceProfile(cfg)
36+
37+ if result.SkiaCPUPaintingThreads != 0 {
38+ t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
39+ }
40+ if result.WebViewPoolPrewarmCount != 4 {
41+ t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
42+ }
43+}
44+
45+func TestResolvePerformanceProfile_Lite(t *testing.T) {
46+ cfg := &PerformanceConfig{
47+ Profile: ProfileLite,
48+ }
49+
50+ result := ResolvePerformanceProfile(cfg)
51+
52+ // Lite should have reduced resource usage
53+ if result.SkiaCPUPaintingThreads != 2 {
54+ t.Errorf("expected SkiaCPUPaintingThreads=2, got %d", result.SkiaCPUPaintingThreads)
55+ }
56+ if result.WebProcessMemoryLimitMB != 512 {
57+ t.Errorf("expected WebProcessMemoryLimitMB=512, got %d", result.WebProcessMemoryLimitMB)
58+ }
59+ if result.NetworkProcessMemoryLimitMB != 256 {
60+ t.Errorf("expected NetworkProcessMemoryLimitMB=256, got %d", result.NetworkProcessMemoryLimitMB)
61+ }
62+ if result.WebViewPoolPrewarmCount != 2 {
63+ t.Errorf("expected WebViewPoolPrewarmCount=2, got %d", result.WebViewPoolPrewarmCount)
64+ }
65+ if result.WebProcessMemoryKillThreshold != 0.8 {
66+ t.Errorf("expected WebProcessMemoryKillThreshold=0.8, got %f", result.WebProcessMemoryKillThreshold)
67+ }
68+}
69+
70+func TestResolvePerformanceProfile_Max(t *testing.T) {
71+ cfg := &PerformanceConfig{
72+ Profile: ProfileMax,
73+ }
74+
75+ result := ResolvePerformanceProfile(cfg)
76+
77+ // Max should have high resource allocation
78+ expectedCPUThreads := runtime.NumCPU() / 2
79+ if expectedCPUThreads < 4 {
80+ expectedCPUThreads = 4
81+ }
82+ if result.SkiaCPUPaintingThreads != expectedCPUThreads {
83+ t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)
84+ }
85+ if result.SkiaGPUPaintingThreads != 2 {
86+ t.Errorf("expected SkiaGPUPaintingThreads=2, got %d", result.SkiaGPUPaintingThreads)
87+ }
88+ if result.WebProcessMemoryLimitMB != 2048 {
89+ t.Errorf("expected WebProcessMemoryLimitMB=2048, got %d", result.WebProcessMemoryLimitMB)
90+ }
91+ if result.WebViewPoolPrewarmCount != 8 {
92+ t.Errorf("expected WebViewPoolPrewarmCount=8, got %d", result.WebViewPoolPrewarmCount)
93+ }
94+ if result.WebProcessMemoryKillThreshold != -1 {
95+ t.Errorf("expected WebProcessMemoryKillThreshold=-1 (never kill), got %f", result.WebProcessMemoryKillThreshold)
96+ }
97+}
98+
99+func TestResolvePerformanceProfile_Custom(t *testing.T) {
100+ cfg := &PerformanceConfig{
101+ Profile: ProfileCustom,
102+ SkiaCPUPaintingThreads: 6,
103+ SkiaGPUPaintingThreads: 3,
104+ SkiaEnableCPURendering: true,
105+ WebProcessMemoryLimitMB: 1024,
106+ WebProcessMemoryConservativeThreshold: 0.4,
107+ WebViewPoolPrewarmCount: 5,
108+ }
109+
110+ result := ResolvePerformanceProfile(cfg)
111+
112+ // Custom should use user-specified values
113+ if result.SkiaCPUPaintingThreads != 6 {
114+ t.Errorf("expected SkiaCPUPaintingThreads=6, got %d", result.SkiaCPUPaintingThreads)
115+ }
116+ if result.SkiaGPUPaintingThreads != 3 {
117+ t.Errorf("expected SkiaGPUPaintingThreads=3, got %d", result.SkiaGPUPaintingThreads)
118+ }
119+ if !result.SkiaEnableCPURendering {
120+ t.Error("expected SkiaEnableCPURendering=true")
121+ }
122+ if result.WebProcessMemoryLimitMB != 1024 {
123+ t.Errorf("expected WebProcessMemoryLimitMB=1024, got %d", result.WebProcessMemoryLimitMB)
124+ }
125+ if result.WebViewPoolPrewarmCount != 5 {
126+ t.Errorf("expected WebViewPoolPrewarmCount=5, got %d", result.WebViewPoolPrewarmCount)
127+ }
128+}
129+
130+func TestHasCustomPerformanceFields(t *testing.T) {
131+ tests := []struct {
132+ name string
133+ cfg PerformanceConfig
134+ expected bool
135+ }{
136+ {
137+ name: "all defaults - no custom fields",
138+ cfg: PerformanceConfig{
139+ SkiaCPUPaintingThreads: 0,
140+ SkiaGPUPaintingThreads: -1,
141+ SkiaEnableCPURendering: false,
142+ WebProcessMemoryKillThreshold: -1,
143+ NetworkProcessMemoryKillThreshold: -1,
144+ },
145+ expected: false,
146+ },
147+ {
148+ name: "skia cpu threads set",
149+ cfg: PerformanceConfig{
150+ SkiaCPUPaintingThreads: 4,
151+ SkiaGPUPaintingThreads: -1,
152+ WebProcessMemoryKillThreshold: -1,
153+ NetworkProcessMemoryKillThreshold: -1,
154+ },
155+ expected: true,
156+ },
157+ {
158+ name: "memory limit set",
159+ cfg: PerformanceConfig{
160+ SkiaGPUPaintingThreads: -1,
161+ WebProcessMemoryLimitMB: 1024,
162+ WebProcessMemoryKillThreshold: -1,
163+ NetworkProcessMemoryKillThreshold: -1,
164+ },
165+ expected: true,
166+ },
167+ {
168+ name: "kill threshold changed from -1",
169+ cfg: PerformanceConfig{
170+ SkiaGPUPaintingThreads: -1,
171+ WebProcessMemoryKillThreshold: 0.9,
172+ NetworkProcessMemoryKillThreshold: -1,
173+ },
174+ expected: true,
175+ },
176+ }
177+
178+ for _, tt := range tests {
179+ t.Run(tt.name, func(t *testing.T) {
180+ result := HasCustomPerformanceFields(&tt.cfg)
181+ if result != tt.expected {
182+ t.Errorf("expected %v, got %v", tt.expected, result)
183+ }
184+ })
185+ }
186+}
187+
188+func TestIsValidPerformanceProfile(t *testing.T) {
189+ tests := []struct {
190+ profile PerformanceProfile
191+ expected bool
192+ }{
193+ {ProfileDefault, true},
194+ {ProfileLite, true},
195+ {ProfileMax, true},
196+ {ProfileCustom, true},
197+ {"", true}, // Empty is valid (treated as default)
198+ {"invalid", false},
199+ {"LITE", false}, // Case sensitive
200+ {"Maximum", false},
201+ }
202+
203+ for _, tt := range tests {
204+ t.Run(string(tt.profile), func(t *testing.T) {
205+ result := IsValidPerformanceProfile(tt.profile)
206+ if result != tt.expected {
207+ t.Errorf("IsValidPerformanceProfile(%q): expected %v, got %v", tt.profile, tt.expected, result)
208+ }
209+ })
210+ }
211+}
M internal/infrastructure/config/schema.go +77 -1
@@ -125,6 +125,20 @@ const (
125125 GLRenderingModeNone GLRenderingMode = "none"
126126 )
127127
128+// PerformanceProfile selects preset performance tuning settings.
129+type PerformanceProfile string
130+
131+const (
132+ // ProfileDefault uses WebKit defaults (no tuning applied).
133+ ProfileDefault PerformanceProfile = "default"
134+ // ProfileLite reduces resource usage for low-RAM systems.
135+ ProfileLite PerformanceProfile = "lite"
136+ // ProfileMax maximizes responsiveness for heavy pages (GitHub PRs, etc).
137+ ProfileMax PerformanceProfile = "max"
138+ // ProfileCustom allows full control over individual performance settings.
139+ ProfileCustom PerformanceProfile = "custom"
140+)
141+
128142 // ThemeDefault is the default theme setting (follows system).
129143 const ThemeDefault = "default"
130144
@@ -181,6 +195,12 @@ type HistoryConfig struct {
181195 // PerformanceConfig holds internal performance tuning options.
182196 // These are not exposed in dumb://config UI but can be set in config file.
183197 type PerformanceConfig struct {
198+ // Profile selects a preset performance configuration.
199+ // Values: "default" (no tuning), "lite" (low RAM), "max" (heavy pages), "custom" (manual control)
200+ // When profile is not "custom", individual tuning fields below are ignored and computed from the profile.
201+ // Default: "default"
202+ Profile PerformanceProfile `mapstructure:"profile" yaml:"profile" toml:"profile"`
203+
184204 // ZoomCacheSize is the number of domain zoom levels to cache in memory.
185205 // Higher values reduce database queries but use more memory.
186206 // Default: 256 domains (~20KB memory)
@@ -188,8 +208,64 @@ type PerformanceConfig struct {
188208
189209 // WebViewPoolPrewarmCount is the number of WebViews to pre-create at startup.
190210 // Higher values speed up initial tab creation but use more memory.
191- // Default: 4
211+ // Default: 4 (custom mode), computed from profile otherwise
192212 WebViewPoolPrewarmCount int `mapstructure:"webview_pool_prewarm_count" yaml:"webview_pool_prewarm_count" toml:"webview_pool_prewarm_count"` //nolint:lll // struct tags must stay on one line
213+
214+ // --- Skia rendering threads (env vars) ---
215+ // NOTE: Fields below only apply when Profile is "custom".
216+ // SkiaCPUPaintingThreads sets WEBKIT_SKIA_CPU_PAINTING_THREADS.
217+ // Default: 0 (unset, uses WebKit default)
218+ SkiaCPUPaintingThreads int `mapstructure:"skia_cpu_painting_threads" yaml:"skia_cpu_painting_threads" toml:"skia_cpu_painting_threads"` //nolint:lll // struct tags must stay on one line
219+
220+ // SkiaGPUPaintingThreads sets WEBKIT_SKIA_GPU_PAINTING_THREADS.
221+ // Default: -1 (unset). Value 0 disables GPU tile painting.
222+ SkiaGPUPaintingThreads int `mapstructure:"skia_gpu_painting_threads" yaml:"skia_gpu_painting_threads" toml:"skia_gpu_painting_threads"` //nolint:lll // struct tags must stay on one line
223+
224+ // SkiaEnableCPURendering forces CPU rendering via WEBKIT_SKIA_ENABLE_CPU_RENDERING=1.
225+ // Default: false
226+ SkiaEnableCPURendering bool `mapstructure:"skia_enable_cpu_rendering" yaml:"skia_enable_cpu_rendering" toml:"skia_enable_cpu_rendering"` //nolint:lll // struct tags must stay on one line
227+
228+ // --- Web process memory pressure ---
229+ // WebProcessMemoryLimitMB sets memory limit in MB for web processes.
230+ // Default: 0 (unset, uses WebKit default: system RAM capped at 3GB)
231+ WebProcessMemoryLimitMB int `mapstructure:"web_process_memory_limit_mb" yaml:"web_process_memory_limit_mb" toml:"web_process_memory_limit_mb"` //nolint:lll // struct tags must stay on one line
232+
233+ // WebProcessMemoryPollIntervalSec sets poll interval for memory checks.
234+ // Default: 0 (unset, uses WebKit default: 30 seconds)
235+ WebProcessMemoryPollIntervalSec float64 `mapstructure:"web_process_memory_poll_interval_sec" yaml:"web_process_memory_poll_interval_sec" toml:"web_process_memory_poll_interval_sec"` //nolint:lll // struct tags must stay on one line
236+
237+ // WebProcessMemoryConservativeThreshold sets threshold for conservative memory release.
238+ // Valid: (0, 1). Default: 0 (unset, uses WebKit default: 0.33)
239+ WebProcessMemoryConservativeThreshold float64 `mapstructure:"web_process_memory_conservative_threshold" yaml:"web_process_memory_conservative_threshold" toml:"web_process_memory_conservative_threshold"` //nolint:lll // struct tags must stay on one line
240+
241+ // WebProcessMemoryStrictThreshold sets threshold for strict memory release.
242+ // Valid: (0, 1). Default: 0 (unset, uses WebKit default: 0.5)
243+ WebProcessMemoryStrictThreshold float64 `mapstructure:"web_process_memory_strict_threshold" yaml:"web_process_memory_strict_threshold" toml:"web_process_memory_strict_threshold"` //nolint:lll // struct tags must stay on one line
244+
245+ // WebProcessMemoryKillThreshold sets threshold for killing processes.
246+ // Default: -1 (unset). Value 0 means never kill.
247+ WebProcessMemoryKillThreshold float64 `mapstructure:"web_process_memory_kill_threshold" yaml:"web_process_memory_kill_threshold" toml:"web_process_memory_kill_threshold"` //nolint:lll // struct tags must stay on one line
248+
249+ // --- Network process memory pressure ---
250+ // NetworkProcessMemoryLimitMB sets memory limit in MB for network process.
251+ // Default: 0 (unset)
252+ NetworkProcessMemoryLimitMB int `mapstructure:"network_process_memory_limit_mb" yaml:"network_process_memory_limit_mb" toml:"network_process_memory_limit_mb"` //nolint:lll // struct tags must stay on one line
253+
254+ // NetworkProcessMemoryPollIntervalSec sets poll interval for memory checks.
255+ // Default: 0 (unset)
256+ NetworkProcessMemoryPollIntervalSec float64 `mapstructure:"network_process_memory_poll_interval_sec" yaml:"network_process_memory_poll_interval_sec" toml:"network_process_memory_poll_interval_sec"` //nolint:lll // struct tags must stay on one line
257+
258+ // NetworkProcessMemoryConservativeThreshold sets threshold for conservative memory release.
259+ // Valid: (0, 1). Default: 0 (unset)
260+ NetworkProcessMemoryConservativeThreshold float64 `mapstructure:"network_process_memory_conservative_threshold" yaml:"network_process_memory_conservative_threshold" toml:"network_process_memory_conservative_threshold"` //nolint:lll // struct tags must stay on one line
261+
262+ // NetworkProcessMemoryStrictThreshold sets threshold for strict memory release.
263+ // Valid: (0, 1). Default: 0 (unset)
264+ NetworkProcessMemoryStrictThreshold float64 `mapstructure:"network_process_memory_strict_threshold" yaml:"network_process_memory_strict_threshold" toml:"network_process_memory_strict_threshold"` //nolint:lll // struct tags must stay on one line
265+
266+ // NetworkProcessMemoryKillThreshold sets threshold for killing network process.
267+ // Default: -1 (unset). Value 0 means never kill.
268+ NetworkProcessMemoryKillThreshold float64 `mapstructure:"network_process_memory_kill_threshold" yaml:"network_process_memory_kill_threshold" toml:"network_process_memory_kill_threshold"` //nolint:lll // struct tags must stay on one line
193269 }
194270
195271 // SearchShortcut represents a search shortcut configuration.
@@ -125,6 +125,20 @@ const (
125 GLRenderingModeNone GLRenderingMode = "none"125 GLRenderingModeNone GLRenderingMode = "none"
126 )126 )
127 127
128+// PerformanceProfile selects preset performance tuning settings.
129+type PerformanceProfile string
130+
131+const (
132+ // ProfileDefault uses WebKit defaults (no tuning applied).
133+ ProfileDefault PerformanceProfile = "default"
134+ // ProfileLite reduces resource usage for low-RAM systems.
135+ ProfileLite PerformanceProfile = "lite"
136+ // ProfileMax maximizes responsiveness for heavy pages (GitHub PRs, etc).
137+ ProfileMax PerformanceProfile = "max"
138+ // ProfileCustom allows full control over individual performance settings.
139+ ProfileCustom PerformanceProfile = "custom"
140+)
141+
128 // ThemeDefault is the default theme setting (follows system).142 // ThemeDefault is the default theme setting (follows system).
129 const ThemeDefault = "default"143 const ThemeDefault = "default"
130 144
@@ -181,6 +195,12 @@ type HistoryConfig struct {
181 // PerformanceConfig holds internal performance tuning options.195 // PerformanceConfig holds internal performance tuning options.
182 // These are not exposed in dumb://config UI but can be set in config file.196 // These are not exposed in dumb://config UI but can be set in config file.
183 type PerformanceConfig struct {197 type PerformanceConfig struct {
198+ // Profile selects a preset performance configuration.
199+ // Values: "default" (no tuning), "lite" (low RAM), "max" (heavy pages), "custom" (manual control)
200+ // When profile is not "custom", individual tuning fields below are ignored and computed from the profile.
201+ // Default: "default"
202+ Profile PerformanceProfile `mapstructure:"profile" yaml:"profile" toml:"profile"`
203+
184 // ZoomCacheSize is the number of domain zoom levels to cache in memory.204 // ZoomCacheSize is the number of domain zoom levels to cache in memory.
185 // Higher values reduce database queries but use more memory.205 // Higher values reduce database queries but use more memory.
186 // Default: 256 domains (~20KB memory)206 // Default: 256 domains (~20KB memory)
@@ -188,8 +208,64 @@ type PerformanceConfig struct {
188 208
189 // WebViewPoolPrewarmCount is the number of WebViews to pre-create at startup.209 // WebViewPoolPrewarmCount is the number of WebViews to pre-create at startup.
190 // Higher values speed up initial tab creation but use more memory.210 // Higher values speed up initial tab creation but use more memory.
191- // Default: 4211+ // Default: 4 (custom mode), computed from profile otherwise
192 WebViewPoolPrewarmCount int `mapstructure:"webview_pool_prewarm_count" yaml:"webview_pool_prewarm_count" toml:"webview_pool_prewarm_count"` //nolint:lll // struct tags must stay on one line212 WebViewPoolPrewarmCount int `mapstructure:"webview_pool_prewarm_count" yaml:"webview_pool_prewarm_count" toml:"webview_pool_prewarm_count"` //nolint:lll // struct tags must stay on one line
213+
214+ // --- Skia rendering threads (env vars) ---
215+ // NOTE: Fields below only apply when Profile is "custom".
216+ // SkiaCPUPaintingThreads sets WEBKIT_SKIA_CPU_PAINTING_THREADS.
217+ // Default: 0 (unset, uses WebKit default)
218+ SkiaCPUPaintingThreads int `mapstructure:"skia_cpu_painting_threads" yaml:"skia_cpu_painting_threads" toml:"skia_cpu_painting_threads"` //nolint:lll // struct tags must stay on one line
219+
220+ // SkiaGPUPaintingThreads sets WEBKIT_SKIA_GPU_PAINTING_THREADS.
221+ // Default: -1 (unset). Value 0 disables GPU tile painting.
222+ SkiaGPUPaintingThreads int `mapstructure:"skia_gpu_painting_threads" yaml:"skia_gpu_painting_threads" toml:"skia_gpu_painting_threads"` //nolint:lll // struct tags must stay on one line
223+
224+ // SkiaEnableCPURendering forces CPU rendering via WEBKIT_SKIA_ENABLE_CPU_RENDERING=1.
225+ // Default: false
226+ SkiaEnableCPURendering bool `mapstructure:"skia_enable_cpu_rendering" yaml:"skia_enable_cpu_rendering" toml:"skia_enable_cpu_rendering"` //nolint:lll // struct tags must stay on one line
227+
228+ // --- Web process memory pressure ---
229+ // WebProcessMemoryLimitMB sets memory limit in MB for web processes.
230+ // Default: 0 (unset, uses WebKit default: system RAM capped at 3GB)
231+ WebProcessMemoryLimitMB int `mapstructure:"web_process_memory_limit_mb" yaml:"web_process_memory_limit_mb" toml:"web_process_memory_limit_mb"` //nolint:lll // struct tags must stay on one line
232+
233+ // WebProcessMemoryPollIntervalSec sets poll interval for memory checks.
234+ // Default: 0 (unset, uses WebKit default: 30 seconds)
235+ WebProcessMemoryPollIntervalSec float64 `mapstructure:"web_process_memory_poll_interval_sec" yaml:"web_process_memory_poll_interval_sec" toml:"web_process_memory_poll_interval_sec"` //nolint:lll // struct tags must stay on one line
236+
237+ // WebProcessMemoryConservativeThreshold sets threshold for conservative memory release.
238+ // Valid: (0, 1). Default: 0 (unset, uses WebKit default: 0.33)
239+ WebProcessMemoryConservativeThreshold float64 `mapstructure:"web_process_memory_conservative_threshold" yaml:"web_process_memory_conservative_threshold" toml:"web_process_memory_conservative_threshold"` //nolint:lll // struct tags must stay on one line
240+
241+ // WebProcessMemoryStrictThreshold sets threshold for strict memory release.
242+ // Valid: (0, 1). Default: 0 (unset, uses WebKit default: 0.5)
243+ WebProcessMemoryStrictThreshold float64 `mapstructure:"web_process_memory_strict_threshold" yaml:"web_process_memory_strict_threshold" toml:"web_process_memory_strict_threshold"` //nolint:lll // struct tags must stay on one line
244+
245+ // WebProcessMemoryKillThreshold sets threshold for killing processes.
246+ // Default: -1 (unset). Value 0 means never kill.
247+ WebProcessMemoryKillThreshold float64 `mapstructure:"web_process_memory_kill_threshold" yaml:"web_process_memory_kill_threshold" toml:"web_process_memory_kill_threshold"` //nolint:lll // struct tags must stay on one line
248+
249+ // --- Network process memory pressure ---
250+ // NetworkProcessMemoryLimitMB sets memory limit in MB for network process.
251+ // Default: 0 (unset)
252+ NetworkProcessMemoryLimitMB int `mapstructure:"network_process_memory_limit_mb" yaml:"network_process_memory_limit_mb" toml:"network_process_memory_limit_mb"` //nolint:lll // struct tags must stay on one line
253+
254+ // NetworkProcessMemoryPollIntervalSec sets poll interval for memory checks.
255+ // Default: 0 (unset)
256+ NetworkProcessMemoryPollIntervalSec float64 `mapstructure:"network_process_memory_poll_interval_sec" yaml:"network_process_memory_poll_interval_sec" toml:"network_process_memory_poll_interval_sec"` //nolint:lll // struct tags must stay on one line
257+
258+ // NetworkProcessMemoryConservativeThreshold sets threshold for conservative memory release.
259+ // Valid: (0, 1). Default: 0 (unset)
260+ NetworkProcessMemoryConservativeThreshold float64 `mapstructure:"network_process_memory_conservative_threshold" yaml:"network_process_memory_conservative_threshold" toml:"network_process_memory_conservative_threshold"` //nolint:lll // struct tags must stay on one line
261+
262+ // NetworkProcessMemoryStrictThreshold sets threshold for strict memory release.
263+ // Valid: (0, 1). Default: 0 (unset)
264+ NetworkProcessMemoryStrictThreshold float64 `mapstructure:"network_process_memory_strict_threshold" yaml:"network_process_memory_strict_threshold" toml:"network_process_memory_strict_threshold"` //nolint:lll // struct tags must stay on one line
265+
266+ // NetworkProcessMemoryKillThreshold sets threshold for killing network process.
267+ // Default: -1 (unset). Value 0 means never kill.
268+ NetworkProcessMemoryKillThreshold float64 `mapstructure:"network_process_memory_kill_threshold" yaml:"network_process_memory_kill_threshold" toml:"network_process_memory_kill_threshold"` //nolint:lll // struct tags must stay on one line
193 }269 }
194 270
195 // SearchShortcut represents a search shortcut configuration.271 // SearchShortcut represents a search shortcut configuration.
M internal/infrastructure/config/validation.go +26 -0
@@ -28,6 +28,7 @@ func validateConfig(config *Config) error {
2828 validationErrors = append(validationErrors, validateRendering(config)...)
2929 validationErrors = append(validationErrors, validateColorScheme(config)...)
3030 validationErrors = append(validationErrors, validateSession(config)...)
31+ validationErrors = append(validationErrors, validatePerformanceProfile(config)...)
3132
3233 // If there are validation errors, return them
3334 if len(validationErrors) > 0 {
@@ -347,3 +348,28 @@ func validateSession(config *Config) []string {
347348 }
348349 return validationErrors
349350 }
351+
352+func validatePerformanceProfile(config *Config) []string {
353+ var validationErrors []string
354+
355+ // Validate profile name
356+ if !IsValidPerformanceProfile(config.Performance.Profile) {
357+ validationErrors = append(validationErrors, fmt.Sprintf(
358+ "performance.profile must be one of: default, lite, max, custom (got: %s)",
359+ config.Performance.Profile,
360+ ))
361+ }
362+
363+ // When profile is not "custom", warn if individual fields are set
364+ // (they will be ignored in favor of profile-computed values)
365+ if config.Performance.Profile != ProfileCustom && config.Performance.Profile != "" {
366+ if HasCustomPerformanceFields(&config.Performance) {
367+ validationErrors = append(validationErrors,
368+ "performance tuning fields (skia_*, *_memory_*) are ignored when profile is not 'custom'; "+
369+ "set profile = \"custom\" to use individual field values",
370+ )
371+ }
372+ }
373+
374+ return validationErrors
375+}
@@ -28,6 +28,7 @@ func validateConfig(config *Config) error {
28 validationErrors = append(validationErrors, validateRendering(config)...)28 validationErrors = append(validationErrors, validateRendering(config)...)
29 validationErrors = append(validationErrors, validateColorScheme(config)...)29 validationErrors = append(validationErrors, validateColorScheme(config)...)
30 validationErrors = append(validationErrors, validateSession(config)...)30 validationErrors = append(validationErrors, validateSession(config)...)
31+ validationErrors = append(validationErrors, validatePerformanceProfile(config)...)
31 32
32 // If there are validation errors, return them33 // If there are validation errors, return them
33 if len(validationErrors) > 0 {34 if len(validationErrors) > 0 {
@@ -347,3 +348,28 @@ func validateSession(config *Config) []string {
347 }348 }
348 return validationErrors349 return validationErrors
349 }350 }
351+
352+func validatePerformanceProfile(config *Config) []string {
353+ var validationErrors []string
354+
355+ // Validate profile name
356+ if !IsValidPerformanceProfile(config.Performance.Profile) {
357+ validationErrors = append(validationErrors, fmt.Sprintf(
358+ "performance.profile must be one of: default, lite, max, custom (got: %s)",
359+ config.Performance.Profile,
360+ ))
361+ }
362+
363+ // When profile is not "custom", warn if individual fields are set
364+ // (they will be ignored in favor of profile-computed values)
365+ if config.Performance.Profile != ProfileCustom && config.Performance.Profile != "" {
366+ if HasCustomPerformanceFields(&config.Performance) {
367+ validationErrors = append(validationErrors,
368+ "performance tuning fields (skia_*, *_memory_*) are ignored when profile is not 'custom'; "+
369+ "set profile = \"custom\" to use individual field values",
370+ )
371+ }
372+ }
373+
374+ return validationErrors
375+}
M internal/infrastructure/webkit/scheme_handler.go +8 -0
@@ -76,8 +76,13 @@ type configAppearancePayload struct {
7676 DarkPalette config.ColorPalette `json:"dark_palette"`
7777 }
7878
79+type configPerformancePayload struct {
80+ Profile string `json:"profile"`
81+}
82+
7983 type configPayload struct {
8084 Appearance configAppearancePayload `json:"appearance"`
85+ Performance configPerformancePayload `json:"performance"`
8186 DefaultUIScale float64 `json:"default_ui_scale"`
8287 DefaultSearchEngine string `json:"default_search_engine"`
8388 SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
@@ -151,6 +156,9 @@ func buildConfigResponse(cfg *config.Config) *SchemeResponse {
151156 LightPalette: cfg.Appearance.LightPalette,
152157 DarkPalette: cfg.Appearance.DarkPalette,
153158 },
159+ Performance: configPerformancePayload{
160+ Profile: string(cfg.Performance.Profile),
161+ },
154162 }
155163
156164 data, err := json.Marshal(resp)
@@ -76,8 +76,13 @@ type configAppearancePayload struct {
76 DarkPalette config.ColorPalette `json:"dark_palette"`76 DarkPalette config.ColorPalette `json:"dark_palette"`
77 }77 }
78 78
79+type configPerformancePayload struct {
80+ Profile string `json:"profile"`
81+}
82+
79 type configPayload struct {83 type configPayload struct {
80 Appearance configAppearancePayload `json:"appearance"`84 Appearance configAppearancePayload `json:"appearance"`
85+ Performance configPerformancePayload `json:"performance"`
81 DefaultUIScale float64 `json:"default_ui_scale"`86 DefaultUIScale float64 `json:"default_ui_scale"`
82 DefaultSearchEngine string `json:"default_search_engine"`87 DefaultSearchEngine string `json:"default_search_engine"`
83 SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`88 SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"`
@@ -151,6 +156,9 @@ func buildConfigResponse(cfg *config.Config) *SchemeResponse {
151 LightPalette: cfg.Appearance.LightPalette,156 LightPalette: cfg.Appearance.LightPalette,
152 DarkPalette: cfg.Appearance.DarkPalette,157 DarkPalette: cfg.Appearance.DarkPalette,
153 },158 },
159+ Performance: configPerformancePayload{
160+ Profile: string(cfg.Performance.Profile),
161+ },
154 }162 }
155 163
156 data, err := json.Marshal(resp)164 data, err := json.Marshal(resp)

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