detached e3d4644

chore(callbacks): make diagnostics opt-in

commit
e3d4644ec3b9b5c5246fc0ab5c3be7dc935ffebf
author
brice · · 2026-06-09 13:14 UTC
committer
brice · · 2026-09-27 05:49 UTC
parent
452be71
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +86 -4
M README.md +11 -0
@@ -95,6 +95,17 @@ func main() {
9595
9696 Then to run: `CGO_ENABLED=0 go run main.go`
9797
98+## Callback diagnostics
99+
100+Callback allocation diagnostics are disabled by default. When debugging callback slot
101+exhaustion on Unix-like platforms, set `PUREGO_CALLBACK_LEDGER=1` to append JSONL
102+callback lifecycle events to `/tmp/purego-callback-ledger-<pid>.jsonl`, or set
103+`PUREGO_CALLBACK_LEDGER_FILE=/path/to/file.jsonl` to choose the output file. Stack
104+capture is also disabled by default; set `PUREGO_CALLBACK_LEDGER_STACK=1` when call
105+site attribution is needed. For ad-hoc stderr/file traces near callback exhaustion,
106+set `PUREGO_CALLBACK_TRACE=1`; `PUREGO_CALLBACK_TRACE_FILE` overrides the default
107+`/tmp/purego-callback-trace-<pid>.log` path.
108+
98109 ## Questions
99110
100111 If you have questions about how to incorporate purego in your project or want to discuss
@@ -95,6 +95,17 @@ func main() {
95 95
96 Then to run: `CGO_ENABLED=0 go run main.go`96 Then to run: `CGO_ENABLED=0 go run main.go`
97 97
98+## Callback diagnostics
99+
100+Callback allocation diagnostics are disabled by default. When debugging callback slot
101+exhaustion on Unix-like platforms, set `PUREGO_CALLBACK_LEDGER=1` to append JSONL
102+callback lifecycle events to `/tmp/purego-callback-ledger-<pid>.jsonl`, or set
103+`PUREGO_CALLBACK_LEDGER_FILE=/path/to/file.jsonl` to choose the output file. Stack
104+capture is also disabled by default; set `PUREGO_CALLBACK_LEDGER_STACK=1` when call
105+site attribution is needed. For ad-hoc stderr/file traces near callback exhaustion,
106+set `PUREGO_CALLBACK_TRACE=1`; `PUREGO_CALLBACK_TRACE_FILE` overrides the default
107+`/tmp/purego-callback-trace-<pid>.log` path.
108+
98 ## Questions109 ## Questions
99 110
100 If you have questions about how to incorporate purego in your project or want to discuss111 If you have questions about how to incorporate purego in your project or want to discuss
A callback_diagnostics_test.go +46 -0
@@ -0,0 +1,46 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
5+
6+package purego
7+
8+import "testing"
9+
10+func TestCallbackDiagnosticsDisabledByDefault(t *testing.T) {
11+ t.Setenv("PUREGO_CALLBACK_LEDGER", "")
12+ t.Setenv("PUREGO_CALLBACK_LEDGER_STACK", "")
13+ t.Setenv("PUREGO_CALLBACK_TRACE", "")
14+
15+ if callbackLedgerEnabled() {
16+ t.Fatal("callback ledger must be disabled by default")
17+ }
18+ if callbackLedgerStackEnabled() {
19+ t.Fatal("callback ledger stack capture must be disabled by default")
20+ }
21+ if callbackTraceEnabled() {
22+ t.Fatal("callback trace must be disabled by default")
23+ }
24+}
25+
26+func TestCallbackDiagnosticsExplicitOptIn(t *testing.T) {
27+ for _, value := range []string{"1", "true", "yes", "on", "TRUE", "On"} {
28+ t.Run(value, func(t *testing.T) {
29+ t.Setenv("PUREGO_CALLBACK_LEDGER", value)
30+ if !callbackLedgerEnabled() {
31+ t.Fatalf("callback ledger was not enabled by %q", value)
32+ }
33+ })
34+ }
35+}
36+
37+func TestCallbackDiagnosticsRejectsImplicitValues(t *testing.T) {
38+ for _, value := range []string{"0", "false", "no", "off", "anything-else"} {
39+ t.Run(value, func(t *testing.T) {
40+ t.Setenv("PUREGO_CALLBACK_LEDGER", value)
41+ if callbackLedgerEnabled() {
42+ t.Fatalf("callback ledger unexpectedly enabled by %q", value)
43+ }
44+ })
45+ }
46+}
@@ -0,0 +1,46 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
5+
6+package purego
7+
8+import "testing"
9+
10+func TestCallbackDiagnosticsDisabledByDefault(t *testing.T) {
11+ t.Setenv("PUREGO_CALLBACK_LEDGER", "")
12+ t.Setenv("PUREGO_CALLBACK_LEDGER_STACK", "")
13+ t.Setenv("PUREGO_CALLBACK_TRACE", "")
14+
15+ if callbackLedgerEnabled() {
16+ t.Fatal("callback ledger must be disabled by default")
17+ }
18+ if callbackLedgerStackEnabled() {
19+ t.Fatal("callback ledger stack capture must be disabled by default")
20+ }
21+ if callbackTraceEnabled() {
22+ t.Fatal("callback trace must be disabled by default")
23+ }
24+}
25+
26+func TestCallbackDiagnosticsExplicitOptIn(t *testing.T) {
27+ for _, value := range []string{"1", "true", "yes", "on", "TRUE", "On"} {
28+ t.Run(value, func(t *testing.T) {
29+ t.Setenv("PUREGO_CALLBACK_LEDGER", value)
30+ if !callbackLedgerEnabled() {
31+ t.Fatalf("callback ledger was not enabled by %q", value)
32+ }
33+ })
34+ }
35+}
36+
37+func TestCallbackDiagnosticsRejectsImplicitValues(t *testing.T) {
38+ for _, value := range []string{"0", "false", "no", "off", "anything-else"} {
39+ t.Run(value, func(t *testing.T) {
40+ t.Setenv("PUREGO_CALLBACK_LEDGER", value)
41+ if callbackLedgerEnabled() {
42+ t.Fatalf("callback ledger unexpectedly enabled by %q", value)
43+ }
44+ })
45+ }
46+}
M syscall_unix.go +29 -4
@@ -193,8 +193,25 @@ type callbackLedgerEvent struct {
193193 Error string `json:"error,omitempty"`
194194 }
195195
196+func callbackEnvEnabled(name string) bool {
197+ switch strings.ToLower(os.Getenv(name)) {
198+ case "1", "true", "yes", "on":
199+ return true
200+ default:
201+ return false
202+ }
203+}
204+
196205 func callbackLedgerEnabled() bool {
197- return os.Getenv("PUREGO_CALLBACK_LEDGER") != "0"
206+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER")
207+}
208+
209+func callbackLedgerStackEnabled() bool {
210+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER_STACK")
211+}
212+
213+func callbackTraceEnabled() bool {
214+ return callbackEnvEnabled("PUREGO_CALLBACK_TRACE")
198215 }
199216
200217 func callbackLedgerPath() string {
@@ -268,8 +285,13 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
268285 if !callbackLedgerEnabled() {
269286 return
270287 }
271- stack := string(debug.Stack())
272- family, stackKey := classifyCallbackStack(stack)
288+ stack := ""
289+ family := "unknown"
290+ stackKey := "unknown"
291+ if callbackLedgerStackEnabled() {
292+ stack = string(debug.Stack())
293+ family, stackKey = classifyCallbackStack(stack)
294+ }
273295 cbs.ledgerSeq++
274296 occupied := cbs.numFn - len(cbs.holes)
275297 entry := callbackLedgerEvent{
@@ -293,7 +315,7 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
293315 ReusedSlot: reused,
294316 Error: errText,
295317 }
296- if os.Getenv("PUREGO_CALLBACK_LEDGER_STACK") != "0" {
318+ if callbackLedgerStackEnabled() {
297319 entry.Stack = stack
298320 }
299321 payload, err := json.Marshal(entry)
@@ -308,6 +330,9 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
308330 }
309331
310332 func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
333+ if !callbackTraceEnabled() {
334+ return
335+ }
311336 message := fmt.Sprintf("PUREGO-CALLBACK-TRACE event=%s remaining=%d live=%d type=%s\n%s\n", event, remaining, maxCB-remaining, val.Type(), debug.Stack())
312337 fmt.Fprint(os.Stderr, message)
313338 traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")
@@ -193,8 +193,25 @@ type callbackLedgerEvent struct {
193 Error string `json:"error,omitempty"`193 Error string `json:"error,omitempty"`
194 }194 }
195 195
196+func callbackEnvEnabled(name string) bool {
197+ switch strings.ToLower(os.Getenv(name)) {
198+ case "1", "true", "yes", "on":
199+ return true
200+ default:
201+ return false
202+ }
203+}
204+
196 func callbackLedgerEnabled() bool {205 func callbackLedgerEnabled() bool {
197- return os.Getenv("PUREGO_CALLBACK_LEDGER") != "0"206+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER")
207+}
208+
209+func callbackLedgerStackEnabled() bool {
210+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER_STACK")
211+}
212+
213+func callbackTraceEnabled() bool {
214+ return callbackEnvEnabled("PUREGO_CALLBACK_TRACE")
198 }215 }
199 216
200 func callbackLedgerPath() string {217 func callbackLedgerPath() string {
@@ -268,8 +285,13 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
268 if !callbackLedgerEnabled() {285 if !callbackLedgerEnabled() {
269 return286 return
270 }287 }
271- stack := string(debug.Stack())288+ stack := ""
272- family, stackKey := classifyCallbackStack(stack)289+ family := "unknown"
290+ stackKey := "unknown"
291+ if callbackLedgerStackEnabled() {
292+ stack = string(debug.Stack())
293+ family, stackKey = classifyCallbackStack(stack)
294+ }
273 cbs.ledgerSeq++295 cbs.ledgerSeq++
274 occupied := cbs.numFn - len(cbs.holes)296 occupied := cbs.numFn - len(cbs.holes)
275 entry := callbackLedgerEvent{297 entry := callbackLedgerEvent{
@@ -293,7 +315,7 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
293 ReusedSlot: reused,315 ReusedSlot: reused,
294 Error: errText,316 Error: errText,
295 }317 }
296- if os.Getenv("PUREGO_CALLBACK_LEDGER_STACK") != "0" {318+ if callbackLedgerStackEnabled() {
297 entry.Stack = stack319 entry.Stack = stack
298 }320 }
299 payload, err := json.Marshal(entry)321 payload, err := json.Marshal(entry)
@@ -308,6 +330,9 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
308 }330 }
309 331
310 func traceCallbackAllocation(event string, val reflect.Value, remaining int) {332 func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
333+ if !callbackTraceEnabled() {
334+ return
335+ }
311 message := fmt.Sprintf("PUREGO-CALLBACK-TRACE event=%s remaining=%d live=%d type=%s\n%s\n", event, remaining, maxCB-remaining, val.Type(), debug.Stack())336 message := fmt.Sprintf("PUREGO-CALLBACK-TRACE event=%s remaining=%d live=%d type=%s\n%s\n", event, remaining, maxCB-remaining, val.Type(), debug.Stack())
312 fmt.Fprint(os.Stderr, message)337 fmt.Fprint(os.Stderr, message)
313 traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")338 traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")

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 1

Tags 102

↑↓ moveenter openesc close