Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion dispatch_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ var (
mkBlock = func(fn func()) uintptr {
return uintptr(objc.NewBlock(func(objc.Block) { fn() }))
}
// rmBlock drops OUR reference to a block once dispatch_async has taken its
// own. See [DispatchMain].
rmBlock = func(b uintptr) { objc.Block(b).Release() }
)

// loadDispatch resolves dispatch_async and the main queue from libSystem,
Expand All @@ -62,6 +65,24 @@ func loadDispatch() {
// methods must be messaged on the main thread). A nil fn is a no-op. If
// libdispatch cannot be resolved, fn runs inline on the calling goroutine rather
// than being dropped.
//
// ⛔⛔ AND THE BLOCK IS RELEASED. It was not, and every call leaked one for the
// life of the process — the block itself, and the entry purego keeps in its
// block table to hold the Go closure alive. Measured, one process posting
// blocks and draining them on the main thread:
//
// 3 576 blocks 14.2 MB resident
// 42 824 blocks 22.3 MB
// 161 782 blocks 44.4 MB
//
// which is about 200 bytes a call, never returned. An application that hops to
// the main thread once a frame reaches those numbers in minutes.
//
// ⭐ RELEASING HERE IS SAFE BECAUSE dispatch_async TAKES ITS OWN REFERENCE, and
// must: its contract is that the caller may free the block as soon as the call
// returns. So the count goes to two and back to one here, libdispatch drops the
// last one after running the block, and purego's dispose helper then removes
// the table entry — which is the only thing that ever removes one.
func DispatchMain(fn func()) {
if fn == nil {
return
Expand All @@ -71,5 +92,7 @@ func DispatchMain(fn func()) {
fn()
return
}
dispatchAsyncFn(dispatchMainQ, mkBlock(fn))
b := mkBlock(fn)
dispatchAsyncFn(dispatchMainQ, b)
rmBlock(b)
}
20 changes: 17 additions & 3 deletions objc_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,19 +268,24 @@ func TestOnDevice_DispatchMainScheduledHop(t *testing.T) {
if dispatchLoadErr != nil || dispatchMainQ == 0 {
t.Fatalf("real load failed: err=%v q=%#x", dispatchLoadErr, dispatchMainQ)
}
savedAsync, savedBlock := dispatchAsyncFn, mkBlock
defer func() { dispatchAsyncFn, mkBlock = savedAsync, savedBlock }()
savedAsync, savedBlock, savedRm := dispatchAsyncFn, mkBlock, rmBlock
defer func() { dispatchAsyncFn, mkBlock, rmBlock = savedAsync, savedBlock, savedRm }()

const sentinel uintptr = 0xB10C
var captured func()
var gotQ, gotBlock uintptr
var gotQ, gotBlock, freed uintptr
mkBlock = func(fn func()) uintptr { captured = fn; return sentinel }
dispatchAsyncFn = func(q, block uintptr) {
gotQ, gotBlock = q, block
if block == sentinel && captured != nil {
captured()
}
}
// ⛔ AND rmBlock IS FAKED TOO, or the sentinel goes to _Block_release: this
// test crashed with "fault 0xb114" -- 0xB10C plus the offset a release reads
// -- the moment the real one was left in place. A seam that is only half
// replaced is a seam that runs half the real thing.
rmBlock = func(b uintptr) { freed = b }

ran := false
DispatchMain(func() { ran = true })
Expand All @@ -293,6 +298,15 @@ func TestOnDevice_DispatchMainScheduledHop(t *testing.T) {
if !ran {
t.Fatal("DispatchMain did not run the scheduled fn")
}
// ⛔⛔ AND THE BLOCK IS GIVEN BACK. Without this, every call leaked a block
// and the purego table entry holding its closure: measured at about 200
// bytes a call, 3 576 blocks costing 14.2 MB resident against 44.4 MB for
// 161 782. dispatch_async takes its own reference, so releasing ours here is
// what its contract is for.
if freed != sentinel {
t.Errorf("the block was released as %#x, want %#x: DispatchMain is "+
"leaking one block and one closure per call", freed, sentinel)
}
t.Log("on-device: DispatchMain scheduled fn onto the main queue via dispatch_async")
}

Expand Down