1package config
2
3import (
4 "runtime"
5 "testing"
6
7 "github.com/bnema/dumber/internal/application/port"
8)
9
10func TestResolvePerformanceProfile_Default(t *testing.T) {
11 cfg := &PerformanceConfig{
12 Profile: ProfileDefault,
13 }
14
15 result := ResolvePerformanceProfile(cfg, nil)
16
17 // Default should have no tuning (unset values)
18 if result.SkiaCPUPaintingThreads != 0 {
19 t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
20 }
21 if result.SkiaGPUPaintingThreads != -1 {
22 t.Errorf("expected SkiaGPUPaintingThreads=-1, got %d", result.SkiaGPUPaintingThreads)
23 }
24 if result.WebProcessMemoryLimitMB != 0 {
25 t.Errorf("expected WebProcessMemoryLimitMB=0, got %d", result.WebProcessMemoryLimitMB)
26 }
27 if result.WebViewPoolPrewarmCount != 4 {
28 t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
29 }
30}
31
32func TestResolvePerformanceProfile_Empty(t *testing.T) {
33 cfg := &PerformanceConfig{
34 Profile: "", // Empty should behave like default
35 }
36
37 result := ResolvePerformanceProfile(cfg, nil)
38
39 if result.SkiaCPUPaintingThreads != 0 {
40 t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
41 }
42 if result.WebViewPoolPrewarmCount != 4 {
43 t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
44 }
45}
46
47func TestResolvePerformanceProfile_Balanced(t *testing.T) {
48 cfg := &PerformanceConfig{Profile: ProfileBalanced}
49
50 result := ResolvePerformanceProfile(cfg, nil)
51
52 if result.SkiaCPUPaintingThreads != 0 {
53 t.Errorf("expected SkiaCPUPaintingThreads=0, got %d", result.SkiaCPUPaintingThreads)
54 }
55 if result.SkiaGPUPaintingThreads != -1 {
56 t.Errorf("expected SkiaGPUPaintingThreads=-1, got %d", result.SkiaGPUPaintingThreads)
57 }
58 if result.WebViewPoolPrewarmCount != 4 {
59 t.Errorf("expected WebViewPoolPrewarmCount=4, got %d", result.WebViewPoolPrewarmCount)
60 }
61}
62
63func TestResolvePerformanceProfile_Lite_NoHardware(t *testing.T) {
64 cfg := &PerformanceConfig{
65 Profile: ProfileLite,
66 }
67
68 result := ResolvePerformanceProfile(cfg, nil)
69
70 // Lite without hardware info should use default lite values
71 if result.SkiaCPUPaintingThreads != 2 {
72 t.Errorf("expected SkiaCPUPaintingThreads=2, got %d", result.SkiaCPUPaintingThreads)
73 }
74 if result.WebProcessMemoryLimitMB != 768 {
75 t.Errorf("expected WebProcessMemoryLimitMB=768, got %d", result.WebProcessMemoryLimitMB)
76 }
77 if result.NetworkProcessMemoryLimitMB != 384 {
78 t.Errorf("expected NetworkProcessMemoryLimitMB=384, got %d", result.NetworkProcessMemoryLimitMB)
79 }
80 if result.WebViewPoolPrewarmCount != 2 {
81 t.Errorf("expected WebViewPoolPrewarmCount=2, got %d", result.WebViewPoolPrewarmCount)
82 }
83}
84
85func TestResolvePerformanceProfile_Lite_LowCoreSystem(t *testing.T) {
86 cfg := &PerformanceConfig{
87 Profile: ProfileLite,
88 }
89 hw := &port.HardwareInfo{
90 CPUCores: 2, // Very low core count
91 CPUThreads: 4,
92 }
93
94 result := ResolvePerformanceProfile(cfg, hw)
95
96 // Lite on low-core system should scale down
97 if result.SkiaCPUPaintingThreads != 1 {
98 t.Errorf("expected SkiaCPUPaintingThreads=1 for 2-core system, got %d", result.SkiaCPUPaintingThreads)
99 }
100}
101
102func TestResolvePerformanceProfile_Max_NoHardware(t *testing.T) {
103 cfg := &PerformanceConfig{
104 Profile: ProfileMax,
105 }
106
107 result := ResolvePerformanceProfile(cfg, nil)
108
109 // Max without hardware info should use fallback values
110 // CPU threads: NumCPU/2 (assuming hyperthreading), clamped to [4, 8]
111 expectedCPUThreads := runtime.NumCPU() / 2
112 if expectedCPUThreads < 4 {
113 expectedCPUThreads = 4
114 }
115 if expectedCPUThreads > 8 {
116 expectedCPUThreads = 8
117 }
118 if result.SkiaCPUPaintingThreads != expectedCPUThreads {
119 t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)
120 }
121 // GPU threads: conservative default when no VRAM info
122 if result.SkiaGPUPaintingThreads != 2 {
123 t.Errorf("expected SkiaGPUPaintingThreads=2 (fallback), got %d", result.SkiaGPUPaintingThreads)
124 }
125 // Max profile: no memory limits (unset)
126 if result.WebProcessMemoryLimitMB != 0 {
127 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset), got %d", result.WebProcessMemoryLimitMB)
128 }
129 if result.WebViewPoolPrewarmCount != 8 {
130 t.Errorf("expected WebViewPoolPrewarmCount=8, got %d", result.WebViewPoolPrewarmCount)
131 }
132}
133
134func TestResolvePerformanceProfile_Max_HighEndSystem(t *testing.T) {
135 cfg := &PerformanceConfig{
136 Profile: ProfileMax,
137 }
138 hw := &port.HardwareInfo{
139 CPUCores: 16,
140 CPUThreads: 32,
141 TotalRAM: 64 * 1024 * 1024 * 1024, // 64 GB
142 VRAM: 16 * 1024 * 1024 * 1024, // 16 GB (high-end GPU)
143 GPUVendor: port.GPUVendorAMD,
144 }
145
146 result := ResolvePerformanceProfile(cfg, hw)
147
148 // CPU threads: use physical cores capped at 8 (WebKitGTK limit)
149 if result.SkiaCPUPaintingThreads != 8 {
150 t.Errorf("expected SkiaCPUPaintingThreads=8 (capped), got %d", result.SkiaCPUPaintingThreads)
151 }
152 // GPU threads: high-end tier (16GB+ VRAM)
153 if result.SkiaGPUPaintingThreads != 8 {
154 t.Errorf("expected SkiaGPUPaintingThreads=8 for 16GB VRAM, got %d", result.SkiaGPUPaintingThreads)
155 }
156 // Max profile: no memory limits (unset)
157 if result.WebProcessMemoryLimitMB != 0 {
158 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.WebProcessMemoryLimitMB)
159 }
160 if result.NetworkProcessMemoryLimitMB != 0 {
161 t.Errorf("expected NetworkProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.NetworkProcessMemoryLimitMB)
162 }
163 // Pool prewarm: high-end tier
164 if result.WebViewPoolPrewarmCount != 12 {
165 t.Errorf("expected WebViewPoolPrewarmCount=12 for 64GB RAM, got %d", result.WebViewPoolPrewarmCount)
166 }
167}
168
169func TestResolvePerformanceProfile_Max_MidRangeSystem(t *testing.T) {
170 cfg := &PerformanceConfig{
171 Profile: ProfileMax,
172 }
173 hw := &port.HardwareInfo{
174 CPUCores: 8,
175 CPUThreads: 16,
176 TotalRAM: 16 * 1024 * 1024 * 1024, // 16 GB
177 VRAM: 8 * 1024 * 1024 * 1024, // 8 GB (mid-range GPU)
178 GPUVendor: port.GPUVendorNVIDIA,
179 }
180
181 result := ResolvePerformanceProfile(cfg, hw)
182
183 // CPU threads: use physical cores (8)
184 if result.SkiaCPUPaintingThreads != 8 {
185 t.Errorf("expected SkiaCPUPaintingThreads=8, got %d", result.SkiaCPUPaintingThreads)
186 }
187 // GPU threads: mid-range tier (8-16GB VRAM)
188 if result.SkiaGPUPaintingThreads != 6 {
189 t.Errorf("expected SkiaGPUPaintingThreads=6 for 8GB VRAM, got %d", result.SkiaGPUPaintingThreads)
190 }
191 // Max profile: no memory limits (unset)
192 if result.WebProcessMemoryLimitMB != 0 {
193 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.WebProcessMemoryLimitMB)
194 }
195 // Pool prewarm: mid-range tier
196 if result.WebViewPoolPrewarmCount != 8 {
197 t.Errorf("expected WebViewPoolPrewarmCount=8 for 16GB RAM, got %d", result.WebViewPoolPrewarmCount)
198 }
199}
200
201func TestResolvePerformanceProfile_Max_LowEndSystem(t *testing.T) {
202 cfg := &PerformanceConfig{
203 Profile: ProfileMax,
204 }
205 hw := &port.HardwareInfo{
206 CPUCores: 4,
207 CPUThreads: 4,
208 TotalRAM: 4 * 1024 * 1024 * 1024, // 4 GB
209 VRAM: 2 * 1024 * 1024 * 1024, // 2 GB (integrated/old discrete)
210 GPUVendor: port.GPUVendorIntel,
211 }
212
213 result := ResolvePerformanceProfile(cfg, hw)
214
215 // CPU threads: use physical cores (4)
216 if result.SkiaCPUPaintingThreads != 4 {
217 t.Errorf("expected SkiaCPUPaintingThreads=4, got %d", result.SkiaCPUPaintingThreads)
218 }
219 // GPU threads: low tier (<4GB VRAM)
220 if result.SkiaGPUPaintingThreads != 2 {
221 t.Errorf("expected SkiaGPUPaintingThreads=2 for 2GB VRAM, got %d", result.SkiaGPUPaintingThreads)
222 }
223 // Max profile: no memory limits (unset)
224 if result.WebProcessMemoryLimitMB != 0 {
225 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.WebProcessMemoryLimitMB)
226 }
227 // Pool prewarm: low tier
228 if result.WebViewPoolPrewarmCount != 4 {
229 t.Errorf("expected WebViewPoolPrewarmCount=4 for 4GB RAM, got %d", result.WebViewPoolPrewarmCount)
230 }
231}
232
233func TestResolvePerformanceProfile_Custom(t *testing.T) {
234 cfg := &PerformanceConfig{
235 Profile: ProfileCustom,
236 SkiaCPUPaintingThreads: 6,
237 SkiaGPUPaintingThreads: 3,
238 SkiaEnableCPURendering: true,
239 WebProcessMemoryLimitMB: 1024,
240 WebProcessMemoryConservativeThreshold: 0.4,
241 WebViewPoolPrewarmCount: 5,
242 }
243 // Hardware info should be ignored for custom profile
244 hw := &port.HardwareInfo{
245 CPUCores: 16,
246 CPUThreads: 32,
247 TotalRAM: 64 * 1024 * 1024 * 1024,
248 VRAM: 16 * 1024 * 1024 * 1024,
249 }
250
251 result := ResolvePerformanceProfile(cfg, hw)
252
253 // Custom should use user-specified values, ignoring hardware
254 if result.SkiaCPUPaintingThreads != 6 {
255 t.Errorf("expected SkiaCPUPaintingThreads=6, got %d", result.SkiaCPUPaintingThreads)
256 }
257 if result.SkiaGPUPaintingThreads != 3 {
258 t.Errorf("expected SkiaGPUPaintingThreads=3, got %d", result.SkiaGPUPaintingThreads)
259 }
260 if !result.SkiaEnableCPURendering {
261 t.Error("expected SkiaEnableCPURendering=true")
262 }
263 if result.WebProcessMemoryLimitMB != 1024 {
264 t.Errorf("expected WebProcessMemoryLimitMB=1024, got %d", result.WebProcessMemoryLimitMB)
265 }
266 if result.WebViewPoolPrewarmCount != 5 {
267 t.Errorf("expected WebViewPoolPrewarmCount=5, got %d", result.WebViewPoolPrewarmCount)
268 }
269}
270
271func TestHasCustomPerformanceFields(t *testing.T) {
272 tests := []struct {
273 name string
274 cfg PerformanceConfig
275 expected bool
276 }{
277 {
278 name: "all defaults - no custom fields",
279 cfg: PerformanceConfig{
280 SkiaCPUPaintingThreads: 0,
281 SkiaGPUPaintingThreads: -1,
282 SkiaEnableCPURendering: false,
283 },
284 expected: false,
285 },
286 {
287 name: "gpu threads zero treated as default",
288 cfg: PerformanceConfig{
289 SkiaGPUPaintingThreads: 0,
290 },
291 expected: false,
292 },
293 {
294 name: "skia cpu threads set",
295 cfg: PerformanceConfig{
296 SkiaCPUPaintingThreads: 4,
297 SkiaGPUPaintingThreads: -1,
298 },
299 expected: true,
300 },
301 {
302 name: "memory limit set",
303 cfg: PerformanceConfig{
304 SkiaGPUPaintingThreads: -1,
305 WebProcessMemoryLimitMB: 1024,
306 },
307 expected: true,
308 },
309 {
310 name: "conservative threshold set",
311 cfg: PerformanceConfig{
312 SkiaGPUPaintingThreads: -1,
313 WebProcessMemoryConservativeThreshold: 0.4,
314 },
315 expected: true,
316 },
317 }
318
319 for _, tt := range tests {
320 t.Run(tt.name, func(t *testing.T) {
321 result := HasCustomPerformanceFields(&tt.cfg)
322 if result != tt.expected {
323 t.Errorf("expected %v, got %v", tt.expected, result)
324 }
325 })
326 }
327}
328
329func TestIsValidPerformanceProfile(t *testing.T) {
330 tests := []struct {
331 profile PerformanceProfile
332 expected bool
333 }{
334 {ProfileDefault, true},
335 {ProfileLite, true},
336 {ProfileBalanced, true},
337 {ProfileMax, true},
338 {ProfileCustom, true},
339 {"", true}, // Empty is valid (treated as default)
340 {"invalid", false},
341 {"LITE", false}, // Case sensitive
342 {"Maximum", false},
343 }
344
345 for _, tt := range tests {
346 t.Run(string(tt.profile), func(t *testing.T) {
347 result := IsValidPerformanceProfile(tt.profile)
348 if result != tt.expected {
349 t.Errorf("IsValidPerformanceProfile(%q): expected %v, got %v", tt.profile, tt.expected, result)
350 }
351 })
352 }
353}
354
355func TestComputeMaxGPUThreads_VRAMTiers(t *testing.T) {
356 tests := []struct {
357 name string
358 vramGB int
359 expected int
360 }{
361 {"no VRAM info", 0, 2},
362 {"2GB VRAM (integrated)", 2, 2},
363 {"4GB VRAM (entry discrete)", 4, 4},
364 {"6GB VRAM (low-mid discrete)", 6, 4},
365 {"8GB VRAM (mid discrete)", 8, 6},
366 {"12GB VRAM (mid-high discrete)", 12, 6},
367 {"16GB VRAM (high-end)", 16, 8},
368 {"24GB VRAM (workstation)", 24, 8},
369 }
370
371 for _, tt := range tests {
372 t.Run(tt.name, func(t *testing.T) {
373 hw := &port.HardwareInfo{
374 VRAM: uint64(tt.vramGB) * 1024 * 1024 * 1024,
375 }
376 if tt.vramGB == 0 {
377 hw = nil
378 }
379 result := computeMaxGPUThreads(hw)
380 if result != tt.expected {
381 t.Errorf("computeMaxGPUThreads(%dGB VRAM): expected %d, got %d", tt.vramGB, tt.expected, result)
382 }
383 })
384 }
385}
386
387func TestComputeMaxMemoryLimits_RAMTiers(t *testing.T) {
388 tests := []struct {
389 name string
390 ramGB int
391 expectedWeb int
392 expectedNet int
393 expectedPool int
394 }{
395 {"no RAM info", 0, 2048, 512, 8},
396 {"4GB RAM", 4, 1024, 256, 4},
397 {"8GB RAM", 8, 2048, 512, 6},
398 {"16GB RAM", 16, 3072, 768, 8},
399 {"32GB RAM", 32, 4096, 1024, 12},
400 {"64GB RAM", 64, 4096, 1024, 12},
401 }
402
403 for _, tt := range tests {
404 t.Run(tt.name, func(t *testing.T) {
405 hw := &port.HardwareInfo{
406 TotalRAM: uint64(tt.ramGB) * 1024 * 1024 * 1024,
407 }
408 if tt.ramGB == 0 {
409 hw = nil
410 }
411 webMB, netMB := computeMaxMemoryLimits(hw)
412 poolPrewarm := computeMaxPoolPrewarm(hw)
413
414 if webMB != tt.expectedWeb {
415 t.Errorf("computeMaxMemoryLimits(%dGB RAM) web: expected %d, got %d", tt.ramGB, tt.expectedWeb, webMB)
416 }
417 if netMB != tt.expectedNet {
418 t.Errorf("computeMaxMemoryLimits(%dGB RAM) net: expected %d, got %d", tt.ramGB, tt.expectedNet, netMB)
419 }
420 if poolPrewarm != tt.expectedPool {
421 t.Errorf("computeMaxPoolPrewarm(%dGB RAM): expected %d, got %d", tt.ramGB, tt.expectedPool, poolPrewarm)
422 }
423 })
424 }
425}