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 := min(max(runtime.NumCPU()/2, 4), 8)
112 if result.SkiaCPUPaintingThreads != expectedCPUThreads {
113 t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)
114 }
115 // GPU threads: conservative default when no VRAM info
116 if result.SkiaGPUPaintingThreads != 2 {
117 t.Errorf("expected SkiaGPUPaintingThreads=2 (fallback), got %d", result.SkiaGPUPaintingThreads)
118 }
119 // Max profile: no memory limits (unset)
120 if result.WebProcessMemoryLimitMB != 0 {
121 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset), got %d", result.WebProcessMemoryLimitMB)
122 }
123 if result.WebViewPoolPrewarmCount != 8 {
124 t.Errorf("expected WebViewPoolPrewarmCount=8, got %d", result.WebViewPoolPrewarmCount)
125 }
126}
127
128func TestResolvePerformanceProfile_Max_HighEndSystem(t *testing.T) {
129 cfg := &PerformanceConfig{
130 Profile: ProfileMax,
131 }
132 hw := &port.HardwareInfo{
133 CPUCores: 16,
134 CPUThreads: 32,
135 TotalRAM: 64 * 1024 * 1024 * 1024, // 64 GB
136 VRAM: 16 * 1024 * 1024 * 1024, // 16 GB (high-end GPU)
137 GPUVendor: port.GPUVendorAMD,
138 }
139
140 result := ResolvePerformanceProfile(cfg, hw)
141
142 // CPU threads: use physical cores capped at 8 (WebKitGTK limit)
143 if result.SkiaCPUPaintingThreads != 8 {
144 t.Errorf("expected SkiaCPUPaintingThreads=8 (capped), got %d", result.SkiaCPUPaintingThreads)
145 }
146 // GPU threads: high-end tier (16GB+ VRAM)
147 if result.SkiaGPUPaintingThreads != 8 {
148 t.Errorf("expected SkiaGPUPaintingThreads=8 for 16GB VRAM, got %d", result.SkiaGPUPaintingThreads)
149 }
150 // Max profile: no memory limits (unset)
151 if result.WebProcessMemoryLimitMB != 0 {
152 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.WebProcessMemoryLimitMB)
153 }
154 if result.NetworkProcessMemoryLimitMB != 0 {
155 t.Errorf("expected NetworkProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.NetworkProcessMemoryLimitMB)
156 }
157 // Pool prewarm: high-end tier
158 if result.WebViewPoolPrewarmCount != 12 {
159 t.Errorf("expected WebViewPoolPrewarmCount=12 for 64GB RAM, got %d", result.WebViewPoolPrewarmCount)
160 }
161}
162
163func TestResolvePerformanceProfile_Max_MidRangeSystem(t *testing.T) {
164 cfg := &PerformanceConfig{
165 Profile: ProfileMax,
166 }
167 hw := &port.HardwareInfo{
168 CPUCores: 8,
169 CPUThreads: 16,
170 TotalRAM: 16 * 1024 * 1024 * 1024, // 16 GB
171 VRAM: 8 * 1024 * 1024 * 1024, // 8 GB (mid-range GPU)
172 GPUVendor: port.GPUVendorNVIDIA,
173 }
174
175 result := ResolvePerformanceProfile(cfg, hw)
176
177 // CPU threads: use physical cores (8)
178 if result.SkiaCPUPaintingThreads != 8 {
179 t.Errorf("expected SkiaCPUPaintingThreads=8, got %d", result.SkiaCPUPaintingThreads)
180 }
181 // GPU threads: mid-range tier (8-16GB VRAM)
182 if result.SkiaGPUPaintingThreads != 6 {
183 t.Errorf("expected SkiaGPUPaintingThreads=6 for 8GB VRAM, got %d", result.SkiaGPUPaintingThreads)
184 }
185 // Max profile: no memory limits (unset)
186 if result.WebProcessMemoryLimitMB != 0 {
187 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.WebProcessMemoryLimitMB)
188 }
189 // Pool prewarm: mid-range tier
190 if result.WebViewPoolPrewarmCount != 8 {
191 t.Errorf("expected WebViewPoolPrewarmCount=8 for 16GB RAM, got %d", result.WebViewPoolPrewarmCount)
192 }
193}
194
195func TestResolvePerformanceProfile_Max_LowEndSystem(t *testing.T) {
196 cfg := &PerformanceConfig{
197 Profile: ProfileMax,
198 }
199 hw := &port.HardwareInfo{
200 CPUCores: 4,
201 CPUThreads: 4,
202 TotalRAM: 4 * 1024 * 1024 * 1024, // 4 GB
203 VRAM: 2 * 1024 * 1024 * 1024, // 2 GB (integrated/old discrete)
204 GPUVendor: port.GPUVendorIntel,
205 }
206
207 result := ResolvePerformanceProfile(cfg, hw)
208
209 // CPU threads: use physical cores (4)
210 if result.SkiaCPUPaintingThreads != 4 {
211 t.Errorf("expected SkiaCPUPaintingThreads=4, got %d", result.SkiaCPUPaintingThreads)
212 }
213 // GPU threads: low tier (<4GB VRAM)
214 if result.SkiaGPUPaintingThreads != 2 {
215 t.Errorf("expected SkiaGPUPaintingThreads=2 for 2GB VRAM, got %d", result.SkiaGPUPaintingThreads)
216 }
217 // Max profile: no memory limits (unset)
218 if result.WebProcessMemoryLimitMB != 0 {
219 t.Errorf("expected WebProcessMemoryLimitMB=0 (unset) for max profile, got %d", result.WebProcessMemoryLimitMB)
220 }
221 // Pool prewarm: low tier
222 if result.WebViewPoolPrewarmCount != 4 {
223 t.Errorf("expected WebViewPoolPrewarmCount=4 for 4GB RAM, got %d", result.WebViewPoolPrewarmCount)
224 }
225}
226
227func TestResolvePerformanceProfile_Custom(t *testing.T) {
228 cfg := &PerformanceConfig{
229 Profile: ProfileCustom,
230 SkiaCPUPaintingThreads: 6,
231 SkiaGPUPaintingThreads: 3,
232 SkiaEnableCPURendering: true,
233 WebProcessMemoryLimitMB: 1024,
234 WebProcessMemoryConservativeThreshold: 0.4,
235 WebViewPoolPrewarmCount: 5,
236 }
237 // Hardware info should be ignored for custom profile
238 hw := &port.HardwareInfo{
239 CPUCores: 16,
240 CPUThreads: 32,
241 TotalRAM: 64 * 1024 * 1024 * 1024,
242 VRAM: 16 * 1024 * 1024 * 1024,
243 }
244
245 result := ResolvePerformanceProfile(cfg, hw)
246
247 // Custom should use user-specified values, ignoring hardware
248 if result.SkiaCPUPaintingThreads != 6 {
249 t.Errorf("expected SkiaCPUPaintingThreads=6, got %d", result.SkiaCPUPaintingThreads)
250 }
251 if result.SkiaGPUPaintingThreads != 3 {
252 t.Errorf("expected SkiaGPUPaintingThreads=3, got %d", result.SkiaGPUPaintingThreads)
253 }
254 if !result.SkiaEnableCPURendering {
255 t.Error("expected SkiaEnableCPURendering=true")
256 }
257 if result.WebProcessMemoryLimitMB != 1024 {
258 t.Errorf("expected WebProcessMemoryLimitMB=1024, got %d", result.WebProcessMemoryLimitMB)
259 }
260 if result.WebViewPoolPrewarmCount != 5 {
261 t.Errorf("expected WebViewPoolPrewarmCount=5, got %d", result.WebViewPoolPrewarmCount)
262 }
263}
264
265func TestHasCustomPerformanceFields(t *testing.T) {
266 tests := []struct {
267 name string
268 cfg PerformanceConfig
269 expected bool
270 }{
271 {
272 name: "all defaults - no custom fields",
273 cfg: PerformanceConfig{
274 SkiaCPUPaintingThreads: 0,
275 SkiaGPUPaintingThreads: -1,
276 SkiaEnableCPURendering: false,
277 },
278 expected: false,
279 },
280 {
281 name: "gpu threads zero treated as default",
282 cfg: PerformanceConfig{
283 SkiaGPUPaintingThreads: 0,
284 },
285 expected: false,
286 },
287 {
288 name: "skia cpu threads set",
289 cfg: PerformanceConfig{
290 SkiaCPUPaintingThreads: 4,
291 SkiaGPUPaintingThreads: -1,
292 },
293 expected: true,
294 },
295 {
296 name: "memory limit set",
297 cfg: PerformanceConfig{
298 SkiaGPUPaintingThreads: -1,
299 WebProcessMemoryLimitMB: 1024,
300 },
301 expected: true,
302 },
303 {
304 name: "conservative threshold set",
305 cfg: PerformanceConfig{
306 SkiaGPUPaintingThreads: -1,
307 WebProcessMemoryConservativeThreshold: 0.4,
308 },
309 expected: true,
310 },
311 }
312
313 for _, tt := range tests {
314 t.Run(tt.name, func(t *testing.T) {
315 result := HasCustomPerformanceFields(&tt.cfg)
316 if result != tt.expected {
317 t.Errorf("expected %v, got %v", tt.expected, result)
318 }
319 })
320 }
321}
322
323func TestIsValidPerformanceProfile(t *testing.T) {
324 tests := []struct {
325 profile PerformanceProfile
326 expected bool
327 }{
328 {ProfileDefault, true},
329 {ProfileLite, true},
330 {ProfileBalanced, true},
331 {ProfileMax, true},
332 {ProfileCustom, true},
333 {"", true}, // Empty is valid (treated as default)
334 {"invalid", false},
335 {"LITE", false}, // Case sensitive
336 {"Maximum", false},
337 }
338
339 for _, tt := range tests {
340 t.Run(string(tt.profile), func(t *testing.T) {
341 result := IsValidPerformanceProfile(tt.profile)
342 if result != tt.expected {
343 t.Errorf("IsValidPerformanceProfile(%q): expected %v, got %v", tt.profile, tt.expected, result)
344 }
345 })
346 }
347}
348
349func TestComputeMaxGPUThreads_VRAMTiers(t *testing.T) {
350 tests := []struct {
351 name string
352 vramGB int
353 expected int
354 }{
355 {"no VRAM info", 0, 2},
356 {"2GB VRAM (integrated)", 2, 2},
357 {"4GB VRAM (entry discrete)", 4, 4},
358 {"6GB VRAM (low-mid discrete)", 6, 4},
359 {"8GB VRAM (mid discrete)", 8, 6},
360 {"12GB VRAM (mid-high discrete)", 12, 6},
361 {"16GB VRAM (high-end)", 16, 8},
362 {"24GB VRAM (workstation)", 24, 8},
363 }
364
365 for _, tt := range tests {
366 t.Run(tt.name, func(t *testing.T) {
367 hw := &port.HardwareInfo{
368 VRAM: uint64(tt.vramGB) * 1024 * 1024 * 1024,
369 }
370 if tt.vramGB == 0 {
371 hw = nil
372 }
373 result := computeMaxGPUThreads(hw)
374 if result != tt.expected {
375 t.Errorf("computeMaxGPUThreads(%dGB VRAM): expected %d, got %d", tt.vramGB, tt.expected, result)
376 }
377 })
378 }
379}
380
381func TestComputeMaxMemoryLimits_RAMTiers(t *testing.T) {
382 tests := []struct {
383 name string
384 ramGB int
385 expectedWeb int
386 expectedNet int
387 expectedPool int
388 }{
389 {"no RAM info", 0, 2048, 512, 8},
390 {"4GB RAM", 4, 1024, 256, 4},
391 {"8GB RAM", 8, 2048, 512, 6},
392 {"16GB RAM", 16, 3072, 768, 8},
393 {"32GB RAM", 32, 4096, 1024, 12},
394 {"64GB RAM", 64, 4096, 1024, 12},
395 }
396
397 for _, tt := range tests {
398 t.Run(tt.name, func(t *testing.T) {
399 hw := &port.HardwareInfo{
400 TotalRAM: uint64(tt.ramGB) * 1024 * 1024 * 1024,
401 }
402 if tt.ramGB == 0 {
403 hw = nil
404 }
405 webMB, netMB := computeMaxMemoryLimits(hw)
406 poolPrewarm := computeMaxPoolPrewarm(hw)
407
408 if webMB != tt.expectedWeb {
409 t.Errorf("computeMaxMemoryLimits(%dGB RAM) web: expected %d, got %d", tt.ramGB, tt.expectedWeb, webMB)
410 }
411 if netMB != tt.expectedNet {
412 t.Errorf("computeMaxMemoryLimits(%dGB RAM) net: expected %d, got %d", tt.ramGB, tt.expectedNet, netMB)
413 }
414 if poolPrewarm != tt.expectedPool {
415 t.Errorf("computeMaxPoolPrewarm(%dGB RAM): expected %d, got %d", tt.ramGB, tt.expectedPool, poolPrewarm)
416 }
417 })
418 }
419}