feat(webkit): implement NewEngine constructor absorbing bootstrap logic
- commit
- 324f8f31a34445fbec3134af7637d50e39203d0c
- author
- brice · · 2026-03-15 16:25 UTC
- parent
- f7e5518
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +272 -0
A internal/infrastructure/webkit/engine_init.go +272 -0
| @@ -0,0 +1,272 @@ | |||
| 1 | + | package webkit | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "path/filepath" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/assets" | |
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/env" | |
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 13 | + | "github.com/bnema/dumber/internal/logging" | |
| 14 | + | "github.com/bnema/dumber/internal/ui/theme" | |
| 15 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 16 | + | "github.com/rs/zerolog" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // NewEngine creates and initializes all WebKit engine components. | |
| 20 | + | // It absorbs the initialization logic previously found in bootstrap.BuildWebKitStack. | |
| 21 | + | func NewEngine( | |
| 22 | + | ctx context.Context, | |
| 23 | + | cfg *config.Config, | |
| 24 | + | opts port.EngineOptions, | |
| 25 | + | wkCfg WebKitEngineConfig, | |
| 26 | + | themeManager *theme.Manager, | |
| 27 | + | colorResolver port.ColorSchemeResolver, | |
| 28 | + | logger zerolog.Logger, | |
| 29 | + | ) (*Engine, error) { | |
| 30 | + | // --- Hardware survey and performance profile resolution --- | |
| 31 | + | perfSettings := engineSurveyHardwareAndResolveProfile(ctx, cfg, logger) | |
| 32 | + | ||
| 33 | + | // --- Configure rendering environment (must be before GTK/WebKit init) --- | |
| 34 | + | engineConfigureRenderingEnvironment(ctx, cfg, wkCfg, &perfSettings, logger) | |
| 35 | + | logging.Trace().Mark("render_env") | |
| 36 | + | ||
| 37 | + | // --- Build WebKitContextOptions from opts + wkCfg + perfSettings --- | |
| 38 | + | wkOpts := engineBuildContextOptions(opts, wkCfg, &perfSettings) | |
| 39 | + | logger.Info(). | |
| 40 | + | Str("cookie_policy", string(wkOpts.CookiePolicy)). | |
| 41 | + | Bool("itp_enabled", wkOpts.ITPEnabled). | |
| 42 | + | Msg("webkit privacy configuration") | |
| 43 | + | ||
| 44 | + | wkCtx, err := NewWebKitContextWithOptions(ctx, wkOpts) | |
| 45 | + | if err != nil { | |
| 46 | + | return nil, err | |
| 47 | + | } | |
| 48 | + | logging.Trace().Mark("webkit_context") | |
| 49 | + | ||
| 50 | + | // --- Filter manager --- | |
| 51 | + | filterManager := engineInitFilterManager(ctx, cfg, opts.DataDir, logger) | |
| 52 | + | ||
| 53 | + | // --- Scheme handler --- | |
| 54 | + | schemeHandler := NewDumbSchemeHandler(ctx) | |
| 55 | + | schemeHandler.SetAssets(assets.WebUIAssets) | |
| 56 | + | schemeHandler.RegisterWithContext(wkCtx) | |
| 57 | + | ||
| 58 | + | // --- Settings, injector, message router --- | |
| 59 | + | settings := NewSettingsManager(ctx, cfg) | |
| 60 | + | injector := NewContentInjector(colorResolver) | |
| 61 | + | ||
| 62 | + | injector.SetAutoCopyConfigGetter(func() bool { | |
| 63 | + | return config.Get().Clipboard.AutoCopyOnSelection | |
| 64 | + | }) | |
| 65 | + | ||
| 66 | + | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | |
| 67 | + | themeCSSText := themeManager.GetWebUIThemeCSS() | |
| 68 | + | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil { | |
| 69 | + | logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS") | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | messageRouter := NewMessageRouter(ctx) | |
| 73 | + | logging.Trace().Mark("settings_manager") | |
| 74 | + | ||
| 75 | + | // --- WebView pool --- | |
| 76 | + | poolCfg := DefaultPoolConfig() | |
| 77 | + | if perfSettings.WebViewPoolPrewarmCount > 0 { | |
| 78 | + | poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount | |
| 79 | + | } | |
| 80 | + | pool := NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | |
| 81 | + | ||
| 82 | + | bgR, bgG, bgB, bgA := themeManager.GetBackgroundRGBA() | |
| 83 | + | pool.SetBackgroundColor(bgR, bgG, bgB, bgA) | |
| 84 | + | ||
| 85 | + | if filterManager != nil { | |
| 86 | + | pool.SetFilterApplier(filterManager) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | if gdk.DisplayGetDefault() != nil { | |
| 90 | + | if err := pool.PrewarmFirst(ctx); err != nil { | |
| 91 | + | logger.Warn().Err(err).Msg("failed to prewarm first webview, first tab may be slower") | |
| 92 | + | } | |
| 93 | + | } else { | |
| 94 | + | logger.Warn().Msg("skipping first webview prewarm: no GDK display available yet") | |
| 95 | + | } | |
| 96 | + | logging.Trace().Mark("pool_prewarm_first") | |
| 97 | + | ||
| 98 | + | // --- WebView factory --- | |
| 99 | + | factory := NewWebViewFactory(wkCtx, settings, pool, injector, messageRouter) | |
| 100 | + | factory.SetBackgroundColor(bgR, bgG, bgB, bgA) | |
| 101 | + | if filterManager != nil { | |
| 102 | + | factory.SetFilterApplier(filterManager) | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // --- Assemble Engine --- | |
| 106 | + | engine := &Engine{ | |
| 107 | + | wkCtx: wkCtx, | |
| 108 | + | settings: settings, | |
| 109 | + | injector: injector, | |
| 110 | + | messageRouter: messageRouter, | |
| 111 | + | pool: pool, | |
| 112 | + | factory: factory, | |
| 113 | + | filterManager: filterManager, | |
| 114 | + | schemeHandler: schemeHandler, | |
| 115 | + | schemePath: "dumb://", | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | return engine, nil | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // engineSurveyHardwareAndResolveProfile surveys hardware and resolves the performance profile. | |
| 122 | + | func engineSurveyHardwareAndResolveProfile(ctx context.Context, cfg *config.Config, logger zerolog.Logger) config.ResolvedPerformanceSettings { | |
| 123 | + | hwSurveyor := env.NewHardwareSurveyor() | |
| 124 | + | hwInfo := hwSurveyor.Survey(ctx) | |
| 125 | + | logging.Trace().Mark("hardware_survey") | |
| 126 | + | logger.Info(). | |
| 127 | + | Int("cpu_cores", hwInfo.CPUCores). | |
| 128 | + | Int("cpu_threads", hwInfo.CPUThreads). | |
| 129 | + | Uint64("ram_mb", hwInfo.TotalRAM/(1024*1024)). | |
| 130 | + | Str("gpu_vendor", string(hwInfo.GPUVendor)). | |
| 131 | + | Uint64("vram_mb", hwInfo.VRAM/(1024*1024)). | |
| 132 | + | Msg("hardware survey completed") | |
| 133 | + | ||
| 134 | + | perfSettings := config.ResolvePerformanceProfile(&cfg.Performance, &hwInfo) | |
| 135 | + | logging.Trace().Mark("performance_profile") | |
| 136 | + | logger.Info(). | |
| 137 | + | Str("profile", string(cfg.Performance.Profile)). | |
| 138 | + | Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads). | |
| 139 | + | Int("skia_gpu_threads", perfSettings.SkiaGPUPaintingThreads). | |
| 140 | + | Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount). | |
| 141 | + | Msg("resolved performance profile") | |
| 142 | + | ||
| 143 | + | return perfSettings | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // engineConfigureRenderingEnvironment sets up the rendering environment. | |
| 147 | + | // Environment variables must be set before GTK/WebKit/GStreamer initializes. | |
| 148 | + | func engineConfigureRenderingEnvironment( | |
| 149 | + | ctx context.Context, | |
| 150 | + | cfg *config.Config, | |
| 151 | + | wkCfg WebKitEngineConfig, | |
| 152 | + | perfSettings *config.ResolvedPerformanceSettings, | |
| 153 | + | logger zerolog.Logger, | |
| 154 | + | ) { | |
| 155 | + | // Install GLib log handler FIRST if configured (must be before any GTK/GLib calls). | |
| 156 | + | if cfg.Logging.CaptureGTKLogs { | |
| 157 | + | enableDebug := cfg.Logging.Level == "debug" || cfg.Logging.Level == "trace" | |
| 158 | + | logging.InstallGLibLogHandler(ctx, logger, enableDebug) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | renderEnv := env.NewManager() | |
| 162 | + | gpuVendor := renderEnv.DetectGPUVendor(ctx) | |
| 163 | + | renderSettings := env.RenderingSettings{ | |
| 164 | + | // GStreamer settings | |
| 165 | + | ForceVSync: wkCfg.ForceVSync, | |
| 166 | + | GLRenderingMode: wkCfg.GLRenderingMode, | |
| 167 | + | GStreamerDebugLevel: wkCfg.GStreamerDebugLevel, | |
| 168 | + | // WebKit compositor settings | |
| 169 | + | DisableDMABufRenderer: wkCfg.DisableDMABufRenderer, | |
| 170 | + | ForceCompositingMode: wkCfg.ForceCompositingMode, | |
| 171 | + | DisableCompositingMode: wkCfg.DisableCompositingMode, | |
| 172 | + | // GTK/GSK settings | |
| 173 | + | GSKRenderer: wkCfg.GSKRenderer, | |
| 174 | + | DisableMipmaps: wkCfg.DisableMipmaps, | |
| 175 | + | PreferGL: wkCfg.PreferGL, | |
| 176 | + | // Debug settings | |
| 177 | + | ShowFPS: wkCfg.ShowFPS, | |
| 178 | + | SampleMemory: wkCfg.SampleMemory, | |
| 179 | + | DebugFrames: wkCfg.DebugFrames, | |
| 180 | + | // Skia rendering thread settings (from resolved performance profile) | |
| 181 | + | SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads, | |
| 182 | + | SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads, | |
| 183 | + | SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering, | |
| 184 | + | } | |
| 185 | + | if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil { | |
| 186 | + | logger.Warn().Err(err).Msg("failed to apply rendering environment") | |
| 187 | + | } | |
| 188 | + | logger.Info(). | |
| 189 | + | Str("gpu", string(gpuVendor)). | |
| 190 | + | Interface("vars", renderEnv.GetAppliedVars()). | |
| 191 | + | Msg("rendering environment configured") | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // engineBuildContextOptions builds port.WebKitContextOptions from EngineOptions, wkCfg and perfSettings. | |
| 195 | + | func engineBuildContextOptions( | |
| 196 | + | opts port.EngineOptions, | |
| 197 | + | wkCfg WebKitEngineConfig, | |
| 198 | + | perfSettings *config.ResolvedPerformanceSettings, | |
| 199 | + | ) port.WebKitContextOptions { | |
| 200 | + | cookiePolicy := port.WebKitCookiePolicyNoThirdParty | |
| 201 | + | if opts.CookiePolicy != "" { | |
| 202 | + | cookiePolicy = port.WebKitCookiePolicy(opts.CookiePolicy) | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | wkOpts := port.WebKitContextOptions{ | |
| 206 | + | DataDir: opts.DataDir, | |
| 207 | + | CacheDir: opts.CacheDir, | |
| 208 | + | CookiePolicy: cookiePolicy, | |
| 209 | + | ITPEnabled: wkCfg.ITPEnabled, | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | if opts.WebProcessMemory != nil { | |
| 213 | + | wkOpts.WebProcessMemory = opts.WebProcessMemory | |
| 214 | + | } else if engineHasWebProcessMemoryConfig(perfSettings) { | |
| 215 | + | wkOpts.WebProcessMemory = &port.MemoryPressureConfig{ | |
| 216 | + | MemoryLimitMB: perfSettings.WebProcessMemoryLimitMB, | |
| 217 | + | PollIntervalSec: perfSettings.WebProcessMemoryPollIntervalSec, | |
| 218 | + | ConservativeThreshold: perfSettings.WebProcessMemoryConservativeThreshold, | |
| 219 | + | StrictThreshold: perfSettings.WebProcessMemoryStrictThreshold, | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | if opts.NetworkProcessMemory != nil { | |
| 224 | + | wkOpts.NetworkProcessMemory = opts.NetworkProcessMemory | |
| 225 | + | } else if engineHasNetworkProcessMemoryConfig(perfSettings) { | |
| 226 | + | wkOpts.NetworkProcessMemory = &port.MemoryPressureConfig{ | |
| 227 | + | MemoryLimitMB: perfSettings.NetworkProcessMemoryLimitMB, | |
| 228 | + | PollIntervalSec: perfSettings.NetworkProcessMemoryPollIntervalSec, | |
| 229 | + | ConservativeThreshold: perfSettings.NetworkProcessMemoryConservativeThreshold, | |
| 230 | + | StrictThreshold: perfSettings.NetworkProcessMemoryStrictThreshold, | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | return wkOpts | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // engineInitFilterManager creates and initializes the content filter manager. | |
| 238 | + | func engineInitFilterManager(ctx context.Context, cfg *config.Config, dataDir string, logger zerolog.Logger) *filtering.Manager { | |
| 239 | + | filterStoreDir := filepath.Join(dataDir, "filters", "store") | |
| 240 | + | filterJSONDir := filepath.Join(dataDir, "filters", "json") | |
| 241 | + | filterManager, err := filtering.NewManager(filtering.ManagerConfig{ | |
| 242 | + | StoreDir: filterStoreDir, | |
| 243 | + | JSONDir: filterJSONDir, | |
| 244 | + | Enabled: cfg.ContentFiltering.Enabled, | |
| 245 | + | AutoUpdate: cfg.ContentFiltering.AutoUpdate, | |
| 246 | + | }) | |
| 247 | + | if err != nil { | |
| 248 | + | logger.Warn().Err(err).Msg("failed to create filter manager, continuing without content filtering") | |
| 249 | + | return nil | |
| 250 | + | } | |
| 251 | + | if err := filterManager.Initialize(ctx); err != nil { | |
| 252 | + | logger.Warn().Err(err).Msg("failed to initialize filters, will load async") | |
| 253 | + | } | |
| 254 | + | logging.Trace().Mark("filter_manager") | |
| 255 | + | return filterManager | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | // engineHasWebProcessMemoryConfig returns true if any web process memory setting is configured. | |
| 259 | + | func engineHasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool { | |
| 260 | + | return p.WebProcessMemoryLimitMB > 0 || | |
| 261 | + | p.WebProcessMemoryPollIntervalSec > 0 || | |
| 262 | + | p.WebProcessMemoryConservativeThreshold > 0 || | |
| 263 | + | p.WebProcessMemoryStrictThreshold > 0 | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | // engineHasNetworkProcessMemoryConfig returns true if any network process memory setting is configured. | |
| 267 | + | func engineHasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool { | |
| 268 | + | return p.NetworkProcessMemoryLimitMB > 0 || | |
| 269 | + | p.NetworkProcessMemoryPollIntervalSec > 0 || | |
| 270 | + | p.NetworkProcessMemoryConservativeThreshold > 0 || | |
| 271 | + | p.NetworkProcessMemoryStrictThreshold > 0 | |
| 272 | + | } | |
| @@ -0,0 +1,272 @@ | |||||
| 1 | + | package webkit | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/assets" | |||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 9 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/env" | |||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/filtering" | |||
| 13 | + | "github.com/bnema/dumber/internal/logging" | |||
| 14 | + | "github.com/bnema/dumber/internal/ui/theme" | |||
| 15 | + | "github.com/jwijenbergh/puregotk/v4/gdk" | |||
| 16 | + | "github.com/rs/zerolog" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | // NewEngine creates and initializes all WebKit engine components. | |||
| 20 | + | // It absorbs the initialization logic previously found in bootstrap.BuildWebKitStack. | |||
| 21 | + | func NewEngine( | |||
| 22 | + | ctx context.Context, | |||
| 23 | + | cfg *config.Config, | |||
| 24 | + | opts port.EngineOptions, | |||
| 25 | + | wkCfg WebKitEngineConfig, | |||
| 26 | + | themeManager *theme.Manager, | |||
| 27 | + | colorResolver port.ColorSchemeResolver, | |||
| 28 | + | logger zerolog.Logger, | |||
| 29 | + | ) (*Engine, error) { | |||
| 30 | + | // --- Hardware survey and performance profile resolution --- | |||
| 31 | + | perfSettings := engineSurveyHardwareAndResolveProfile(ctx, cfg, logger) | |||
| 32 | + | ||||
| 33 | + | // --- Configure rendering environment (must be before GTK/WebKit init) --- | |||
| 34 | + | engineConfigureRenderingEnvironment(ctx, cfg, wkCfg, &perfSettings, logger) | |||
| 35 | + | logging.Trace().Mark("render_env") | |||
| 36 | + | ||||
| 37 | + | // --- Build WebKitContextOptions from opts + wkCfg + perfSettings --- | |||
| 38 | + | wkOpts := engineBuildContextOptions(opts, wkCfg, &perfSettings) | |||
| 39 | + | logger.Info(). | |||
| 40 | + | Str("cookie_policy", string(wkOpts.CookiePolicy)). | |||
| 41 | + | Bool("itp_enabled", wkOpts.ITPEnabled). | |||
| 42 | + | Msg("webkit privacy configuration") | |||
| 43 | + | ||||
| 44 | + | wkCtx, err := NewWebKitContextWithOptions(ctx, wkOpts) | |||
| 45 | + | if err != nil { | |||
| 46 | + | return nil, err | |||
| 47 | + | } | |||
| 48 | + | logging.Trace().Mark("webkit_context") | |||
| 49 | + | ||||
| 50 | + | // --- Filter manager --- | |||
| 51 | + | filterManager := engineInitFilterManager(ctx, cfg, opts.DataDir, logger) | |||
| 52 | + | ||||
| 53 | + | // --- Scheme handler --- | |||
| 54 | + | schemeHandler := NewDumbSchemeHandler(ctx) | |||
| 55 | + | schemeHandler.SetAssets(assets.WebUIAssets) | |||
| 56 | + | schemeHandler.RegisterWithContext(wkCtx) | |||
| 57 | + | ||||
| 58 | + | // --- Settings, injector, message router --- | |||
| 59 | + | settings := NewSettingsManager(ctx, cfg) | |||
| 60 | + | injector := NewContentInjector(colorResolver) | |||
| 61 | + | ||||
| 62 | + | injector.SetAutoCopyConfigGetter(func() bool { | |||
| 63 | + | return config.Get().Clipboard.AutoCopyOnSelection | |||
| 64 | + | }) | |||
| 65 | + | ||||
| 66 | + | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | |||
| 67 | + | themeCSSText := themeManager.GetWebUIThemeCSS() | |||
| 68 | + | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil { | |||
| 69 | + | logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS") | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | messageRouter := NewMessageRouter(ctx) | |||
| 73 | + | logging.Trace().Mark("settings_manager") | |||
| 74 | + | ||||
| 75 | + | // --- WebView pool --- | |||
| 76 | + | poolCfg := DefaultPoolConfig() | |||
| 77 | + | if perfSettings.WebViewPoolPrewarmCount > 0 { | |||
| 78 | + | poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount | |||
| 79 | + | } | |||
| 80 | + | pool := NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | |||
| 81 | + | ||||
| 82 | + | bgR, bgG, bgB, bgA := themeManager.GetBackgroundRGBA() | |||
| 83 | + | pool.SetBackgroundColor(bgR, bgG, bgB, bgA) | |||
| 84 | + | ||||
| 85 | + | if filterManager != nil { | |||
| 86 | + | pool.SetFilterApplier(filterManager) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | if gdk.DisplayGetDefault() != nil { | |||
| 90 | + | if err := pool.PrewarmFirst(ctx); err != nil { | |||
| 91 | + | logger.Warn().Err(err).Msg("failed to prewarm first webview, first tab may be slower") | |||
| 92 | + | } | |||
| 93 | + | } else { | |||
| 94 | + | logger.Warn().Msg("skipping first webview prewarm: no GDK display available yet") | |||
| 95 | + | } | |||
| 96 | + | logging.Trace().Mark("pool_prewarm_first") | |||
| 97 | + | ||||
| 98 | + | // --- WebView factory --- | |||
| 99 | + | factory := NewWebViewFactory(wkCtx, settings, pool, injector, messageRouter) | |||
| 100 | + | factory.SetBackgroundColor(bgR, bgG, bgB, bgA) | |||
| 101 | + | if filterManager != nil { | |||
| 102 | + | factory.SetFilterApplier(filterManager) | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // --- Assemble Engine --- | |||
| 106 | + | engine := &Engine{ | |||
| 107 | + | wkCtx: wkCtx, | |||
| 108 | + | settings: settings, | |||
| 109 | + | injector: injector, | |||
| 110 | + | messageRouter: messageRouter, | |||
| 111 | + | pool: pool, | |||
| 112 | + | factory: factory, | |||
| 113 | + | filterManager: filterManager, | |||
| 114 | + | schemeHandler: schemeHandler, | |||
| 115 | + | schemePath: "dumb://", | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | return engine, nil | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // engineSurveyHardwareAndResolveProfile surveys hardware and resolves the performance profile. | |||
| 122 | + | func engineSurveyHardwareAndResolveProfile(ctx context.Context, cfg *config.Config, logger zerolog.Logger) config.ResolvedPerformanceSettings { | |||
| 123 | + | hwSurveyor := env.NewHardwareSurveyor() | |||
| 124 | + | hwInfo := hwSurveyor.Survey(ctx) | |||
| 125 | + | logging.Trace().Mark("hardware_survey") | |||
| 126 | + | logger.Info(). | |||
| 127 | + | Int("cpu_cores", hwInfo.CPUCores). | |||
| 128 | + | Int("cpu_threads", hwInfo.CPUThreads). | |||
| 129 | + | Uint64("ram_mb", hwInfo.TotalRAM/(1024*1024)). | |||
| 130 | + | Str("gpu_vendor", string(hwInfo.GPUVendor)). | |||
| 131 | + | Uint64("vram_mb", hwInfo.VRAM/(1024*1024)). | |||
| 132 | + | Msg("hardware survey completed") | |||
| 133 | + | ||||
| 134 | + | perfSettings := config.ResolvePerformanceProfile(&cfg.Performance, &hwInfo) | |||
| 135 | + | logging.Trace().Mark("performance_profile") | |||
| 136 | + | logger.Info(). | |||
| 137 | + | Str("profile", string(cfg.Performance.Profile)). | |||
| 138 | + | Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads). | |||
| 139 | + | Int("skia_gpu_threads", perfSettings.SkiaGPUPaintingThreads). | |||
| 140 | + | Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount). | |||
| 141 | + | Msg("resolved performance profile") | |||
| 142 | + | ||||
| 143 | + | return perfSettings | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | // engineConfigureRenderingEnvironment sets up the rendering environment. | |||
| 147 | + | // Environment variables must be set before GTK/WebKit/GStreamer initializes. | |||
| 148 | + | func engineConfigureRenderingEnvironment( | |||
| 149 | + | ctx context.Context, | |||
| 150 | + | cfg *config.Config, | |||
| 151 | + | wkCfg WebKitEngineConfig, | |||
| 152 | + | perfSettings *config.ResolvedPerformanceSettings, | |||
| 153 | + | logger zerolog.Logger, | |||
| 154 | + | ) { | |||
| 155 | + | // Install GLib log handler FIRST if configured (must be before any GTK/GLib calls). | |||
| 156 | + | if cfg.Logging.CaptureGTKLogs { | |||
| 157 | + | enableDebug := cfg.Logging.Level == "debug" || cfg.Logging.Level == "trace" | |||
| 158 | + | logging.InstallGLibLogHandler(ctx, logger, enableDebug) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | renderEnv := env.NewManager() | |||
| 162 | + | gpuVendor := renderEnv.DetectGPUVendor(ctx) | |||
| 163 | + | renderSettings := env.RenderingSettings{ | |||
| 164 | + | // GStreamer settings | |||
| 165 | + | ForceVSync: wkCfg.ForceVSync, | |||
| 166 | + | GLRenderingMode: wkCfg.GLRenderingMode, | |||
| 167 | + | GStreamerDebugLevel: wkCfg.GStreamerDebugLevel, | |||
| 168 | + | // WebKit compositor settings | |||
| 169 | + | DisableDMABufRenderer: wkCfg.DisableDMABufRenderer, | |||
| 170 | + | ForceCompositingMode: wkCfg.ForceCompositingMode, | |||
| 171 | + | DisableCompositingMode: wkCfg.DisableCompositingMode, | |||
| 172 | + | // GTK/GSK settings | |||
| 173 | + | GSKRenderer: wkCfg.GSKRenderer, | |||
| 174 | + | DisableMipmaps: wkCfg.DisableMipmaps, | |||
| 175 | + | PreferGL: wkCfg.PreferGL, | |||
| 176 | + | // Debug settings | |||
| 177 | + | ShowFPS: wkCfg.ShowFPS, | |||
| 178 | + | SampleMemory: wkCfg.SampleMemory, | |||
| 179 | + | DebugFrames: wkCfg.DebugFrames, | |||
| 180 | + | // Skia rendering thread settings (from resolved performance profile) | |||
| 181 | + | SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads, | |||
| 182 | + | SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads, | |||
| 183 | + | SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering, | |||
| 184 | + | } | |||
| 185 | + | if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil { | |||
| 186 | + | logger.Warn().Err(err).Msg("failed to apply rendering environment") | |||
| 187 | + | } | |||
| 188 | + | logger.Info(). | |||
| 189 | + | Str("gpu", string(gpuVendor)). | |||
| 190 | + | Interface("vars", renderEnv.GetAppliedVars()). | |||
| 191 | + | Msg("rendering environment configured") | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // engineBuildContextOptions builds port.WebKitContextOptions from EngineOptions, wkCfg and perfSettings. | |||
| 195 | + | func engineBuildContextOptions( | |||
| 196 | + | opts port.EngineOptions, | |||
| 197 | + | wkCfg WebKitEngineConfig, | |||
| 198 | + | perfSettings *config.ResolvedPerformanceSettings, | |||
| 199 | + | ) port.WebKitContextOptions { | |||
| 200 | + | cookiePolicy := port.WebKitCookiePolicyNoThirdParty | |||
| 201 | + | if opts.CookiePolicy != "" { | |||
| 202 | + | cookiePolicy = port.WebKitCookiePolicy(opts.CookiePolicy) | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | wkOpts := port.WebKitContextOptions{ | |||
| 206 | + | DataDir: opts.DataDir, | |||
| 207 | + | CacheDir: opts.CacheDir, | |||
| 208 | + | CookiePolicy: cookiePolicy, | |||
| 209 | + | ITPEnabled: wkCfg.ITPEnabled, | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | if opts.WebProcessMemory != nil { | |||
| 213 | + | wkOpts.WebProcessMemory = opts.WebProcessMemory | |||
| 214 | + | } else if engineHasWebProcessMemoryConfig(perfSettings) { | |||
| 215 | + | wkOpts.WebProcessMemory = &port.MemoryPressureConfig{ | |||
| 216 | + | MemoryLimitMB: perfSettings.WebProcessMemoryLimitMB, | |||
| 217 | + | PollIntervalSec: perfSettings.WebProcessMemoryPollIntervalSec, | |||
| 218 | + | ConservativeThreshold: perfSettings.WebProcessMemoryConservativeThreshold, | |||
| 219 | + | StrictThreshold: perfSettings.WebProcessMemoryStrictThreshold, | |||
| 220 | + | } | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | if opts.NetworkProcessMemory != nil { | |||
| 224 | + | wkOpts.NetworkProcessMemory = opts.NetworkProcessMemory | |||
| 225 | + | } else if engineHasNetworkProcessMemoryConfig(perfSettings) { | |||
| 226 | + | wkOpts.NetworkProcessMemory = &port.MemoryPressureConfig{ | |||
| 227 | + | MemoryLimitMB: perfSettings.NetworkProcessMemoryLimitMB, | |||
| 228 | + | PollIntervalSec: perfSettings.NetworkProcessMemoryPollIntervalSec, | |||
| 229 | + | ConservativeThreshold: perfSettings.NetworkProcessMemoryConservativeThreshold, | |||
| 230 | + | StrictThreshold: perfSettings.NetworkProcessMemoryStrictThreshold, | |||
| 231 | + | } | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | return wkOpts | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // engineInitFilterManager creates and initializes the content filter manager. | |||
| 238 | + | func engineInitFilterManager(ctx context.Context, cfg *config.Config, dataDir string, logger zerolog.Logger) *filtering.Manager { | |||
| 239 | + | filterStoreDir := filepath.Join(dataDir, "filters", "store") | |||
| 240 | + | filterJSONDir := filepath.Join(dataDir, "filters", "json") | |||
| 241 | + | filterManager, err := filtering.NewManager(filtering.ManagerConfig{ | |||
| 242 | + | StoreDir: filterStoreDir, | |||
| 243 | + | JSONDir: filterJSONDir, | |||
| 244 | + | Enabled: cfg.ContentFiltering.Enabled, | |||
| 245 | + | AutoUpdate: cfg.ContentFiltering.AutoUpdate, | |||
| 246 | + | }) | |||
| 247 | + | if err != nil { | |||
| 248 | + | logger.Warn().Err(err).Msg("failed to create filter manager, continuing without content filtering") | |||
| 249 | + | return nil | |||
| 250 | + | } | |||
| 251 | + | if err := filterManager.Initialize(ctx); err != nil { | |||
| 252 | + | logger.Warn().Err(err).Msg("failed to initialize filters, will load async") | |||
| 253 | + | } | |||
| 254 | + | logging.Trace().Mark("filter_manager") | |||
| 255 | + | return filterManager | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | // engineHasWebProcessMemoryConfig returns true if any web process memory setting is configured. | |||
| 259 | + | func engineHasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool { | |||
| 260 | + | return p.WebProcessMemoryLimitMB > 0 || | |||
| 261 | + | p.WebProcessMemoryPollIntervalSec > 0 || | |||
| 262 | + | p.WebProcessMemoryConservativeThreshold > 0 || | |||
| 263 | + | p.WebProcessMemoryStrictThreshold > 0 | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | // engineHasNetworkProcessMemoryConfig returns true if any network process memory setting is configured. | |||
| 267 | + | func engineHasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool { | |||
| 268 | + | return p.NetworkProcessMemoryLimitMB > 0 || | |||
| 269 | + | p.NetworkProcessMemoryPollIntervalSec > 0 || | |||
| 270 | + | p.NetworkProcessMemoryConservativeThreshold > 0 || | |||
| 271 | + | p.NetworkProcessMemoryStrictThreshold > 0 | |||
| 272 | + | } | |||