detached 452be71

chore(callbacks): add callback ledger instrumentation

commit
452be713f589b53733ad2bcc56aed87e17aa860d
author
brice · · 2026-06-09 12:34 UTC
committer
brice · · 2026-09-27 05:49 UTC
parent
b335e8a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +196 -7
M syscall_unix.go +196 -7
@@ -6,14 +6,20 @@
66 package purego
77
88 import (
9+ "encoding/json"
910 "errors"
11+ "fmt"
1012 "math"
13+ "os"
1114 "reflect"
1215 "runtime"
16+ "runtime/debug"
17+ "strings"
1318 "sync"
19+ "time"
1420 "unsafe"
1521
16- "github.com/bnema/purego/internal/strings"
22+ internalstrings "github.com/bnema/purego/internal/strings"
1723 )
1824
1925 var syscallXABI0 uintptr
@@ -60,7 +66,10 @@ func NewCallbackFnPtr(fnPtr any) uintptr {
6066 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
6167 panic("purego: the type must be a function pointer but was not")
6268 }
63- if addr, ok := getCallbackByFnPtr(val); ok {
69+ if addr, idx, ok := getCallbackByFnPtr(val); ok {
70+ cbs.lock.Lock()
71+ recordCallbackLedger("dedupe", idx, addr, val.Pointer(), val.Elem(), false, "")
72+ cbs.lock.Unlock()
6473 return addr
6574 }
6675 addr := compileCallback(val.Elem().Interface())
@@ -68,6 +77,7 @@ func NewCallbackFnPtr(fnPtr any) uintptr {
6877 cbs.knownFnPtr[val.Pointer()] = addr
6978 if idx, ok := cbs.knownIdx[addr]; ok {
7079 cbs.fnPtrKeys[idx] = val.Pointer()
80+ recordCallbackLedger("fnptr-bind", idx, addr, val.Pointer(), val.Elem(), false, "")
7181 }
7282 cbs.lock.Unlock()
7383 return addr
@@ -79,15 +89,19 @@ func UnrefCallback(cb uintptr) error {
7989 defer cbs.lock.Unlock()
8090 idx, ok := cbs.knownIdx[cb]
8191 if !ok {
92+ recordCallbackLedger("release-miss", -1, cb, 0, reflect.Value{}, false, "callback not found")
8293 return errors.New("callback not found")
8394 }
84- if key := cbs.fnPtrKeys[idx]; key != 0 {
95+ val := cbs.funcs[idx]
96+ key := cbs.fnPtrKeys[idx]
97+ if key != 0 {
8598 delete(cbs.knownFnPtr, key)
8699 cbs.fnPtrKeys[idx] = 0
87100 }
88101 delete(cbs.knownIdx, cb)
89102 cbs.holes[idx] = struct{}{}
90103 cbs.funcs[idx] = reflect.Value{}
104+ recordCallbackLedger("release", idx, cb, key, val, false, "")
91105 return nil
92106 }
93107
@@ -104,18 +118,22 @@ func UnrefCallbackFnPtr(fnPtr any) error {
104118 defer cbs.lock.Unlock()
105119 addr, ok := cbs.knownFnPtr[val.Pointer()]
106120 if !ok {
121+ recordCallbackLedger("release-fnptr-miss", -1, 0, val.Pointer(), val.Elem(), false, "callback not found")
107122 return errors.New("callback not found")
108123 }
109124 idx, ok := cbs.knownIdx[addr]
110125 if !ok {
111126 delete(cbs.knownFnPtr, val.Pointer())
127+ recordCallbackLedger("release-fnptr-miss", -1, addr, val.Pointer(), val.Elem(), false, "callback not found")
112128 return errors.New("callback not found")
113129 }
130+ callbackVal := cbs.funcs[idx]
114131 delete(cbs.knownFnPtr, val.Pointer())
115132 delete(cbs.knownIdx, addr)
116133 cbs.fnPtrKeys[idx] = 0
117134 cbs.holes[idx] = struct{}{}
118135 cbs.funcs[idx] = reflect.Value{}
136+ recordCallbackLedger("release-fnptr", idx, addr, val.Pointer(), callbackVal, false, "")
119137 return nil
120138 }
121139
@@ -131,17 +149,175 @@ var cbs = struct {
131149 knownIdx map[uintptr]int // callback address -> slot index
132150 knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
133151 fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
152+ ledgerSeq uint64 // monotonic callback ledger event sequence
134153 }{
135154 holes: make(map[int]struct{}),
136155 knownIdx: make(map[uintptr]int, maxCB),
137156 knownFnPtr: make(map[uintptr]uintptr, maxCB),
138157 }
139158
140-func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
159+func getCallbackByFnPtr(val reflect.Value) (uintptr, int, bool) {
141160 cbs.lock.RLock()
142161 defer cbs.lock.RUnlock()
143162 addr, ok := cbs.knownFnPtr[val.Pointer()]
144- return addr, ok
163+ if !ok {
164+ return 0, -1, false
165+ }
166+ idx, ok := cbs.knownIdx[addr]
167+ if !ok {
168+ return addr, -1, true
169+ }
170+ return addr, idx, true
171+}
172+
173+type callbackLedgerEvent struct {
174+ Marker string `json:"marker"`
175+ PID int `json:"pid"`
176+ Seq uint64 `json:"seq"`
177+ Time string `json:"time"`
178+ Event string `json:"event"`
179+ Index int `json:"index"`
180+ Addr string `json:"addr,omitempty"`
181+ FnPtrKey string `json:"fn_ptr_key,omitempty"`
182+ Type string `json:"type,omitempty"`
183+ Family string `json:"family"`
184+ StackKey string `json:"stack_key"`
185+ NumFn int `json:"num_fn"`
186+ Occupied int `json:"occupied"`
187+ Reusable int `json:"reusable"`
188+ KnownFnPtr int `json:"known_fn_ptr"`
189+ Remaining int `json:"remaining"`
190+ Max int `json:"max"`
191+ ReusedSlot bool `json:"reused_slot,omitempty"`
192+ Stack string `json:"stack,omitempty"`
193+ Error string `json:"error,omitempty"`
194+}
195+
196+func callbackLedgerEnabled() bool {
197+ return os.Getenv("PUREGO_CALLBACK_LEDGER") != "0"
198+}
199+
200+func callbackLedgerPath() string {
201+ if path := os.Getenv("PUREGO_CALLBACK_LEDGER_FILE"); path != "" {
202+ return path
203+ }
204+ return fmt.Sprintf("/tmp/purego-callback-ledger-%d.jsonl", os.Getpid())
205+}
206+
207+func typeName(val reflect.Value) string {
208+ if !val.IsValid() {
209+ return ""
210+ }
211+ return val.Type().String()
212+}
213+
214+func hexUintptr(v uintptr) string {
215+ if v == 0 {
216+ return ""
217+ }
218+ return fmt.Sprintf("0x%x", v)
219+}
220+
221+func classifyCallbackStack(stack string) (family, stackKey string) {
222+ family = "unknown"
223+ stackKey = "unknown"
224+ lines := strings.Split(stack, "\n")
225+ for i, line := range lines {
226+ if line == "" || strings.HasPrefix(line, "goroutine ") || strings.Contains(line, "runtime/debug.Stack") || strings.Contains(line, "recordCallbackLedger") || strings.Contains(line, "traceCallbackAllocation") {
227+ continue
228+ }
229+ if strings.Contains(line, "github.com/bnema/purego.") || strings.Contains(line, "github.com/ebitengine/purego.") {
230+ continue
231+ }
232+ if strings.HasPrefix(strings.TrimSpace(line), "/") || strings.HasPrefix(strings.TrimSpace(line), "../") {
233+ continue
234+ }
235+ stackKey = strings.TrimSpace(line)
236+ for j := i + 1; j < len(lines); j++ {
237+ fileLine := strings.TrimSpace(lines[j])
238+ if fileLine == "" {
239+ continue
240+ }
241+ family = classifyCallbackFamily(fileLine + " " + stackKey)
242+ return family, stackKey
243+ }
244+ family = classifyCallbackFamily(stackKey)
245+ return family, stackKey
246+ }
247+ return family, stackKey
248+}
249+
250+func classifyCallbackFamily(s string) string {
251+ switch {
252+ case strings.Contains(s, "purego-cef2gtk"):
253+ return "purego-cef2gtk"
254+ case strings.Contains(s, "purego-cef"):
255+ return "purego-cef"
256+ case strings.Contains(s, "puregotk"):
257+ return "puregotk"
258+ case strings.Contains(s, "dumber"):
259+ return "dumber"
260+ case strings.Contains(s, "github.com/bnema/purego") || strings.Contains(s, "/purego/"):
261+ return "purego"
262+ default:
263+ return "unknown"
264+ }
265+}
266+
267+func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr, val reflect.Value, reused bool, errText string) {
268+ if !callbackLedgerEnabled() {
269+ return
270+ }
271+ stack := string(debug.Stack())
272+ family, stackKey := classifyCallbackStack(stack)
273+ cbs.ledgerSeq++
274+ occupied := cbs.numFn - len(cbs.holes)
275+ entry := callbackLedgerEvent{
276+ Marker: "PUREGO-CALLBACK-LEDGER",
277+ PID: os.Getpid(),
278+ Seq: cbs.ledgerSeq,
279+ Time: time.Now().Format(time.RFC3339Nano),
280+ Event: event,
281+ Index: idx,
282+ Addr: hexUintptr(addr),
283+ FnPtrKey: hexUintptr(fnPtrKey),
284+ Type: typeName(val),
285+ Family: family,
286+ StackKey: stackKey,
287+ NumFn: cbs.numFn,
288+ Occupied: occupied,
289+ Reusable: len(cbs.holes),
290+ KnownFnPtr: len(cbs.knownFnPtr),
291+ Remaining: maxCB - occupied,
292+ Max: maxCB,
293+ ReusedSlot: reused,
294+ Error: errText,
295+ }
296+ if os.Getenv("PUREGO_CALLBACK_LEDGER_STACK") != "0" {
297+ entry.Stack = stack
298+ }
299+ payload, err := json.Marshal(entry)
300+ if err != nil {
301+ return
302+ }
303+ payload = append(payload, '\n')
304+ if f, err := os.OpenFile(callbackLedgerPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
305+ _, _ = f.Write(payload)
306+ _ = f.Close()
307+ }
308+}
309+
310+func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
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())
312+ fmt.Fprint(os.Stderr, message)
313+ traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")
314+ if traceFile == "" {
315+ traceFile = fmt.Sprintf("/tmp/purego-callback-trace-%d.log", os.Getpid())
316+ }
317+ if f, err := os.OpenFile(traceFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
318+ _, _ = f.WriteString(message)
319+ _ = f.Close()
320+ }
145321 }
146322
147323 func compileCallback(fn any) uintptr {
@@ -189,21 +365,34 @@ output:
189365 cbs.lock.Lock()
190366 defer cbs.lock.Unlock()
191367 index := -1
368+ reused := false
192369 for i := range cbs.holes {
193370 index = i
194371 delete(cbs.holes, i)
372+ reused = true
195373 break
196374 }
197375 if index < 0 {
198- if cbs.numFn >= maxCB {
376+ remaining := maxCB - cbs.numFn
377+ if remaining <= 0 {
378+ recordCallbackLedger("exhausted", -1, 0, 0, val, false, "maximum callbacks reached")
379+ traceCallbackAllocation("exhausted", val, 0)
199380 panic("purego: the maximum number of callbacks has been reached")
200381 }
382+ if remaining <= 100 || remaining%250 == 0 {
383+ traceCallbackAllocation("allocated", val, remaining)
384+ }
201385 index = cbs.numFn
202386 cbs.numFn++
203387 }
204388 cbs.funcs[index] = val
205389 addr := callbackasmAddr(index)
206390 cbs.knownIdx[addr] = index
391+ if reused {
392+ recordCallbackLedger("reuse", index, addr, 0, val, true, "")
393+ } else {
394+ recordCallbackLedger("alloc", index, addr, 0, val, false, "")
395+ }
207396 return addr
208397 }
209398
@@ -350,7 +539,7 @@ func callbackWrap(a *callbackArgs) {
350539 ptr = frame[pos]
351540 }
352541 intsN += slots
353- args[i] = reflect.ValueOf(strings.GoString(ptr))
542+ args[i] = reflect.ValueOf(internalstrings.GoString(ptr))
354543 continue
355544 case reflect.Struct:
356545 if i == 0 && inType.AssignableTo(reflect.TypeFor[CDecl]()) {
@@ -6,14 +6,20 @@
6 package purego6 package purego
7 7
8 import (8 import (
9+ "encoding/json"
9 "errors"10 "errors"
11+ "fmt"
10 "math"12 "math"
13+ "os"
11 "reflect"14 "reflect"
12 "runtime"15 "runtime"
16+ "runtime/debug"
17+ "strings"
13 "sync"18 "sync"
19+ "time"
14 "unsafe"20 "unsafe"
15 21
16- "github.com/bnema/purego/internal/strings"22+ internalstrings "github.com/bnema/purego/internal/strings"
17 )23 )
18 24
19 var syscallXABI0 uintptr25 var syscallXABI0 uintptr
@@ -60,7 +66,10 @@ func NewCallbackFnPtr(fnPtr any) uintptr {
60 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {66 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
61 panic("purego: the type must be a function pointer but was not")67 panic("purego: the type must be a function pointer but was not")
62 }68 }
63- if addr, ok := getCallbackByFnPtr(val); ok {69+ if addr, idx, ok := getCallbackByFnPtr(val); ok {
70+ cbs.lock.Lock()
71+ recordCallbackLedger("dedupe", idx, addr, val.Pointer(), val.Elem(), false, "")
72+ cbs.lock.Unlock()
64 return addr73 return addr
65 }74 }
66 addr := compileCallback(val.Elem().Interface())75 addr := compileCallback(val.Elem().Interface())
@@ -68,6 +77,7 @@ func NewCallbackFnPtr(fnPtr any) uintptr {
68 cbs.knownFnPtr[val.Pointer()] = addr77 cbs.knownFnPtr[val.Pointer()] = addr
69 if idx, ok := cbs.knownIdx[addr]; ok {78 if idx, ok := cbs.knownIdx[addr]; ok {
70 cbs.fnPtrKeys[idx] = val.Pointer()79 cbs.fnPtrKeys[idx] = val.Pointer()
80+ recordCallbackLedger("fnptr-bind", idx, addr, val.Pointer(), val.Elem(), false, "")
71 }81 }
72 cbs.lock.Unlock()82 cbs.lock.Unlock()
73 return addr83 return addr
@@ -79,15 +89,19 @@ func UnrefCallback(cb uintptr) error {
79 defer cbs.lock.Unlock()89 defer cbs.lock.Unlock()
80 idx, ok := cbs.knownIdx[cb]90 idx, ok := cbs.knownIdx[cb]
81 if !ok {91 if !ok {
92+ recordCallbackLedger("release-miss", -1, cb, 0, reflect.Value{}, false, "callback not found")
82 return errors.New("callback not found")93 return errors.New("callback not found")
83 }94 }
84- if key := cbs.fnPtrKeys[idx]; key != 0 {95+ val := cbs.funcs[idx]
96+ key := cbs.fnPtrKeys[idx]
97+ if key != 0 {
85 delete(cbs.knownFnPtr, key)98 delete(cbs.knownFnPtr, key)
86 cbs.fnPtrKeys[idx] = 099 cbs.fnPtrKeys[idx] = 0
87 }100 }
88 delete(cbs.knownIdx, cb)101 delete(cbs.knownIdx, cb)
89 cbs.holes[idx] = struct{}{}102 cbs.holes[idx] = struct{}{}
90 cbs.funcs[idx] = reflect.Value{}103 cbs.funcs[idx] = reflect.Value{}
104+ recordCallbackLedger("release", idx, cb, key, val, false, "")
91 return nil105 return nil
92 }106 }
93 107
@@ -104,18 +118,22 @@ func UnrefCallbackFnPtr(fnPtr any) error {
104 defer cbs.lock.Unlock()118 defer cbs.lock.Unlock()
105 addr, ok := cbs.knownFnPtr[val.Pointer()]119 addr, ok := cbs.knownFnPtr[val.Pointer()]
106 if !ok {120 if !ok {
121+ recordCallbackLedger("release-fnptr-miss", -1, 0, val.Pointer(), val.Elem(), false, "callback not found")
107 return errors.New("callback not found")122 return errors.New("callback not found")
108 }123 }
109 idx, ok := cbs.knownIdx[addr]124 idx, ok := cbs.knownIdx[addr]
110 if !ok {125 if !ok {
111 delete(cbs.knownFnPtr, val.Pointer())126 delete(cbs.knownFnPtr, val.Pointer())
127+ recordCallbackLedger("release-fnptr-miss", -1, addr, val.Pointer(), val.Elem(), false, "callback not found")
112 return errors.New("callback not found")128 return errors.New("callback not found")
113 }129 }
130+ callbackVal := cbs.funcs[idx]
114 delete(cbs.knownFnPtr, val.Pointer())131 delete(cbs.knownFnPtr, val.Pointer())
115 delete(cbs.knownIdx, addr)132 delete(cbs.knownIdx, addr)
116 cbs.fnPtrKeys[idx] = 0133 cbs.fnPtrKeys[idx] = 0
117 cbs.holes[idx] = struct{}{}134 cbs.holes[idx] = struct{}{}
118 cbs.funcs[idx] = reflect.Value{}135 cbs.funcs[idx] = reflect.Value{}
136+ recordCallbackLedger("release-fnptr", idx, addr, val.Pointer(), callbackVal, false, "")
119 return nil137 return nil
120 }138 }
121 139
@@ -131,17 +149,175 @@ var cbs = struct {
131 knownIdx map[uintptr]int // callback address -> slot index149 knownIdx map[uintptr]int // callback address -> slot index
132 knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address150 knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
133 fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address151 fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
152+ ledgerSeq uint64 // monotonic callback ledger event sequence
134 }{153 }{
135 holes: make(map[int]struct{}),154 holes: make(map[int]struct{}),
136 knownIdx: make(map[uintptr]int, maxCB),155 knownIdx: make(map[uintptr]int, maxCB),
137 knownFnPtr: make(map[uintptr]uintptr, maxCB),156 knownFnPtr: make(map[uintptr]uintptr, maxCB),
138 }157 }
139 158
140-func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {159+func getCallbackByFnPtr(val reflect.Value) (uintptr, int, bool) {
141 cbs.lock.RLock()160 cbs.lock.RLock()
142 defer cbs.lock.RUnlock()161 defer cbs.lock.RUnlock()
143 addr, ok := cbs.knownFnPtr[val.Pointer()]162 addr, ok := cbs.knownFnPtr[val.Pointer()]
144- return addr, ok163+ if !ok {
164+ return 0, -1, false
165+ }
166+ idx, ok := cbs.knownIdx[addr]
167+ if !ok {
168+ return addr, -1, true
169+ }
170+ return addr, idx, true
171+}
172+
173+type callbackLedgerEvent struct {
174+ Marker string `json:"marker"`
175+ PID int `json:"pid"`
176+ Seq uint64 `json:"seq"`
177+ Time string `json:"time"`
178+ Event string `json:"event"`
179+ Index int `json:"index"`
180+ Addr string `json:"addr,omitempty"`
181+ FnPtrKey string `json:"fn_ptr_key,omitempty"`
182+ Type string `json:"type,omitempty"`
183+ Family string `json:"family"`
184+ StackKey string `json:"stack_key"`
185+ NumFn int `json:"num_fn"`
186+ Occupied int `json:"occupied"`
187+ Reusable int `json:"reusable"`
188+ KnownFnPtr int `json:"known_fn_ptr"`
189+ Remaining int `json:"remaining"`
190+ Max int `json:"max"`
191+ ReusedSlot bool `json:"reused_slot,omitempty"`
192+ Stack string `json:"stack,omitempty"`
193+ Error string `json:"error,omitempty"`
194+}
195+
196+func callbackLedgerEnabled() bool {
197+ return os.Getenv("PUREGO_CALLBACK_LEDGER") != "0"
198+}
199+
200+func callbackLedgerPath() string {
201+ if path := os.Getenv("PUREGO_CALLBACK_LEDGER_FILE"); path != "" {
202+ return path
203+ }
204+ return fmt.Sprintf("/tmp/purego-callback-ledger-%d.jsonl", os.Getpid())
205+}
206+
207+func typeName(val reflect.Value) string {
208+ if !val.IsValid() {
209+ return ""
210+ }
211+ return val.Type().String()
212+}
213+
214+func hexUintptr(v uintptr) string {
215+ if v == 0 {
216+ return ""
217+ }
218+ return fmt.Sprintf("0x%x", v)
219+}
220+
221+func classifyCallbackStack(stack string) (family, stackKey string) {
222+ family = "unknown"
223+ stackKey = "unknown"
224+ lines := strings.Split(stack, "\n")
225+ for i, line := range lines {
226+ if line == "" || strings.HasPrefix(line, "goroutine ") || strings.Contains(line, "runtime/debug.Stack") || strings.Contains(line, "recordCallbackLedger") || strings.Contains(line, "traceCallbackAllocation") {
227+ continue
228+ }
229+ if strings.Contains(line, "github.com/bnema/purego.") || strings.Contains(line, "github.com/ebitengine/purego.") {
230+ continue
231+ }
232+ if strings.HasPrefix(strings.TrimSpace(line), "/") || strings.HasPrefix(strings.TrimSpace(line), "../") {
233+ continue
234+ }
235+ stackKey = strings.TrimSpace(line)
236+ for j := i + 1; j < len(lines); j++ {
237+ fileLine := strings.TrimSpace(lines[j])
238+ if fileLine == "" {
239+ continue
240+ }
241+ family = classifyCallbackFamily(fileLine + " " + stackKey)
242+ return family, stackKey
243+ }
244+ family = classifyCallbackFamily(stackKey)
245+ return family, stackKey
246+ }
247+ return family, stackKey
248+}
249+
250+func classifyCallbackFamily(s string) string {
251+ switch {
252+ case strings.Contains(s, "purego-cef2gtk"):
253+ return "purego-cef2gtk"
254+ case strings.Contains(s, "purego-cef"):
255+ return "purego-cef"
256+ case strings.Contains(s, "puregotk"):
257+ return "puregotk"
258+ case strings.Contains(s, "dumber"):
259+ return "dumber"
260+ case strings.Contains(s, "github.com/bnema/purego") || strings.Contains(s, "/purego/"):
261+ return "purego"
262+ default:
263+ return "unknown"
264+ }
265+}
266+
267+func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr, val reflect.Value, reused bool, errText string) {
268+ if !callbackLedgerEnabled() {
269+ return
270+ }
271+ stack := string(debug.Stack())
272+ family, stackKey := classifyCallbackStack(stack)
273+ cbs.ledgerSeq++
274+ occupied := cbs.numFn - len(cbs.holes)
275+ entry := callbackLedgerEvent{
276+ Marker: "PUREGO-CALLBACK-LEDGER",
277+ PID: os.Getpid(),
278+ Seq: cbs.ledgerSeq,
279+ Time: time.Now().Format(time.RFC3339Nano),
280+ Event: event,
281+ Index: idx,
282+ Addr: hexUintptr(addr),
283+ FnPtrKey: hexUintptr(fnPtrKey),
284+ Type: typeName(val),
285+ Family: family,
286+ StackKey: stackKey,
287+ NumFn: cbs.numFn,
288+ Occupied: occupied,
289+ Reusable: len(cbs.holes),
290+ KnownFnPtr: len(cbs.knownFnPtr),
291+ Remaining: maxCB - occupied,
292+ Max: maxCB,
293+ ReusedSlot: reused,
294+ Error: errText,
295+ }
296+ if os.Getenv("PUREGO_CALLBACK_LEDGER_STACK") != "0" {
297+ entry.Stack = stack
298+ }
299+ payload, err := json.Marshal(entry)
300+ if err != nil {
301+ return
302+ }
303+ payload = append(payload, '\n')
304+ if f, err := os.OpenFile(callbackLedgerPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
305+ _, _ = f.Write(payload)
306+ _ = f.Close()
307+ }
308+}
309+
310+func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
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())
312+ fmt.Fprint(os.Stderr, message)
313+ traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")
314+ if traceFile == "" {
315+ traceFile = fmt.Sprintf("/tmp/purego-callback-trace-%d.log", os.Getpid())
316+ }
317+ if f, err := os.OpenFile(traceFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
318+ _, _ = f.WriteString(message)
319+ _ = f.Close()
320+ }
145 }321 }
146 322
147 func compileCallback(fn any) uintptr {323 func compileCallback(fn any) uintptr {
@@ -189,21 +365,34 @@ output:
189 cbs.lock.Lock()365 cbs.lock.Lock()
190 defer cbs.lock.Unlock()366 defer cbs.lock.Unlock()
191 index := -1367 index := -1
368+ reused := false
192 for i := range cbs.holes {369 for i := range cbs.holes {
193 index = i370 index = i
194 delete(cbs.holes, i)371 delete(cbs.holes, i)
372+ reused = true
195 break373 break
196 }374 }
197 if index < 0 {375 if index < 0 {
198- if cbs.numFn >= maxCB {376+ remaining := maxCB - cbs.numFn
377+ if remaining <= 0 {
378+ recordCallbackLedger("exhausted", -1, 0, 0, val, false, "maximum callbacks reached")
379+ traceCallbackAllocation("exhausted", val, 0)
199 panic("purego: the maximum number of callbacks has been reached")380 panic("purego: the maximum number of callbacks has been reached")
200 }381 }
382+ if remaining <= 100 || remaining%250 == 0 {
383+ traceCallbackAllocation("allocated", val, remaining)
384+ }
201 index = cbs.numFn385 index = cbs.numFn
202 cbs.numFn++386 cbs.numFn++
203 }387 }
204 cbs.funcs[index] = val388 cbs.funcs[index] = val
205 addr := callbackasmAddr(index)389 addr := callbackasmAddr(index)
206 cbs.knownIdx[addr] = index390 cbs.knownIdx[addr] = index
391+ if reused {
392+ recordCallbackLedger("reuse", index, addr, 0, val, true, "")
393+ } else {
394+ recordCallbackLedger("alloc", index, addr, 0, val, false, "")
395+ }
207 return addr396 return addr
208 }397 }
209 398
@@ -350,7 +539,7 @@ func callbackWrap(a *callbackArgs) {
350 ptr = frame[pos]539 ptr = frame[pos]
351 }540 }
352 intsN += slots541 intsN += slots
353- args[i] = reflect.ValueOf(strings.GoString(ptr))542+ args[i] = reflect.ValueOf(internalstrings.GoString(ptr))
354 continue543 continue
355 case reflect.Struct:544 case reflect.Struct:
356 if i == 0 && inType.AssignableTo(reflect.TypeFor[CDecl]()) {545 if i == 0 && inType.AssignableTo(reflect.TypeFor[CDecl]()) {

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