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