v0.9.0 636c44c

workspace_manager_bench_test.go RawHistoryFolder

1package browser
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/dumber/pkg/webkit"
8)
9
10func BenchmarkSplitNode(b *testing.B) {
11	wm := newTestWorkspaceManagerWithMocks(b)
12
13	b.ResetTimer()
14	for i := 0; i < b.N; i++ {
15		b.StopTimer()
16		// Reset to single node for each iteration
17		wm = newTestWorkspaceManagerWithMocks(b)
18		b.StartTimer()
19
20		_, err := wm.splitNode(wm.root, "right")
21		if err != nil {
22			b.Fatalf("Split failed: %v", err)
23		}
24	}
25}
26
27func BenchmarkClosePane(b *testing.B) {
28	for i := 0; i < b.N; i++ {
29		b.StopTimer()
30		wm := newTestWorkspaceManagerWithMocks(b)
31		node, err := wm.splitNode(wm.root, "right")
32		if err != nil {
33			b.Fatalf("Setup split failed: %v", err)
34		}
35		b.StartTimer()
36
37		err = wm.closePane(node)
38		if err != nil {
39			b.Fatalf("Close failed: %v", err)
40		}
41	}
42}
43
44func BenchmarkTreeTraversal(b *testing.B) {
45	wm := newTestWorkspaceManagerWithMocks(b)
46
47	// Build a complex tree with 15 leaves
48	leaves := []*paneNode{wm.root}
49	for i := 0; i < 14; i++ {
50		if len(leaves) > 0 {
51			// Split the first leaf
52			target := leaves[0]
53			leaves = leaves[1:] // Remove the target from leaves
54
55			newNode, err := wm.splitNode(target, "right")
56			if err != nil {
57				b.Fatalf("Setup split %d failed: %v", i, err)
58			}
59
60			// Add both resulting leaves back
61			leaves = append(leaves, target, newNode)
62		}
63	}
64
65	b.ResetTimer()
66	for i := 0; i < b.N; i++ {
67		wm.collectLeaves()
68	}
69}
70
71func BenchmarkCollectLeavesFrom(b *testing.B) {
72	wm := newTestWorkspaceManagerWithMocks(b)
73
74	// Build a balanced binary tree
75	for i := 0; i < 10; i++ {
76		leaves := wm.collectLeaves()
77		if len(leaves) > 0 {
78			_, err := wm.splitNode(leaves[0], "right")
79			if err != nil {
80				b.Fatalf("Setup split %d failed: %v", i, err)
81			}
82		}
83	}
84
85	b.ResetTimer()
86	for i := 0; i < b.N; i++ {
87		wm.collectLeavesFrom(wm.root)
88	}
89}
90
91func BenchmarkLeftmostLeaf(b *testing.B) {
92	wm := newTestWorkspaceManagerWithMocks(b)
93
94	// Create a deep left-leaning tree
95	current := wm.root
96	for i := 0; i < 20; i++ {
97		newNode, err := wm.splitNode(current, "left")
98		if err != nil {
99			b.Fatalf("Setup split %d failed: %v", i, err)
100		}
101		current = newNode
102	}
103
104	b.ResetTimer()
105	for i := 0; i < b.N; i++ {
106		wm.leftmostLeaf(wm.root)
107	}
108}
109
110func BenchmarkFindReplacementRoot(b *testing.B) {
111	wm := newTestWorkspaceManagerWithMocks(b)
112
113	// Build a complex tree
114	leaves := wm.collectLeaves()
115	for i := 0; i < 15; i++ {
116		if len(leaves) > 0 {
117			target := leaves[i%len(leaves)]
118			newNode, err := wm.splitNode(target, "right")
119			if err != nil {
120				continue
121			}
122			leaves = append(leaves, newNode)
123		}
124	}
125
126	excludeNode := wm.root
127
128	b.ResetTimer()
129	for i := 0; i < b.N; i++ {
130		wm.findReplacementRoot(excludeNode)
131	}
132}
133
134func BenchmarkFocusAdjacent(b *testing.B) {
135	wm := newTestWorkspaceManagerWithMocks(b)
136
137	// Create a 2x2 grid of panes with known geometry
138	right, _ := wm.splitNode(wm.root, "right")
139	bottom, _ := wm.splitNode(wm.root, "down")
140	_, _ = wm.splitNode(right, "down")
141
142	// Set up widget bounds for focus calculations
143	webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})
144	webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100})
145	webkit.SetWidgetBoundsForTesting(bottom.container, webkit.WidgetBounds{X: 0, Y: 120, Width: 100, Height: 100})
146
147	wm.active = wm.root
148
149	b.ResetTimer()
150	for i := 0; i < b.N; i++ {
151		wm.focusAdjacent("right")
152		wm.focusAdjacent("left")
153	}
154}
155
156func BenchmarkStructuralNeighbor(b *testing.B) {
157	wm := newTestWorkspaceManagerWithMocks(b)
158
159	// Create a complex nested structure
160	right, _ := wm.splitNode(wm.root, "right")
161	_, _ = wm.splitNode(right, "down")
162	_, _ = wm.splitNode(wm.root, "down")
163
164	// Set up geometry
165	webkit.SetWidgetBoundsForTesting(wm.root.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100})
166
167	b.ResetTimer()
168	for i := 0; i < b.N; i++ {
169		wm.structuralNeighbor(wm.root, "right")
170		wm.structuralNeighbor(wm.root, "down")
171	}
172}
173
174func BenchmarkClosestLeafFromSubtree(b *testing.B) {
175	wm := newTestWorkspaceManagerWithMocks(b)
176
177	// Build a subtree with multiple leaves
178	right, _ := wm.splitNode(wm.root, "right")
179	_, _ = wm.splitNode(right, "down")
180	_, _ = wm.splitNode(right, "up")
181
182	// Set up geometry for all nodes
183	leaves := wm.collectLeavesFrom(right)
184	for i, leaf := range leaves {
185		webkit.SetWidgetBoundsForTesting(leaf.container, webkit.WidgetBounds{
186			X: float64(i * 50), Y: 0, Width: 45, Height: 100,
187		})
188	}
189
190	b.ResetTimer()
191	for i := 0; i < b.N; i++ {
192		wm.closestLeafFromSubtree(right, 25, 50, "right")
193	}
194}
195
196func BenchmarkComplexTreeOperations(b *testing.B) {
197	b.Run("SplitAndClose", func(b *testing.B) {
198		for i := 0; i < b.N; i++ {
199			b.StopTimer()
200			wm := newTestWorkspaceManagerWithMocks(b)
201			b.StartTimer()
202
203			// Split multiple times then close in reverse order
204			nodes := []*paneNode{wm.root}
205			for j := 0; j < 5; j++ {
206				newNode, err := wm.splitNode(nodes[len(nodes)-1], "right")
207				if err != nil {
208					b.Fatalf("Split failed: %v", err)
209				}
210				nodes = append(nodes, newNode)
211			}
212
213			// Close all but root
214			for j := len(nodes) - 1; j > 0; j-- {
215				err := wm.closePane(nodes[j])
216				if err != nil {
217					b.Fatalf("Close failed: %v", err)
218				}
219			}
220		}
221	})
222
223	b.Run("BuildBalancedTree", func(b *testing.B) {
224		for i := 0; i < b.N; i++ {
225			b.StopTimer()
226			wm := newTestWorkspaceManagerWithMocks(b)
227			b.StartTimer()
228
229			// Build a balanced tree by alternating split directions
230			leaves := []*paneNode{wm.root}
231			directions := []string{"right", "down", "left", "up"}
232
233			for j := 0; j < 15 && len(leaves) > 0; j++ {
234				target := leaves[j%len(leaves)]
235				direction := directions[j%len(directions)]
236
237				newNode, err := wm.splitNode(target, direction)
238				if err != nil {
239					continue
240				}
241				leaves = append(leaves, newNode)
242			}
243		}
244	})
245}
246
247func BenchmarkMemoryOperations(b *testing.B) {
248	b.Run("ViewToNodeMapping", func(b *testing.B) {
249		wm := newTestWorkspaceManagerWithMocks(b)
250
251		// Create many nodes to stress the map
252		for i := 0; i < 100; i++ {
253			leaves := wm.collectLeaves()
254			if len(leaves) > 0 {
255				_, _ = wm.splitNode(leaves[0], "right")
256			}
257		}
258
259		b.ResetTimer()
260		for i := 0; i < b.N; i++ {
261			// Access all mappings
262			for view, node := range wm.viewToNode {
263				_ = view
264				_ = node
265			}
266		}
267	})
268
269	b.Run("TreeWalkMemory", func(b *testing.B) {
270		wm := newTestWorkspaceManagerWithMocks(b)
271
272		// Build deep tree
273		current := wm.root
274		for i := 0; i < 50; i++ {
275			newNode, err := wm.splitNode(current, "right")
276			if err != nil {
277				break
278			}
279			current = newNode
280		}
281
282		b.ResetTimer()
283		for i := 0; i < b.N; i++ {
284			// Multiple tree operations that allocate
285			leaves := wm.collectLeaves()
286			_ = leaves
287			countNodes(wm.root)
288			wm.leftmostLeaf(wm.root)
289		}
290	})
291}
292
293// Helper function for benchmarks
294func newTestWorkspaceManagerWithMocks(tb testing.TB) *WorkspaceManager {
295	tb.Helper()
296	webkit.ResetWidgetStubsForTesting()
297
298	rootView, err := webkit.NewWebView(&webkit.Config{})
299	if err != nil {
300		tb.Fatalf("failed to create root webview: %v", err)
301	}
302
303	rootPane := &BrowserPane{
304		webView: rootView,
305		id:      "root",
306	}
307
308	app := &BrowserApp{
309		panes:      []*BrowserPane{rootPane},
310		activePane: rootPane,
311		webView:    rootView,
312	}
313
314	wm := &WorkspaceManager{
315		app:          app,
316		window:       rootView.Window(),
317		viewToNode:   make(map[*webkit.WebView]*paneNode),
318		lastSplitMsg: make(map[*webkit.WebView]time.Time),
319		lastExitMsg:  make(map[*webkit.WebView]time.Time),
320	}
321
322	wm.createWebViewFn = func() (*webkit.WebView, error) {
323		return webkit.NewWebView(&webkit.Config{})
324	}
325
326	wm.createPaneFn = func(view *webkit.WebView) (*BrowserPane, error) {
327		return &BrowserPane{
328			webView: view,
329			id:      "bench-pane",
330		}, nil
331	}
332
333	root := &paneNode{
334		pane:      rootPane,
335		container: rootView.RootWidget(),
336		isLeaf:    true,
337	}
338	wm.root = root
339	wm.active = root
340	wm.mainPane = root
341	wm.viewToNode[rootView] = root
342
343	return wm
344}

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