profile_test.go RawHistoryFolder

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}

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