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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +196 -7
M syscall_unix.go +196 -7
| @@ -6,14 +6,20 @@ | |||
| 6 | 6 | package purego | |
| 7 | 7 | ||
| 8 | 8 | import ( | |
| 9 | + | "encoding/json" | |
| 9 | 10 | "errors" | |
| 11 | + | "fmt" | |
| 10 | 12 | "math" | |
| 13 | + | "os" | |
| 11 | 14 | "reflect" | |
| 12 | 15 | "runtime" | |
| 16 | + | "runtime/debug" | |
| 17 | + | "strings" | |
| 13 | 18 | "sync" | |
| 19 | + | "time" | |
| 14 | 20 | "unsafe" | |
| 15 | 21 | ||
| 16 | - | "github.com/bnema/purego/internal/strings" | |
| 22 | + | internalstrings "github.com/bnema/purego/internal/strings" | |
| 17 | 23 | ) | |
| 18 | 24 | ||
| 19 | 25 | var syscallXABI0 uintptr | |
| @@ -60,7 +66,10 @@ func NewCallbackFnPtr(fnPtr any) uintptr { | |||
| 60 | 66 | if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func { | |
| 61 | 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 | 73 | return addr | |
| 65 | 74 | } | |
| 66 | 75 | addr := compileCallback(val.Elem().Interface()) | |
| @@ -68,6 +77,7 @@ func NewCallbackFnPtr(fnPtr any) uintptr { | |||
| 68 | 77 | cbs.knownFnPtr[val.Pointer()] = addr | |
| 69 | 78 | if idx, ok := cbs.knownIdx[addr]; ok { | |
| 70 | 79 | cbs.fnPtrKeys[idx] = val.Pointer() | |
| 80 | + | recordCallbackLedger("fnptr-bind", idx, addr, val.Pointer(), val.Elem(), false, "") | |
| 71 | 81 | } | |
| 72 | 82 | cbs.lock.Unlock() | |
| 73 | 83 | return addr | |
| @@ -79,15 +89,19 @@ func UnrefCallback(cb uintptr) error { | |||
| 79 | 89 | defer cbs.lock.Unlock() | |
| 80 | 90 | idx, ok := cbs.knownIdx[cb] | |
| 81 | 91 | if !ok { | |
| 92 | + | recordCallbackLedger("release-miss", -1, cb, 0, reflect.Value{}, false, "callback not found") | |
| 82 | 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 | 98 | delete(cbs.knownFnPtr, key) | |
| 86 | 99 | cbs.fnPtrKeys[idx] = 0 | |
| 87 | 100 | } | |
| 88 | 101 | delete(cbs.knownIdx, cb) | |
| 89 | 102 | cbs.holes[idx] = struct{}{} | |
| 90 | 103 | cbs.funcs[idx] = reflect.Value{} | |
| 104 | + | recordCallbackLedger("release", idx, cb, key, val, false, "") | |
| 91 | 105 | return nil | |
| 92 | 106 | } | |
| 93 | 107 | ||
| @@ -104,18 +118,22 @@ func UnrefCallbackFnPtr(fnPtr any) error { | |||
| 104 | 118 | defer cbs.lock.Unlock() | |
| 105 | 119 | addr, ok := cbs.knownFnPtr[val.Pointer()] | |
| 106 | 120 | if !ok { | |
| 121 | + | recordCallbackLedger("release-fnptr-miss", -1, 0, val.Pointer(), val.Elem(), false, "callback not found") | |
| 107 | 122 | return errors.New("callback not found") | |
| 108 | 123 | } | |
| 109 | 124 | idx, ok := cbs.knownIdx[addr] | |
| 110 | 125 | if !ok { | |
| 111 | 126 | delete(cbs.knownFnPtr, val.Pointer()) | |
| 127 | + | recordCallbackLedger("release-fnptr-miss", -1, addr, val.Pointer(), val.Elem(), false, "callback not found") | |
| 112 | 128 | return errors.New("callback not found") | |
| 113 | 129 | } | |
| 130 | + | callbackVal := cbs.funcs[idx] | |
| 114 | 131 | delete(cbs.knownFnPtr, val.Pointer()) | |
| 115 | 132 | delete(cbs.knownIdx, addr) | |
| 116 | 133 | cbs.fnPtrKeys[idx] = 0 | |
| 117 | 134 | cbs.holes[idx] = struct{}{} | |
| 118 | 135 | cbs.funcs[idx] = reflect.Value{} | |
| 136 | + | recordCallbackLedger("release-fnptr", idx, addr, val.Pointer(), callbackVal, false, "") | |
| 119 | 137 | return nil | |
| 120 | 138 | } | |
| 121 | 139 | ||
| @@ -131,17 +149,175 @@ var cbs = struct { | |||
| 131 | 149 | knownIdx map[uintptr]int // callback address -> slot index | |
| 132 | 150 | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address | |
| 133 | 151 | fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address | |
| 152 | + | ledgerSeq uint64 // monotonic callback ledger event sequence | |
| 134 | 153 | }{ | |
| 135 | 154 | holes: make(map[int]struct{}), | |
| 136 | 155 | knownIdx: make(map[uintptr]int, maxCB), | |
| 137 | 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 | 160 | cbs.lock.RLock() | |
| 142 | 161 | defer cbs.lock.RUnlock() | |
| 143 | 162 | 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 | + | } | |
| 145 | 321 | } | |
| 146 | 322 | ||
| 147 | 323 | func compileCallback(fn any) uintptr { | |
| @@ -189,21 +365,34 @@ output: | |||
| 189 | 365 | cbs.lock.Lock() | |
| 190 | 366 | defer cbs.lock.Unlock() | |
| 191 | 367 | index := -1 | |
| 368 | + | reused := false | |
| 192 | 369 | for i := range cbs.holes { | |
| 193 | 370 | index = i | |
| 194 | 371 | delete(cbs.holes, i) | |
| 372 | + | reused = true | |
| 195 | 373 | break | |
| 196 | 374 | } | |
| 197 | 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 | 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 | 385 | index = cbs.numFn | |
| 202 | 386 | cbs.numFn++ | |
| 203 | 387 | } | |
| 204 | 388 | cbs.funcs[index] = val | |
| 205 | 389 | addr := callbackasmAddr(index) | |
| 206 | 390 | 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 | 396 | return addr | |
| 208 | 397 | } | |
| 209 | 398 | ||
| @@ -350,7 +539,7 @@ func callbackWrap(a *callbackArgs) { | |||
| 350 | 539 | ptr = frame[pos] | |
| 351 | 540 | } | |
| 352 | 541 | intsN += slots | |
| 353 | - | args[i] = reflect.ValueOf(strings.GoString(ptr)) | |
| 542 | + | args[i] = reflect.ValueOf(internalstrings.GoString(ptr)) | |
| 354 | 543 | continue | |
| 355 | 544 | case reflect.Struct: | |
| 356 | 545 | if i == 0 && inType.AssignableTo(reflect.TypeFor[CDecl]()) { | |
| @@ -6,14 +6,20 @@ | |||||
| 6 | package purego | 6 | 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 uintptr | 25 | 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 addr | 73 | 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()] = addr | 77 | 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 addr | 83 | 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] = 0 | 99 | 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 nil | 105 | 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] = 0 | 133 | 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 nil | 137 | return nil | ||
| 120 | } | 138 | } | ||
| 121 | 139 | ||||
| @@ -131,17 +149,175 @@ var cbs = struct { | |||||
| 131 | knownIdx map[uintptr]int // callback address -> slot index | 149 | knownIdx map[uintptr]int // callback address -> slot index | ||
| 132 | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address | 150 | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address | ||
| 133 | fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address | 151 | 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, 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 | + | } | |||
| 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 := -1 | 367 | index := -1 | ||
| 368 | + | reused := false | |||
| 192 | for i := range cbs.holes { | 369 | for i := range cbs.holes { | ||
| 193 | index = i | 370 | index = i | ||
| 194 | delete(cbs.holes, i) | 371 | delete(cbs.holes, i) | ||
| 372 | + | reused = true | |||
| 195 | break | 373 | 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.numFn | 385 | index = cbs.numFn | ||
| 202 | cbs.numFn++ | 386 | cbs.numFn++ | ||
| 203 | } | 387 | } | ||
| 204 | cbs.funcs[index] = val | 388 | cbs.funcs[index] = val | ||
| 205 | addr := callbackasmAddr(index) | 389 | addr := callbackasmAddr(index) | ||
| 206 | cbs.knownIdx[addr] = index | 390 | 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 addr | 396 | 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 += slots | 541 | intsN += slots | ||
| 353 | - | args[i] = reflect.ValueOf(strings.GoString(ptr)) | 542 | + | args[i] = reflect.ValueOf(internalstrings.GoString(ptr)) |
| 354 | continue | 543 | 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]()) { | ||