Summary
UnfairLock (the Darwin path in BezierKit/Library/Lock.swift) backs os_unfair_lock with a manually heap-allocated pointer. We should switch to OSAllocatedUnfairLock, which holds the lock storage inline (no separate heap allocation) and removes the manual pointer lifecycle.
Current code
BezierKit/Library/Lock.swift:11-26:
#if canImport(Darwin)
internal final class UnfairLock {
private let lockPointer: UnsafeMutablePointer<os_unfair_lock>
init() {
lockPointer = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1) // heap allocation
lockPointer.initialize(to: os_unfair_lock())
}
deinit {
lockPointer.deallocate()
}
func sync<T>(_ f: () throws -> T) rethrows -> T {
os_unfair_lock_lock(lockPointer)
defer { os_unfair_lock_unlock(lockPointer) }
return try f()
}
}
#else
internal final class UnfairLock {
private let lock = NSLock()
...
}
#endif
The os_unfair_lock must not be copied (the lock is identified by its address), which is why it is pinned behind an UnsafeMutablePointer and explicitly allocate/deallocated. That's an extra heap allocation per lock plus manual lifetime management.
Proposal
Use OSAllocatedUnfairLock on platforms where it's available. It owns the lock storage internally (no caller-managed heap pointer), is Sendable, and is the Apple-recommended replacement for raw os_unfair_lock. It also offers a withLock { } API that maps directly onto our sync { }.
Availability: OSAllocatedUnfairLock requires iOS 16 / macOS 13 / tvOS 16 / watchOS 9. For older Apple OS versions, fall back to the existing raw-os_unfair_lock implementation; non-Darwin (Linux/WASM) keeps the NSLock path. Sketch:
#if canImport(Darwin)
import os
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
// OSAllocatedUnfairLock<Void>, withLock { f() }
} else {
// existing UnsafeMutablePointer<os_unfair_lock> path
}
#else
// NSLock path (unchanged)
#endif
(An @available-gated type/typealias is cleaner than a runtime branch on the hot path; the exact shape is an implementation detail.)
Verification
- Confirm in the release-build assembly that the lock no longer triggers a per-instance heap allocation.
- Confirm no regression in lock acquire/release cost (it should be equivalent —
OSAllocatedUnfairLock wraps the same primitive).
- Keep behavior identical on the pre-iOS-16 and non-Darwin fallbacks.
Summary
UnfairLock(the Darwin path inBezierKit/Library/Lock.swift) backsos_unfair_lockwith a manually heap-allocated pointer. We should switch toOSAllocatedUnfairLock, which holds the lock storage inline (no separate heap allocation) and removes the manual pointer lifecycle.Current code
BezierKit/Library/Lock.swift:11-26:The
os_unfair_lockmust not be copied (the lock is identified by its address), which is why it is pinned behind anUnsafeMutablePointerand explicitlyallocate/deallocated. That's an extra heap allocation per lock plus manual lifetime management.Proposal
Use
OSAllocatedUnfairLockon platforms where it's available. It owns the lock storage internally (no caller-managed heap pointer), isSendable, and is the Apple-recommended replacement for rawos_unfair_lock. It also offers awithLock { }API that maps directly onto oursync { }.Availability:
OSAllocatedUnfairLockrequires iOS 16 / macOS 13 / tvOS 16 / watchOS 9. For older Apple OS versions, fall back to the existing raw-os_unfair_lockimplementation; non-Darwin (Linux/WASM) keeps theNSLockpath. Sketch:(An
@available-gated type/typealias is cleaner than a runtime branch on the hot path; the exact shape is an implementation detail.)Verification
OSAllocatedUnfairLockwraps the same primitive).