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
14 changes: 3 additions & 11 deletions Sources/StateGraph/Observation/withGraphTrackingGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ public func withGraphTrackingGroup(
return
}

let _handlerBox = OSAllocatedUnfairLock<ClosureBox?>(
uncheckedState: .init(handler: handler)
let _handlerBox = OSAllocatedUnfairLock<ClosureBox<Void>?>(
uncheckedState: ClosureBox(handler)
)

// Create a cancellable for this scope that manages nested tracking
Expand All @@ -113,7 +113,7 @@ public func withGraphTrackingGroup(
// Nested groups/maps will register with this parent via addChild()
ThreadLocal.currentCancellable.withValue(scopeCancellable) {
_handlerBox.withLock {
$0?.handler()
$0?()
}
}
},
Expand All @@ -132,11 +132,3 @@ public func withGraphTrackingGroup(
}

}

private struct ClosureBox {
let handler: () -> Void

init(handler: @escaping () -> Void) {
self.handler = handler
}
}
20 changes: 6 additions & 14 deletions Sources/StateGraph/Observation/withGraphTrackingMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ public func withGraphTrackingMap<Projection>(

var filter = filter

let _handlerBox = OSAllocatedUnfairLock<ClosureBox?>(
uncheckedState: .init(handler: {
let _handlerBox = OSAllocatedUnfairLock<ClosureBox<Void>?>(
uncheckedState: ClosureBox({
let result = applier()
let filtered = filter.send(value: result)
if let filtered {
Expand All @@ -160,7 +160,7 @@ public func withGraphTrackingMap<Projection>(
// Set this scope's cancellable as the current parent for nested tracking
// Nested groups/maps will register with this parent via addChild()
ThreadLocal.currentCancellable.withValue(scopeCancellable) {
_handlerBox.withLock { $0?.handler() }
_handlerBox.withLock { $0?() }
}
},
didChange: {
Expand Down Expand Up @@ -305,8 +305,8 @@ public func withGraphTrackingMap<Dependency: AnyObject, Projection>(

var filter = filter

let _handlerBox = OSAllocatedUnfairLock<ClosureBox?>(
uncheckedState: .init(handler: {
let _handlerBox = OSAllocatedUnfairLock<ClosureBox<Void>?>(
uncheckedState: ClosureBox({
guard let dependency = weakDependency else {
return
}
Expand Down Expand Up @@ -335,7 +335,7 @@ public func withGraphTrackingMap<Dependency: AnyObject, Projection>(
// Set this scope's cancellable as the current parent for nested tracking
// Nested groups/maps will register with this parent via addChild()
ThreadLocal.currentCancellable.withValue(scopeCancellable) {
_handlerBox.withLock { $0?.handler() }
_handlerBox.withLock { $0?() }
}
},
didChange: {
Expand All @@ -353,11 +353,3 @@ public func withGraphTrackingMap<Dependency: AnyObject, Projection>(
subscriptions!.append(AnyCancellable(scopeCancellable))
}
}

private struct ClosureBox {
let handler: () -> Void

init(handler: @escaping () -> Void) {
self.handler = handler
}
}
95 changes: 67 additions & 28 deletions Sources/StateGraph/Observation/withTracking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,49 @@ public enum StateGraphTrackingContinuation: Sendable {
case next
}

// MARK: - Internal Types

struct UnsafeSendable<V>: ~Copyable, @unchecked Sendable {

let _value: V

init(_ value: consuming V) {
_value = value
}

}

/// A box that wraps a closure to prevent thunk stack growing during recursive calls.
/// By wrapping closures in this struct and passing the struct instead of the raw closure,
/// we avoid the overhead of repeatedly wrapping/unwrapping closure types.
struct ClosureBox<R> {
let closure: () -> R

init(_ closure: @escaping () -> R) {
self.closure = closure
}

func callAsFunction() -> R {
closure()
}
}

func perform<Return>(
_ closure: () -> Return,
isolation: isolated (any Actor)? = #isolation
) -> Return {
closure()
}

func perform<Return>(
_ box: ClosureBox<Return>,
isolation: isolated (any Actor)? = #isolation
) -> Return {
box()
}

// MARK: - Continuous Tracking

/// Tracks access to the properties of StoredNode or Computed.
/// Continuously tracks until `didChange` returns `.stop`.
/// It does not provides update of the properties granurarly. some frequency of updates may be aggregated into single event.
Expand All @@ -40,21 +83,37 @@ func withContinuousStateGraphTracking<R>(
didChange: @escaping () -> StateGraphTrackingContinuation,
isolation: isolated (any Actor)? = #isolation
) {
// Wrap closures in ClosureBox to prevent thunk stack growing during recursive calls.
// The boxes are created once here and passed through all recursive iterations.
let applyBox = ClosureBox(apply)
let didChangeBox = ClosureBox(didChange)
_withContinuousStateGraphTracking(
apply: applyBox,
didChange: didChangeBox,
isolation: isolation
)
}

let applyBox = UnsafeSendable(apply)
let didChangeBox = UnsafeSendable(didChange)

withStateGraphTracking(apply: apply) {
let continuation = perform(didChangeBox._value, isolation: isolation)
/// Private implementation that receives pre-wrapped closures to prevent thunk stack growing.
/// By passing ClosureBox<...> directly in recursive calls, we avoid
/// the cost of re-wrapping/unwrapping closure types on each iteration.
private func _withContinuousStateGraphTracking<R>(
apply: ClosureBox<R>,
didChange: ClosureBox<StateGraphTrackingContinuation>,
isolation: isolated (any Actor)? = #isolation
) {
withStateGraphTracking(apply: apply.closure) {
let continuation = perform(didChange, isolation: isolation)
switch continuation {
case .stop:
break
case .next:
// continue tracking on next event loop.
// It uses isolation and task dispatching to ensure apply closure is called on the same actor.
withContinuousStateGraphTracking(
apply: applyBox._value,
didChange: didChangeBox._value,
// Pass the already-wrapped closures directly to avoid thunk stack growing.
_withContinuousStateGraphTracking(
apply: apply,
didChange: didChange,
isolation: isolation
)
}
Expand Down Expand Up @@ -239,23 +298,3 @@ public final class TrackingRegistration: Sendable, Hashable {
}

}

struct UnsafeSendable<V>: ~Copyable, @unchecked Sendable {

let _value: V

init(_ value: consuming V) {
_value = value
}

}

func perform<Return>(
_ closure: () -> Return,
isolation: isolated (any Actor)? = #isolation
)
-> Return
{
closure()
}