main 5041353

omnibox_callback_pressure_stress_test.go RawHistoryFolder

1package component
2
3import (
4	"bufio"
5	"encoding/json"
6	"os"
7	"testing"
8	"time"
9
10	"github.com/bnema/purego"
11	"github.com/bnema/puregotk/v4/gdk"
12	"github.com/bnema/puregotk/v4/glib"
13	"github.com/bnema/puregotk/v4/gobject"
14	"github.com/bnema/puregotk/v4/gtk"
15	"github.com/stretchr/testify/require"
16)
17
18const gtkCallbackStressIterations = 3_200
19
20type callbackLedgerEvent struct {
21	Event string `json:"event"`
22	Type  string `json:"type"`
23}
24
25// runGTKCallbackLifecycleStress exercises real GTK motion controllers and raw
26// tick callbacks while p3 and p0 are repeatedly removed/rebuilt. It is never
27// opt-in: GTK init is skipped only when the host has no usable native display.
28func runGTKCallbackLifecycleStress(t *testing.T) {
29	t.Helper()
30	ledgerPath := t.TempDir() + "/purego-callback-ledger.jsonl"
31	t.Setenv("PUREGO_CALLBACK_LEDGER", "1")
32	t.Setenv("PUREGO_CALLBACK_LEDGER_FILE", ledgerPath)
33	if !gtk.InitCheck() {
34		t.Skip("GTK native display prerequisite unavailable (gtk.InitCheck returned false)")
35	}
36	display := gdk.DisplayGetDefault()
37	if display == nil {
38		t.Skip("GTK native display prerequisite unavailable (no default GDK display)")
39	}
40	mainContext := glib.MainContextDefault()
41	require.NotNil(t, mainContext)
42	done := make(chan struct{})
43	stress := glib.SourceFunc(func(uintptr) bool {
44		defer close(done)
45		runGTKCallbackLifecycleStressOnMain(t, mainContext, ledgerPath)
46		return false
47	})
48	// Invoke guarantees that the entire GTK mutation sequence is owned by the
49	// default context, whether the test thread owns it or a native GTK loop does.
50	mainContext.Invoke(&stress, 0)
51	select {
52	case <-done:
53	case <-time.After(30 * time.Second):
54		t.Fatal("GTK main context did not execute callback lifecycle stress")
55	}
56}
57
58func runGTKCallbackLifecycleStressOnMain(t *testing.T, mainContext *glib.MainContext, ledgerPath string) {
59	t.Helper()
60	assertMainContextOwner := func() {
61		t.Helper()
62		require.True(t, mainContext.IsOwner(), "GTK mutation must own the default GLib main context")
63	}
64
65	assertMainContextOwner()
66	root := gtk.NewBox(gtk.OrientationVerticalValue, 0)
67	assertMainContextOwner()
68	p0 := gtk.NewBox(gtk.OrientationVerticalValue, 0)
69	assertMainContextOwner()
70	p3 := gtk.NewBox(gtk.OrientationVerticalValue, 0)
71	require.NotNil(t, root)
72	require.NotNil(t, p0)
73	require.NotNil(t, p3)
74	assertMainContextOwner()
75	root.Append(&p0.Widget)
76	assertMainContextOwner()
77	root.Append(&p3.Widget)
78	ledgerStart := len(readCallbackLedgerEvents(t, ledgerPath))
79
80	enters, leaves := 0, 0
81	for i := 0; i < gtkCallbackStressIterations; i++ {
82		pane := p0
83		if i%2 != 0 {
84			pane = p3
85		}
86		assertMainContextOwner()
87		controller := gtk.NewEventControllerMotion()
88		require.NotNil(t, controller)
89		enter := func(_ gtk.EventControllerMotion, _ float64, _ float64) { enters++ }
90		leave := func(_ gtk.EventControllerMotion) { leaves++ }
91		enterID := controller.ConnectEnter(&enter)
92		leaveID := controller.ConnectLeave(&leave)
93		assertMainContextOwner()
94		pane.AddController(&controller.EventController)
95
96		// Invoke the real connected handler functions in GTK enter/leave order
97		// while the alternate pane is closed and the workspace rebuilt.
98		enter(*controller, 0, 0)
99		leave(*controller)
100
101		tick := gtk.TickCallback(func(uintptr, uintptr, uintptr) bool { return false })
102		tickCallback := glib.NewCallback(&tick)
103		assertMainContextOwner()
104		tickID := pane.AddTickCallback(&tick, 0, nil)
105		require.NotZero(t, tickID)
106		assertMainContextOwner()
107		pane.RemoveTickCallback(tickID)
108		require.NoError(t, purego.UnrefCallback(tickCallback), "raw tick slot must be released after GTK removal")
109
110		assertMainContextOwner()
111		gobject.SignalHandlerDisconnect(gobject.ObjectNewFromInternalPtr(controller.GoPointer()), enterID)
112		assertMainContextOwner()
113		gobject.SignalHandlerDisconnect(gobject.ObjectNewFromInternalPtr(controller.GoPointer()), leaveID)
114		assertMainContextOwner()
115		pane.RemoveController(&controller.EventController)
116
117		// Alternate p3 <-> p0 close/rebuild mutations on the same main context.
118		assertMainContextOwner()
119		root.Remove(&pane.Widget)
120		assertMainContextOwner()
121		root.Append(&pane.Widget)
122	}
123	require.Equal(t, gtkCallbackStressIterations, enters)
124	require.Equal(t, gtkCallbackStressIterations, leaves)
125
126	events := readCallbackLedgerEvents(t, ledgerPath)[ledgerStart:]
127	var tickReleases, tickReuses int
128	for _, event := range events {
129		switch event.Event {
130		case "release-fnptr", "release":
131			tickReleases++
132		case "reuse":
133			tickReuses++
134		case "release-miss", "release-fnptr-miss", "exhausted":
135			t.Fatalf("callback ledger recorded %s for tick callback", event.Event)
136		}
137	}
138	require.Equal(t, gtkCallbackStressIterations, tickReleases, "every raw GTK tick slot must be released exactly once")
139	// The first motion-controller connection may initialize one shared GTK
140	// trampoline after the baseline; every subsequent raw tick slot is reused.
141	require.GreaterOrEqual(t, tickReuses, gtkCallbackStressIterations-2, "released raw tick slots must be reused")
142}
143
144func readCallbackLedgerEvents(t *testing.T, path string) []callbackLedgerEvent {
145	t.Helper()
146	file, err := os.Open(path)
147	require.NoError(t, err)
148	defer file.Close()
149
150	var events []callbackLedgerEvent
151	scanner := bufio.NewScanner(file)
152	for scanner.Scan() {
153		var event callbackLedgerEvent
154		require.NoError(t, json.Unmarshal(scanner.Bytes(), &event))
155		events = append(events, event)
156	}
157	require.NoError(t, scanner.Err())
158	return events
159}

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