From 90a6a26ea05c38b49f59aa2a0e09f4a805fbf7bb Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 30 Aug 2025 21:47:42 +0800 Subject: [PATCH 01/12] Add OpenObservation dependency --- Package.resolved | 20 +++++++++++++++++++- Package.swift | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 20ca07d37..8fc575df7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "f229c995beae82593994793abdd0fb2239bd44bded0c9672a73e95e1a2baecf5", + "originHash" : "2432e3fbb351913595eab925a316b1813a5e4b112660f54286c5b94f893e6fd1", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -28,6 +28,15 @@ "revision" : "1a1b9ad092ed373a3c94df7b589e3e099da93699" } }, + { + "identity" : "openobservation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenSwiftUIProject/OpenObservation", + "state" : { + "branch" : "main", + "revision" : "07e0d9c39c616dff07598ad7c1c1221524f734b9" + } + }, { "identity" : "openrenderbox", "kind" : "remoteSourceControl", @@ -46,6 +55,15 @@ "version" : "1.0.3" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, { "identity" : "symbollocator", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 59b4a24d3..fb48c53e3 100644 --- a/Package.swift +++ b/Package.swift @@ -224,6 +224,7 @@ let openSwiftUICoreTarget = Target.target( .product(name: "OpenQuartzCoreShims", package: "OpenCoreGraphics"), .product(name: "OpenAttributeGraphShims", package: "OpenAttributeGraph"), .product(name: "OpenRenderBoxShims", package: "OpenRenderBox"), + .product(name: "OpenObservation", package: "OpenObservation"), ] + (swiftUIRenderCondition && symbolLocatorCondition ? ["OpenSwiftUISymbolDualTestsSupport"] : []), cSettings: sharedCSettings, cxxSettings: sharedCxxSettings, @@ -542,6 +543,7 @@ if useLocalDeps { .package(path: "../OpenCoreGraphics"), .package(path: "../OpenAttributeGraph"), .package(path: "../OpenRenderBox"), + .package(path: "../OpenObservation"), ] if attributeGraphCondition || renderBoxCondition || linkCoreUI { dependencies.append(.package(path: "../DarwinPrivateFrameworks")) @@ -553,6 +555,7 @@ if useLocalDeps { // FIXME: on Linux platform: OG contains unsafe build flags which prevents us using version dependency .package(url: "https://github.com/OpenSwiftUIProject/OpenAttributeGraph", branch: "main"), .package(url: "https://github.com/OpenSwiftUIProject/OpenRenderBox", branch: "main"), + .package(url: "https://github.com/OpenSwiftUIProject/OpenObservation", branch: "main"), ] if attributeGraphCondition || renderBoxCondition || linkCoreUI { dependencies.append(.package(url: "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", branch: "main")) From 793078cefe6b99f2811f02b91613596867d18788 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 30 Aug 2025 22:42:08 +0800 Subject: [PATCH 02/12] Add ObservationUtil API --- .../Data/Observation/ObservationUtil.swift | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift diff --git a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift new file mode 100644 index 000000000..128715fea --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift @@ -0,0 +1,67 @@ +// +// ObservationUtil.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 7DF024579E4FC31D4E92A33BBA0366D6 (SwiftUI?) + +package import OpenAttributeGraphShims +@_spi(OpenSwiftUI) +package import OpenObservation + +private struct ObservationEntry { + let context: AnyObject + var properties: Set +} + +private struct ObservationGraphMutation { + var invalidatingMutation: InvalidatingGraphMutation + var observationTracking: [ObservationTracking] + var subgraphObservers: [(Subgraph, Int)] +} + +extension ObservationRegistrar { + package static var latestTriggers: [AnyKeyPath] = [] + + private var invalidation: ThreadSpecific<[AnyWeakAttribute: (mutation: ObservationGraphMutation, accessList: ObservationTracking._AccessList)]> { + _openSwiftUIUnimplementedFailure() + } +} + +@inline(__always) +package func _withObservation( + do work: () throws -> T +) rethrows -> (value: T, accessList: ObservationTracking._AccessList?) { + _openSwiftUIUnimplementedFailure() +} + +@inline(__always) +package func _installObservation( + accessList: ObservationTracking._AccessList?, + attribute: Attribute +) { + _openSwiftUIUnimplementedFailure() +} + +extension Rule { + @inline(__always) + package func withObservation(do work: () throws -> T) rethrows -> T { + _openSwiftUIUnimplementedFailure() + } + + package var observationInstaller: (ObservationTracking._AccessList) -> Void { + _openSwiftUIUnimplementedFailure() + } +} + +extension StatefulRule { + @inline(__always) + package func withObservation(do work: () throws -> T) rethrows -> T { + _openSwiftUIUnimplementedFailure() + } + + package var observationInstaller: (ObservationTracking._AccessList) -> Void { + _openSwiftUIUnimplementedFailure() + } +} From 83c2ad0c4a5fd42ffbcb8412cce31a7ebb96d60a Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 31 Aug 2025 00:17:40 +0800 Subject: [PATCH 03/12] Add _ThreadLocal API --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 8fc575df7..e62e54da7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -34,7 +34,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenObservation", "state" : { "branch" : "main", - "revision" : "07e0d9c39c616dff07598ad7c1c1221524f734b9" + "revision" : "1eb29e56b75f86cf7ee1593e315bafc1ab07be3d" } }, { From 739e5ef8ac340333cb60b12a90dca84851eee406 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 31 Aug 2025 00:19:51 +0800 Subject: [PATCH 04/12] Add Subgraph.removeObserver API --- .../{OpenGraph/Attribute => Util}/AnyAttributeFix.swift | 0 .../{OpenGraph => Util}/AnyAttributeFix.swift | 0 .../AttributeGraphAdditions.swift} | 9 ++++++++- 3 files changed, 8 insertions(+), 1 deletion(-) rename Sources/OpenSwiftUI/{OpenGraph/Attribute => Util}/AnyAttributeFix.swift (100%) rename Sources/OpenSwiftUICore/{OpenGraph => Util}/AnyAttributeFix.swift (100%) rename Sources/OpenSwiftUICore/{OpenGraph/OpenGraphAdditions.swift => Util/AttributeGraphAdditions.swift} (97%) diff --git a/Sources/OpenSwiftUI/OpenGraph/Attribute/AnyAttributeFix.swift b/Sources/OpenSwiftUI/Util/AnyAttributeFix.swift similarity index 100% rename from Sources/OpenSwiftUI/OpenGraph/Attribute/AnyAttributeFix.swift rename to Sources/OpenSwiftUI/Util/AnyAttributeFix.swift diff --git a/Sources/OpenSwiftUICore/OpenGraph/AnyAttributeFix.swift b/Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift similarity index 100% rename from Sources/OpenSwiftUICore/OpenGraph/AnyAttributeFix.swift rename to Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift diff --git a/Sources/OpenSwiftUICore/OpenGraph/OpenGraphAdditions.swift b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift similarity index 97% rename from Sources/OpenSwiftUICore/OpenGraph/OpenGraphAdditions.swift rename to Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift index aff0ba357..902ebf78b 100644 --- a/Sources/OpenSwiftUICore/OpenGraph/OpenGraphAdditions.swift +++ b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift @@ -1,5 +1,5 @@ // -// OpenAttributeGraphAdditions.swift +// AttributeGraphAdditions.swift // OpenSwiftUICore // // Status: WIP @@ -7,6 +7,13 @@ package import OpenAttributeGraphShims +extension Subgraph { + package func removeObserver(_ id: Int) { + // TODO + _openSwiftUIUnimplementedFailure() + } +} + // MARK: - Defaultable [6.5.4] package protocol Defaultable { From e9c0d994bd4bb94ec956a388a848fcfac8e6b304 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 31 Aug 2025 01:35:17 +0800 Subject: [PATCH 05/12] Update ObservationGraphMutation --- .../Data/Observation/ObservationUtil.swift | 190 +++++++++++++++++- 1 file changed, 179 insertions(+), 11 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift index 128715fea..112a300b0 100644 --- a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift +++ b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift @@ -10,58 +10,226 @@ package import OpenAttributeGraphShims @_spi(OpenSwiftUI) package import OpenObservation +// MARK: - ObservationEntry + private struct ObservationEntry { let context: AnyObject var properties: Set } -private struct ObservationGraphMutation { +// MARK: - ObservationGraphMutation + +private struct ObservationGraphMutation: GraphMutation { var invalidatingMutation: InvalidatingGraphMutation var observationTracking: [ObservationTracking] var subgraphObservers: [(Subgraph, Int)] + + func apply() { + for (subgraph, observerID) in subgraphObservers { + subgraph.removeObserver(observerID) + } + ObservationRegistrar.latestTriggers.removeAll(keepingCapacity: true) + + for tracking in observationTracking { + if let changedKeyPath = tracking.changed { + ObservationRegistrar.latestTriggers.append(changedKeyPath) + } + tracking.cancel() + } + invalidatingMutation.apply() + ObservationRegistrar.invalidations.value.removeValue(forKey: invalidatingMutation.attribute) + } + + mutating func combine(with other: T) -> Bool where T: GraphMutation { + guard invalidatingMutation.combine(with: other) else { + return false + } + if let otherObservation = other as? ObservationGraphMutation { + observationTracking.append(contentsOf: otherObservation.observationTracking) + subgraphObservers.append(contentsOf: otherObservation.subgraphObservers) + } + return true + } + + func cancel() { + for tracking in observationTracking { + tracking.cancel() + } + for (subgraph, observerID) in subgraphObservers { + subgraph.removeObserver(observerID) + } + } } +// MARK: - ObservationRegistrar + Extension + extension ObservationRegistrar { package static var latestTriggers: [AnyKeyPath] = [] - private var invalidation: ThreadSpecific<[AnyWeakAttribute: (mutation: ObservationGraphMutation, accessList: ObservationTracking._AccessList)]> { - _openSwiftUIUnimplementedFailure() - } + package static var latestAccessLists: [ObservationTracking._AccessList] = [] + + fileprivate static var invalidations: ThreadSpecific<[AnyWeakAttribute: (mutation: ObservationGraphMutation, accessList: ObservationTracking._AccessList)]> = .init([:]) } +// MARK: - Observation Utilities [WIP] + @inline(__always) package func _withObservation( do work: () throws -> T ) rethrows -> (value: T, accessList: ObservationTracking._AccessList?) { - _openSwiftUIUnimplementedFailure() + var accessList: ObservationTracking._AccessList? + let result = try withUnsafeMutablePointer(to: &accessList) { ptr in + let previous = _ThreadLocal.value + _ThreadLocal.value = UnsafeMutableRawPointer(ptr) + defer { _ThreadLocal.value = previous } + return try work() + } + if let accessList { + ObservationRegistrar.latestAccessLists.append(accessList) + } + return (result, accessList) +} + +@inline(__always) +package func _withObservation( + attribute: Attribute, + do work: () throws -> T +) rethrows -> T { + let previousAccessLists = ObservationRegistrar.latestAccessLists + ObservationRegistrar.latestAccessLists = [] + defer { ObservationRegistrar.latestAccessLists = previousAccessLists } + + var accessList: ObservationTracking._AccessList? + let result = try withUnsafeMutablePointer(to: &accessList) { ptr in + let previous = _ThreadLocal.value + _ThreadLocal.value = UnsafeMutableRawPointer(ptr) + defer { _ThreadLocal.value = previous } + return try work() + } + + // TODO + // Install observation if we collected any accesses + if let accessList { + _installObservation(accessList: accessList, attribute: attribute) + } + + // Merge any access lists collected from nested observations + // Since merge is internal, we'll just append to our tracking list + if !ObservationRegistrar.latestAccessLists.isEmpty { + if accessList == nil { + // If we don't have an access list yet, use the first nested one + accessList = ObservationRegistrar.latestAccessLists.first + } + // Additional nested access lists will be handled by the observation system + } + + return result } @inline(__always) package func _installObservation( - accessList: ObservationTracking._AccessList?, + accessLists: [ObservationTracking._AccessList], attribute: Attribute ) { - _openSwiftUIUnimplementedFailure() + guard !accessLists.isEmpty else { return } + for accessList in accessLists { + installObservationSlow(accessList: accessList, attribute: attribute) + } } +private func installObservationSlow( + accessList: ObservationTracking._AccessList, + attribute: Attribute +) { + // TODO +} + +// MARK: - Rule + Observation [WIP] + extension Rule { @inline(__always) package func withObservation(do work: () throws -> T) rethrows -> T { - _openSwiftUIUnimplementedFailure() + // For rules, we track observation access and install it on the rule's attribute + var accessList: ObservationTracking._AccessList? + let result = try withUnsafeMutablePointer(to: &accessList) { ptr in + let previous = _ThreadLocal.value + _ThreadLocal.value = UnsafeMutableRawPointer(ptr) + defer { + _ThreadLocal.value = previous + } + return try work() + } + + // Install observation tracking if we collected any accesses + if let accessList { + // Get the rule's associated attribute and install observation + // This would typically be done through the rule's context + // For now, save the access list for later installation + ObservationRegistrar.latestAccessLists.append(accessList) + } + + return result } package var observationInstaller: (ObservationTracking._AccessList) -> Void { - _openSwiftUIUnimplementedFailure() + return { accessList in + // This closure will be called to install observation tracking + // on the rule's attribute when needed + let tracking = ObservationTracking(accessList) + ObservationTracking._installTracking( + tracking, + willSet: { _ in + // Trigger rule re-evaluation + }, + didSet: { _ in + // Trigger rule re-evaluation + } + ) + } } } +// MARK: - StatefulRule + Observation [WIP] + extension StatefulRule { @inline(__always) package func withObservation(do work: () throws -> T) rethrows -> T { - _openSwiftUIUnimplementedFailure() + // For stateful rules, we track observation access and install it on the rule's attribute + var accessList: ObservationTracking._AccessList? + let result = try withUnsafeMutablePointer(to: &accessList) { ptr in + let previous = _ThreadLocal.value + _ThreadLocal.value = UnsafeMutableRawPointer(ptr) + defer { + _ThreadLocal.value = previous + } + return try work() + } + + // Install observation tracking if we collected any accesses + if let accessList { + // Get the rule's associated attribute and install observation + // This would typically be done through the rule's context + // For now, save the access list for later installation + ObservationRegistrar.latestAccessLists.append(accessList) + } + + return result } package var observationInstaller: (ObservationTracking._AccessList) -> Void { - _openSwiftUIUnimplementedFailure() + return { accessList in + // This closure will be called to install observation tracking + // on the rule's attribute when needed + let tracking = ObservationTracking(accessList) + ObservationTracking._installTracking( + tracking, + willSet: { _ in + // Trigger rule re-evaluation + }, + didSet: { _ in + // Trigger rule re-evaluation + } + ) + } } } From 14af4c331427704857b93c962cd5a961c7fda615 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 31 Aug 2025 01:34:13 +0800 Subject: [PATCH 06/12] Update withObservation --- .../Data/Observation/ObservationUtil.swift | 112 ++++-------------- 1 file changed, 21 insertions(+), 91 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift index 112a300b0..ada685677 100644 --- a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift +++ b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift @@ -99,30 +99,10 @@ package func _withObservation( ObservationRegistrar.latestAccessLists = [] defer { ObservationRegistrar.latestAccessLists = previousAccessLists } - var accessList: ObservationTracking._AccessList? - let result = try withUnsafeMutablePointer(to: &accessList) { ptr in - let previous = _ThreadLocal.value - _ThreadLocal.value = UnsafeMutableRawPointer(ptr) - defer { _ThreadLocal.value = previous } - return try work() - } - - // TODO - // Install observation if we collected any accesses - if let accessList { - _installObservation(accessList: accessList, attribute: attribute) - } - - // Merge any access lists collected from nested observations - // Since merge is internal, we'll just append to our tracking list - if !ObservationRegistrar.latestAccessLists.isEmpty { - if accessList == nil { - // If we don't have an access list yet, use the first nested one - accessList = ObservationRegistrar.latestAccessLists.first - } - // Additional nested access lists will be handled by the observation system + let (result, _) = try _withObservation(do: work) + for accessList in ObservationRegistrar.latestAccessLists { + installObservationSlow(accessList: accessList, attribute: attribute) } - return result } @@ -144,92 +124,42 @@ private func installObservationSlow( // TODO } -// MARK: - Rule + Observation [WIP] +// MARK: - Rule + Observation extension Rule { @inline(__always) package func withObservation(do work: () throws -> T) rethrows -> T { - // For rules, we track observation access and install it on the rule's attribute - var accessList: ObservationTracking._AccessList? - let result = try withUnsafeMutablePointer(to: &accessList) { ptr in - let previous = _ThreadLocal.value - _ThreadLocal.value = UnsafeMutableRawPointer(ptr) - defer { - _ThreadLocal.value = previous - } - return try work() - } - - // Install observation tracking if we collected any accesses - if let accessList { - // Get the rule's associated attribute and install observation - // This would typically be done through the rule's context - // For now, save the access list for later installation - ObservationRegistrar.latestAccessLists.append(accessList) - } - - return result + try _withObservation(attribute: attribute, do: work) } package var observationInstaller: (ObservationTracking._AccessList) -> Void { - return { accessList in - // This closure will be called to install observation tracking - // on the rule's attribute when needed - let tracking = ObservationTracking(accessList) - ObservationTracking._installTracking( - tracking, - willSet: { _ in - // Trigger rule re-evaluation - }, - didSet: { _ in - // Trigger rule re-evaluation - } - ) + { [attribute] accessList in + guard attribute.subgraph.isValid else { + return + } + attribute.subgraph.apply { + installObservationSlow(accessList: accessList, attribute: attribute) + } } } } -// MARK: - StatefulRule + Observation [WIP] +// MARK: - StatefulRule + Observation extension StatefulRule { @inline(__always) package func withObservation(do work: () throws -> T) rethrows -> T { - // For stateful rules, we track observation access and install it on the rule's attribute - var accessList: ObservationTracking._AccessList? - let result = try withUnsafeMutablePointer(to: &accessList) { ptr in - let previous = _ThreadLocal.value - _ThreadLocal.value = UnsafeMutableRawPointer(ptr) - defer { - _ThreadLocal.value = previous - } - return try work() - } - - // Install observation tracking if we collected any accesses - if let accessList { - // Get the rule's associated attribute and install observation - // This would typically be done through the rule's context - // For now, save the access list for later installation - ObservationRegistrar.latestAccessLists.append(accessList) - } - - return result + try _withObservation(attribute: attribute, do: work) } package var observationInstaller: (ObservationTracking._AccessList) -> Void { - return { accessList in - // This closure will be called to install observation tracking - // on the rule's attribute when needed - let tracking = ObservationTracking(accessList) - ObservationTracking._installTracking( - tracking, - willSet: { _ in - // Trigger rule re-evaluation - }, - didSet: { _ in - // Trigger rule re-evaluation - } - ) + { [attribute] accessList in + guard attribute.subgraph.isValid else { + return + } + attribute.subgraph.apply { + installObservationSlow(accessList: accessList, attribute: attribute) + } } } } From 981db137e55254f025666cd8e15d54ebf5d65398 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 31 Aug 2025 13:42:03 +0800 Subject: [PATCH 07/12] Complete Observation implementation --- .../Data/Observation/ObservationUtil.swift | 70 ++++++++++++++++++- .../Util/AttributeGraphAdditions.swift | 10 ++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift index ada685677..4d3bfd444 100644 --- a/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift +++ b/Sources/OpenSwiftUICore/Data/Observation/ObservationUtil.swift @@ -3,9 +3,10 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Blocked by TraceEvent // ID: 7DF024579E4FC31D4E92A33BBA0366D6 (SwiftUI?) +import Foundation package import OpenAttributeGraphShims @_spi(OpenSwiftUI) package import OpenObservation @@ -15,6 +16,24 @@ package import OpenObservation private struct ObservationEntry { let context: AnyObject var properties: Set + + func union(_ entry: ObservationEntry) -> ObservationEntry { + ObservationEntry(context: context, properties: properties.union(entry.properties)) + } +} + +extension ObservationTracking._AccessList { + mutating func merge(_ other: ObservationTracking._AccessList) { + withUnsafeMutablePointer(to: &self) { ptr1 in + withUnsafePointer(to: other) { ptr2 in + let selfPtr = UnsafeMutableRawPointer(mutating: ptr1) + .assumingMemoryBound(to: [ObjectIdentifier: ObservationEntry].self) + let otherPtr = UnsafeRawPointer(ptr2) + .assumingMemoryBound(to: [ObjectIdentifier: ObservationEntry].self) + selfPtr.pointee.merge(otherPtr.pointee) { $0.union($1) } + } + } + } } // MARK: - ObservationGraphMutation @@ -71,7 +90,7 @@ extension ObservationRegistrar { fileprivate static var invalidations: ThreadSpecific<[AnyWeakAttribute: (mutation: ObservationGraphMutation, accessList: ObservationTracking._AccessList)]> = .init([:]) } -// MARK: - Observation Utilities [WIP] +// MARK: - Observation Utilities @inline(__always) package func _withObservation( @@ -121,7 +140,52 @@ private func installObservationSlow( accessList: ObservationTracking._AccessList, attribute: Attribute ) { - // TODO + guard let subgraph = attribute.identifier.subgraph2 else { + return + } + let weakViewGraph = WeakUncheckedSendable(ViewGraph.current) + let weakAttribute = AnyWeakAttribute(attribute.identifier) + + var newAccessList = accessList + let removedValue = ObservationRegistrar.invalidations.value.removeValue(forKey: weakAttribute) + if let removedValue { + newAccessList.merge(removedValue.accessList) + removedValue.mutation.cancel() + } + + let tracking = ObservationTracking(newAccessList) + let observerID = subgraph.addObserver { + let removedValue = ObservationRegistrar.invalidations.value.removeValue(forKey: weakAttribute) + if let removedValue { + removedValue.mutation.cancel() + } + } + let mutation = ObservationGraphMutation( + invalidatingMutation: InvalidatingGraphMutation(attribute: weakAttribute), + observationTracking: [tracking], + subgraphObservers: [(subgraph, observerID)] + ) + ObservationRegistrar.invalidations.value[weakAttribute] = (mutation: mutation, accessList: newAccessList) + ObservationTracking._installTracking( + tracking, + willSet: { tracking in + guard subgraph.isValid else { return } + Update.ensure { + guard let attribute = weakAttribute.attribute, + let viewGraph = weakViewGraph.value else { + mutation.cancel() + return + } + // TODO: transaction result + let _ = viewGraph.asyncTransaction( + Transaction.current, + mutation: mutation, + style: Thread.isMainThread ? .immediate : .deferred, + ) + // TODO: AGGraphAddTraceEvent + } + } + ) } // MARK: - Rule + Observation diff --git a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift index 902ebf78b..b2c0f9592 100644 --- a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift +++ b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift @@ -10,7 +10,15 @@ package import OpenAttributeGraphShims extension Subgraph { package func removeObserver(_ id: Int) { // TODO - _openSwiftUIUnimplementedFailure() + _openSwiftUIUnimplementedWarning() + } +} + +extension AnyAttribute { + package var subgraph2: Subgraph? { + // TODO + _openSwiftUIUnimplementedWarning() + return subgraph } } From 536600bf8e48b01c5e3d921e291540c21d4f7049 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 31 Aug 2025 20:11:22 +0800 Subject: [PATCH 08/12] Update OpenAttributeGraph dependency https://github.com/OpenSwiftUIProject/OpenAttributeGraph/pull/176 --- Package.resolved | 4 ++-- .../Util/AttributeGraphAdditions.swift | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/Package.resolved b/Package.resolved index e62e54da7..0f466057b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "4aa30d65aae91b8cd3aa7e6b5910d281a77e9af0" + "revision" : "fdb349e4715fafdb847852e03c414739b21ae8b8" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenAttributeGraph", "state" : { "branch" : "main", - "revision" : "cc5a58ccf7720bf096fc487c2a518ae1c5139c4d" + "revision" : "7e1664cd4fcd2f019be59f2969619eb022bdcec2" } }, { diff --git a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift index b2c0f9592..2d3677c4f 100644 --- a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift +++ b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift @@ -7,21 +7,6 @@ package import OpenAttributeGraphShims -extension Subgraph { - package func removeObserver(_ id: Int) { - // TODO - _openSwiftUIUnimplementedWarning() - } -} - -extension AnyAttribute { - package var subgraph2: Subgraph? { - // TODO - _openSwiftUIUnimplementedWarning() - return subgraph - } -} - // MARK: - Defaultable [6.5.4] package protocol Defaultable { From 0debb63eb75a5210f484493edd188ab5308681f0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Sep 2025 01:22:50 +0800 Subject: [PATCH 09/12] Update OpenAttributeGraph dependency to fix link issue See https://github.com/OpenSwiftUIProject/OpenAttributeGraph/pull/179 --- Package.resolved | 4 ++-- Sources/OpenSwiftUICore/Data/State/State.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 0f466057b..cb37a0743 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "fdb349e4715fafdb847852e03c414739b21ae8b8" + "revision" : "00977e88d14f98c248bf1c8370855ad6ecc35977" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenAttributeGraph", "state" : { "branch" : "main", - "revision" : "7e1664cd4fcd2f019be59f2969619eb022bdcec2" + "revision" : "d0a060c05377350dea911601dc5416738c000541" } }, { diff --git a/Sources/OpenSwiftUICore/Data/State/State.swift b/Sources/OpenSwiftUICore/Data/State/State.swift index 5b5a0e4aa..9a7eae7b8 100644 --- a/Sources/OpenSwiftUICore/Data/State/State.swift +++ b/Sources/OpenSwiftUICore/Data/State/State.swift @@ -266,7 +266,7 @@ private struct StatePropertyBox: DynamicPropertyBox { signal: signal ) } - let signalChanged = signal.changedValue()?.changed ?? false + let signalChanged = signal.changedValue(options: [])?.changed ?? false property._value = location!.updateValue property._location = location! return (signalChanged ? location!.wasRead : false) || locationChanged From bb0ca9e725ab6413d010b866cf61edd20a8191eb Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Sep 2025 01:31:12 +0800 Subject: [PATCH 10/12] Add ObservationUtilTests --- .../Observation/ObservationUtilTests.swift | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Tests/OpenSwiftUICoreTests/Data/Observation/ObservationUtilTests.swift diff --git a/Tests/OpenSwiftUICoreTests/Data/Observation/ObservationUtilTests.swift b/Tests/OpenSwiftUICoreTests/Data/Observation/ObservationUtilTests.swift new file mode 100644 index 000000000..44f4b69d5 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Data/Observation/ObservationUtilTests.swift @@ -0,0 +1,106 @@ +// +// ObservationUtilTests.swift +// OpenSwiftUICoreTests +// + +import Testing +import OpenAttributeGraphShims +@_spi(OpenSwiftUI) +import OpenObservation +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +@Suite("ObservationUtil Tests") +struct ObservationUtilTests { + + @Observable + final class TestModel { + var value: Int = 0 + var text: String = "" + } + + @Test("_withObservation tracks property access") + func withObservationTracking() { + let model = TestModel() + + // Test that we can track observation access + let (result, accessList) = _withObservation { + // Access the model's properties + let _ = model.value + let _ = model.text + return model.value + 10 + } + + // Verify the result + #expect(result == 10) + + // Verify that an access list was created + #expect(accessList != nil) + } + + @MainActor + @Test( + "_withObservation with attribute installation", + .disabled("retain a invalid ptr and cause crash. Investigate it later.") // TODO + ) + func withObservationAttribute() { + let model = TestModel() + let viewGraph = ViewGraph(rootViewType: EmptyView.self) + viewGraph.rootSubgraph.apply { + let attribute = Attribute(value: 0) + + // Use _withObservation with an attribute + let result = _withObservation(attribute: attribute) { + // Access the model's properties + let sum = model.value + 5 + return sum + } + + #expect(result == 5) + } + } + + @Test("Nested observation contexts") + func nestedObservationContexts() { + let model1 = TestModel() + let model2 = TestModel() + + model1.value = 10 + model2.value = 20 + + let (outerResult, outerAccessList) = _withObservation { + let val1 = model1.value + + // Nested observation context + let (innerResult, innerAccessList) = _withObservation { + let val2 = model2.value + return val2 * 2 + } + + #expect(innerResult == 40) + #expect(innerAccessList != nil) + + return val1 + innerResult + } + + #expect(outerResult == 50) + #expect(outerAccessList != nil) + } + + @Test("ObservationRegistrar latestAccessLists tracking") + func latestAccessListsTracking() { + let model = TestModel() + + // Clear any previous access lists + ObservationRegistrar.latestAccessLists = [] + + let (_, accessList) = _withObservation { + let _ = model.value + return "test" + } + + // Verify that an access list was created and recorded + #expect(accessList != nil) + #expect(!ObservationRegistrar.latestAccessLists.isEmpty) + } +} From ab333ef18ced654df1cb00b17a12eca1308de5fd Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Sep 2025 02:53:25 +0800 Subject: [PATCH 11/12] Add OpenObservation setup to CI pipeline - Create openobservation_setup.sh script to clone OpenObservation repository - Update darwin_setup_build.sh to include OpenObservation setup - Fix CI failures caused by missing local OpenObservation dependency --- Scripts/CI/darwin_setup_build.sh | 1 + Scripts/CI/openobservation_setup.sh | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100755 Scripts/CI/openobservation_setup.sh diff --git a/Scripts/CI/darwin_setup_build.sh b/Scripts/CI/darwin_setup_build.sh index 8a362c3d3..6662ab251 100755 --- a/Scripts/CI/darwin_setup_build.sh +++ b/Scripts/CI/darwin_setup_build.sh @@ -11,4 +11,5 @@ cd $REPO_ROOT Scripts/CI/opencoregraphics_setup.sh Scripts/CI/openattributegraph_setup.sh Scripts/CI/openrenderbox_setup.sh +Scripts/CI/openobservation_setup.sh Scripts/CI/framework_setup.sh diff --git a/Scripts/CI/openobservation_setup.sh b/Scripts/CI/openobservation_setup.sh new file mode 100755 index 000000000..a9548469e --- /dev/null +++ b/Scripts/CI/openobservation_setup.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# A `realpath` alternative using the default C implementation. +filepath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} + +REPO_ROOT="$(dirname $(dirname $(dirname $(filepath $0))))" + +clone_checkout_openobservation() { + cd $REPO_ROOT + revision=$(Scripts/CI/get_revision.sh openobservation) + cd .. + if [ ! -d OpenObservation ]; then + gh repo clone OpenSwiftUIProject/OpenObservation + cd OpenObservation + else + echo "OpenObservation already exists, skipping clone." + cd OpenObservation + git fetch --all --quiet + git stash --quiet || true + git reset --hard --quiet origin/main + fi + git checkout --quiet $revision +} + +clone_checkout_openobservation \ No newline at end of file From be6ee0548938d90ec17fc6cb84b4a2c14452ab6e Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 1 Sep 2025 02:37:04 +0800 Subject: [PATCH 12/12] Fix Linux build issue --- Sources/OpenSwiftUI/Util/AnyAttributeFix.swift | 8 ++++++++ Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/Sources/OpenSwiftUI/Util/AnyAttributeFix.swift b/Sources/OpenSwiftUI/Util/AnyAttributeFix.swift index a58025568..158949cbb 100644 --- a/Sources/OpenSwiftUI/Util/AnyAttributeFix.swift +++ b/Sources/OpenSwiftUI/Util/AnyAttributeFix.swift @@ -33,6 +33,14 @@ extension AnyAttribute { package func createIndirect() -> AnyAttribute { preconditionFailure("#39") } + + package var subgraph: Subgraph { + preconditionFailure("#39") + } + + package var subgraph2: Subgraph? { + preconditionFailure("#39") + } } extension AnyAttribute { diff --git a/Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift b/Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift index 7fe20215b..337f1e24e 100644 --- a/Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift +++ b/Sources/OpenSwiftUICore/Util/AnyAttributeFix.swift @@ -76,6 +76,14 @@ extension AnyAttribute { package func invalidateValue() { preconditionFailure("#39") } + + package var subgraph: Subgraph { + preconditionFailure("#39") + } + + package var subgraph2: Subgraph? { + preconditionFailure("#39") + } } extension AnyAttribute {