detached bc8a21d

chore(callbacks): make diagnostics opt-in

commit
bc8a21d02fa5c68dad14e1fb915ab195ce749128
author
brice · · 2026-06-09 13:14 UTC
parent
8e5e32b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +86 -4
M README.md +11 -0
@@ -94,6 +94,17 @@ func main() {
9494
9595 Then to run: `CGO_ENABLED=0 go run main.go`
9696
97+## Callback diagnostics
98+
99+Callback allocation diagnostics are disabled by default. When debugging callback slot
100+exhaustion on Unix-like platforms, set `PUREGO_CALLBACK_LEDGER=1` to append JSONL
101+callback lifecycle events to `/tmp/purego-callback-ledger-<pid>.jsonl`, or set
102+`PUREGO_CALLBACK_LEDGER_FILE=/path/to/file.jsonl` to choose the output file. Stack
103+capture is also disabled by default; set `PUREGO_CALLBACK_LEDGER_STACK=1` when call
104+site attribution is needed. For ad-hoc stderr/file traces near callback exhaustion,
105+set `PUREGO_CALLBACK_TRACE=1`; `PUREGO_CALLBACK_TRACE_FILE` overrides the default
106+`/tmp/purego-callback-trace-<pid>.log` path.
107+
97108 ## Questions
98109
99110 If you have questions about how to incorporate purego in your project or want to discuss
@@ -94,6 +94,17 @@ func main() {
94 94
95 Then to run: `CGO_ENABLED=0 go run main.go`95 Then to run: `CGO_ENABLED=0 go run main.go`
96 96
97+## Callback diagnostics
98+
99+Callback allocation diagnostics are disabled by default. When debugging callback slot
100+exhaustion on Unix-like platforms, set `PUREGO_CALLBACK_LEDGER=1` to append JSONL
101+callback lifecycle events to `/tmp/purego-callback-ledger-<pid>.jsonl`, or set
102+`PUREGO_CALLBACK_LEDGER_FILE=/path/to/file.jsonl` to choose the output file. Stack
103+capture is also disabled by default; set `PUREGO_CALLBACK_LEDGER_STACK=1` when call
104+site attribution is needed. For ad-hoc stderr/file traces near callback exhaustion,
105+set `PUREGO_CALLBACK_TRACE=1`; `PUREGO_CALLBACK_TRACE_FILE` overrides the default
106+`/tmp/purego-callback-trace-<pid>.log` path.
107+
97 ## Questions108 ## Questions
98 109
99 If you have questions about how to incorporate purego in your project or want to discuss110 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
@@ -196,8 +196,25 @@ type callbackLedgerEvent struct {
196196 Error string `json:"error,omitempty"`
197197 }
198198
199+func callbackEnvEnabled(name string) bool {
200+ switch strings.ToLower(os.Getenv(name)) {
201+ case "1", "true", "yes", "on":
202+ return true
203+ default:
204+ return false
205+ }
206+}
207+
199208 func callbackLedgerEnabled() bool {
200- return os.Getenv("PUREGO_CALLBACK_LEDGER") != "0"
209+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER")
210+}
211+
212+func callbackLedgerStackEnabled() bool {
213+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER_STACK")
214+}
215+
216+func callbackTraceEnabled() bool {
217+ return callbackEnvEnabled("PUREGO_CALLBACK_TRACE")
201218 }
202219
203220 func callbackLedgerPath() string {
@@ -271,8 +288,13 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
271288 if !callbackLedgerEnabled() {
272289 return
273290 }
274- stack := string(debug.Stack())
275- family, stackKey := classifyCallbackStack(stack)
291+ stack := ""
292+ family := "unknown"
293+ stackKey := "unknown"
294+ if callbackLedgerStackEnabled() {
295+ stack = string(debug.Stack())
296+ family, stackKey = classifyCallbackStack(stack)
297+ }
276298 cbs.ledgerSeq++
277299 occupied := cbs.numFn - len(cbs.holes)
278300 entry := callbackLedgerEvent{
@@ -296,7 +318,7 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
296318 ReusedSlot: reused,
297319 Error: errText,
298320 }
299- if os.Getenv("PUREGO_CALLBACK_LEDGER_STACK") != "0" {
321+ if callbackLedgerStackEnabled() {
300322 entry.Stack = stack
301323 }
302324 payload, err := json.Marshal(entry)
@@ -311,6 +333,9 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
311333 }
312334
313335 func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
336+ if !callbackTraceEnabled() {
337+ return
338+ }
314339 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())
315340 fmt.Fprint(os.Stderr, message)
316341 traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")
@@ -196,8 +196,25 @@ type callbackLedgerEvent struct {
196 Error string `json:"error,omitempty"`196 Error string `json:"error,omitempty"`
197 }197 }
198 198
199+func callbackEnvEnabled(name string) bool {
200+ switch strings.ToLower(os.Getenv(name)) {
201+ case "1", "true", "yes", "on":
202+ return true
203+ default:
204+ return false
205+ }
206+}
207+
199 func callbackLedgerEnabled() bool {208 func callbackLedgerEnabled() bool {
200- return os.Getenv("PUREGO_CALLBACK_LEDGER") != "0"209+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER")
210+}
211+
212+func callbackLedgerStackEnabled() bool {
213+ return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER_STACK")
214+}
215+
216+func callbackTraceEnabled() bool {
217+ return callbackEnvEnabled("PUREGO_CALLBACK_TRACE")
201 }218 }
202 219
203 func callbackLedgerPath() string {220 func callbackLedgerPath() string {
@@ -271,8 +288,13 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
271 if !callbackLedgerEnabled() {288 if !callbackLedgerEnabled() {
272 return289 return
273 }290 }
274- stack := string(debug.Stack())291+ stack := ""
275- family, stackKey := classifyCallbackStack(stack)292+ family := "unknown"
293+ stackKey := "unknown"
294+ if callbackLedgerStackEnabled() {
295+ stack = string(debug.Stack())
296+ family, stackKey = classifyCallbackStack(stack)
297+ }
276 cbs.ledgerSeq++298 cbs.ledgerSeq++
277 occupied := cbs.numFn - len(cbs.holes)299 occupied := cbs.numFn - len(cbs.holes)
278 entry := callbackLedgerEvent{300 entry := callbackLedgerEvent{
@@ -296,7 +318,7 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
296 ReusedSlot: reused,318 ReusedSlot: reused,
297 Error: errText,319 Error: errText,
298 }320 }
299- if os.Getenv("PUREGO_CALLBACK_LEDGER_STACK") != "0" {321+ if callbackLedgerStackEnabled() {
300 entry.Stack = stack322 entry.Stack = stack
301 }323 }
302 payload, err := json.Marshal(entry)324 payload, err := json.Marshal(entry)
@@ -311,6 +333,9 @@ func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr,
311 }333 }
312 334
313 func traceCallbackAllocation(event string, val reflect.Value, remaining int) {335 func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
336+ if !callbackTraceEnabled() {
337+ return
338+ }
314 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())339 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())
315 fmt.Fprint(os.Stderr, message)340 fmt.Fprint(os.Stderr, message)
316 traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")341 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