v0.29.0 21b3150

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 := 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}

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