Skip to content

Commit 8462f5c

Browse files
authored
[Observation] Optimize the storage of registrar entries, provide KeyPath caching, and uniqueness notification (#78151)
1 parent 0a9ab41 commit 8462f5c

File tree

4 files changed

+99
-130
lines changed

4 files changed

+99
-130
lines changed

lib/Macros/Sources/ObservationMacros/ObservableMacro.swift

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,35 +36,70 @@ public struct ObservableMacro {
3636

3737
static let registrarVariableName = "_$observationRegistrar"
3838

39-
static func registrarVariable(_ observableType: TokenSyntax) -> DeclSyntax {
39+
static func registrarVariable(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
4040
return
4141
"""
4242
@\(raw: ignoredMacroName) private let \(raw: registrarVariableName) = \(raw: qualifiedRegistrarTypeName)()
4343
"""
4444
}
4545

46-
static func accessFunction(_ observableType: TokenSyntax) -> DeclSyntax {
47-
return
46+
static func accessFunction(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
47+
let memberGeneric = context.makeUniqueName("Member")
48+
return
4849
"""
49-
internal nonisolated func access<Member>(
50-
keyPath: KeyPath<\(observableType), Member>
50+
internal nonisolated func access<\(memberGeneric)>(
51+
keyPath: KeyPath<\(observableType), \(memberGeneric)>
5152
) {
52-
\(raw: registrarVariableName).access(self, keyPath: keyPath)
53+
\(raw: registrarVariableName).access(self, keyPath: keyPath)
5354
}
5455
"""
5556
}
5657

57-
static func withMutationFunction(_ observableType: TokenSyntax) -> DeclSyntax {
58-
return
58+
static func withMutationFunction(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
59+
let memberGeneric = context.makeUniqueName("Member")
60+
let mutationGeneric = context.makeUniqueName("MutationResult")
61+
return
5962
"""
60-
internal nonisolated func withMutation<Member, MutationResult>(
61-
keyPath: KeyPath<\(observableType), Member>,
62-
_ mutation: () throws -> MutationResult
63-
) rethrows -> MutationResult {
64-
try \(raw: registrarVariableName).withMutation(of: self, keyPath: keyPath, mutation)
63+
internal nonisolated func withMutation<\(memberGeneric), \(mutationGeneric)>(
64+
keyPath: KeyPath<\(observableType), \(memberGeneric)>,
65+
_ mutation: () throws -> \(mutationGeneric)
66+
) rethrows -> \(mutationGeneric) {
67+
try \(raw: registrarVariableName).withMutation(of: self, keyPath: keyPath, mutation)
6568
}
6669
"""
6770
}
71+
72+
static func shouldNotifyObserversNonEquatableFunction(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
73+
let memberGeneric = context.makeUniqueName("Member")
74+
return
75+
"""
76+
private nonisolated func shouldNotifyObservers<\(memberGeneric)>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { true }
77+
"""
78+
}
79+
80+
static func shouldNotifyObserversEquatableFunction(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
81+
let memberGeneric = context.makeUniqueName("Member")
82+
return
83+
"""
84+
private nonisolated func shouldNotifyObservers<\(memberGeneric): Equatable>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { lhs != rhs }
85+
"""
86+
}
87+
88+
static func shouldNotifyObserversNonEquatableObjectFunction(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
89+
let memberGeneric = context.makeUniqueName("Member")
90+
return
91+
"""
92+
private nonisolated func shouldNotifyObservers<\(memberGeneric): AnyObject>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { lhs !== rhs }
93+
"""
94+
}
95+
96+
static func shouldNotifyObserversEquatableObjectFunction(_ observableType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
97+
let memberGeneric = context.makeUniqueName("Member")
98+
return
99+
"""
100+
private nonisolated func shouldNotifyObservers<\(memberGeneric): Equatable & AnyObject>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { lhs != rhs }
101+
"""
102+
}
68103

69104
static var ignoredAttribute: AttributeSyntax {
70105
AttributeSyntax(
@@ -220,9 +255,13 @@ extension ObservableMacro: MemberMacro {
220255

221256
var declarations = [DeclSyntax]()
222257

223-
declaration.addIfNeeded(ObservableMacro.registrarVariable(observableType), to: &declarations)
224-
declaration.addIfNeeded(ObservableMacro.accessFunction(observableType), to: &declarations)
225-
declaration.addIfNeeded(ObservableMacro.withMutationFunction(observableType), to: &declarations)
258+
declaration.addIfNeeded(ObservableMacro.registrarVariable(observableType, context: context), to: &declarations)
259+
declaration.addIfNeeded(ObservableMacro.accessFunction(observableType, context: context), to: &declarations)
260+
declaration.addIfNeeded(ObservableMacro.withMutationFunction(observableType, context: context), to: &declarations)
261+
declaration.addIfNeeded(ObservableMacro.shouldNotifyObserversNonEquatableFunction(observableType, context: context), to: &declarations)
262+
declaration.addIfNeeded(ObservableMacro.shouldNotifyObserversEquatableFunction(observableType, context: context), to: &declarations)
263+
declaration.addIfNeeded(ObservableMacro.shouldNotifyObserversNonEquatableObjectFunction(observableType, context: context), to: &declarations)
264+
declaration.addIfNeeded(ObservableMacro.shouldNotifyObserversEquatableObjectFunction(observableType, context: context), to: &declarations)
226265

227266
return declarations
228267
}
@@ -298,6 +337,10 @@ public struct ObservationTrackedMacro: AccessorMacro {
298337
let identifier = property.identifier?.trimmed else {
299338
return []
300339
}
340+
341+
guard let container = context.lexicalContext[0].as(ClassDeclSyntax.self) else {
342+
return []
343+
}
301344

302345
if property.hasMacroApplication(ObservableMacro.ignoredMacroName) {
303346
return []
@@ -307,34 +350,46 @@ public struct ObservationTrackedMacro: AccessorMacro {
307350
"""
308351
@storageRestrictions(initializes: _\(identifier))
309352
init(initialValue) {
310-
_\(identifier) = initialValue
353+
_\(identifier) = initialValue
311354
}
312355
"""
313356

314357
let getAccessor: AccessorDeclSyntax =
315358
"""
316359
get {
317-
access(keyPath: \\.\(identifier))
318-
return _\(identifier)
360+
access(keyPath: \(container.trimmed.name)._cachedKeypath_\(identifier))
361+
return _\(identifier)
319362
}
320363
"""
321364

322365
let setAccessor: AccessorDeclSyntax =
323366
"""
324367
set {
325-
withMutation(keyPath: \\.\(identifier)) {
326-
_\(identifier) = newValue
327-
}
368+
guard shouldNotifyObservers(_\(identifier), newValue) else {
369+
return
370+
}
371+
withMutation(keyPath: \(container.trimmed.name)._cachedKeypath_\(identifier)) {
372+
_\(identifier) = newValue
373+
}
328374
}
329375
"""
330376

377+
// Note: this accessor cannot test the equality since it would incur
378+
// additional CoW's on structural types. Most mutations in-place do
379+
// not leave the value equal so this is "fine"-ish.
380+
// Warning to future maintence: adding equality checks here can make
381+
// container mutation O(N) instead of O(1).
382+
// e.g. observable.array.append(element) should just emit a change
383+
// to the new array, and NOT cause a copy of each element of the
384+
// array to an entirely new array.
331385
let modifyAccessor: AccessorDeclSyntax =
332386
"""
333387
_modify {
334-
access(keyPath: \\.\(identifier))
335-
\(raw: ObservableMacro.registrarVariableName).willSet(self, keyPath: \\.\(identifier))
336-
defer { \(raw: ObservableMacro.registrarVariableName).didSet(self, keyPath: \\.\(identifier)) }
337-
yield &_\(identifier)
388+
let keyPath = \(container.trimmed.name)._cachedKeypath_\(identifier)
389+
access(keyPath: keyPath)
390+
\(raw: ObservableMacro.registrarVariableName).willSet(self, keyPath: keyPath)
391+
defer { \(raw: ObservableMacro.registrarVariableName).didSet(self, keyPath: keyPath) }
392+
yield &_\(identifier)
338393
}
339394
"""
340395

@@ -352,7 +407,12 @@ extension ObservationTrackedMacro: PeerMacro {
352407
in context: Context
353408
) throws -> [DeclSyntax] {
354409
guard let property = declaration.as(VariableDeclSyntax.self),
355-
property.isValidForObservation else {
410+
property.isValidForObservation,
411+
let identifier = property.identifier?.trimmed else {
412+
return []
413+
}
414+
415+
guard let container = context.lexicalContext[0].as(ClassDeclSyntax.self) else {
356416
return []
357417
}
358418

@@ -362,7 +422,11 @@ extension ObservationTrackedMacro: PeerMacro {
362422
}
363423

364424
let storage = DeclSyntax(property.privatePrefixed("_", addingAttribute: ObservableMacro.ignoredAttribute))
365-
return [storage]
425+
let cachedKeypath: DeclSyntax =
426+
"""
427+
private static let _cachedKeypath_\(identifier) = \\\(container.name).\(identifier)
428+
"""
429+
return [storage, cachedKeypath]
366430
}
367431
}
368432

stdlib/public/Observation/Sources/Observation/Observable.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public protocol Observable { }
3939
/// }
4040
/// }
4141
@available(SwiftStdlib 5.9, *)
42-
@attached(member, names: named(_$observationRegistrar), named(access), named(withMutation))
42+
@attached(member, names: named(_$observationRegistrar), named(access), named(withMutation), named(shouldNotifyObservers))
4343
@attached(memberAttribute)
4444
@attached(extension, conformances: Observable)
4545
public macro Observable() =
@@ -51,7 +51,7 @@ public macro Observable() =
5151
/// framework isn't necessary.
5252
@available(SwiftStdlib 5.9, *)
5353
@attached(accessor, names: named(init), named(get), named(set), named(_modify))
54-
@attached(peer, names: prefixed(_))
54+
@attached(peer, names: prefixed(_), prefixed(_cachedKeypath_))
5555
public macro ObservationTracked() =
5656
#externalMacro(module: "ObservationMacros", type: "ObservationTrackedMacro")
5757

@@ -61,7 +61,7 @@ public macro ObservationTracked() =
6161
/// is accessible to the observing object. To prevent observation of an
6262
/// accessible property, attach the `ObservationIgnored` macro to the property.
6363
@available(SwiftStdlib 5.9, *)
64-
@attached(accessor, names: named(willSet))
64+
@attached(accessor)
6565
public macro ObservationIgnored() =
6666
#externalMacro(module: "ObservationMacros", type: "ObservationIgnoredMacro")
6767

stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift

Lines changed: 3 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ public struct ObservationRegistrar: Sendable {
4040
private enum ObservationKind {
4141
case willSetTracking(@Sendable (AnyKeyPath) -> Void)
4242
case didSetTracking(@Sendable (AnyKeyPath) -> Void)
43-
case computed(@Sendable (Any) -> Void)
44-
case values(ValuesObserver)
4543
}
4644

4745
private struct Observation {
@@ -70,42 +68,6 @@ public struct ObservationRegistrar: Sendable {
7068
return nil
7169
}
7270
}
73-
74-
var observer: (@Sendable (Any) -> Void)? {
75-
switch kind {
76-
case .computed(let observer):
77-
return observer
78-
default:
79-
return nil
80-
}
81-
}
82-
83-
var isValueObserver: Bool {
84-
switch kind {
85-
case .values:
86-
return true
87-
default:
88-
return false
89-
}
90-
}
91-
92-
func emit<Element>(_ value: Element) -> Bool {
93-
switch kind {
94-
case .values(let observer):
95-
return observer.emit(value)
96-
default:
97-
return false
98-
}
99-
}
100-
101-
func cancel() {
102-
switch kind {
103-
case .values(let observer):
104-
observer.cancel()
105-
default:
106-
break
107-
}
108-
}
10971
}
11072

11173
private var id = 0
@@ -135,31 +97,6 @@ public struct ObservationRegistrar: Sendable {
13597
return id
13698
}
13799

138-
internal mutating func registerComputedValues(for properties: Set<AnyKeyPath>, observer: @Sendable @escaping (Any) -> Void) -> Int {
139-
let id = generateId()
140-
observations[id] = Observation(kind: .computed(observer), properties: properties)
141-
for keyPath in properties {
142-
lookups[keyPath, default: []].insert(id)
143-
}
144-
return id
145-
}
146-
147-
internal mutating func registerValues(for properties: Set<AnyKeyPath>, storage: ValueObservationStorage) -> Int {
148-
let id = generateId()
149-
observations[id] = Observation(kind: .values(ValuesObserver(storage: storage)), properties: properties)
150-
for keyPath in properties {
151-
lookups[keyPath, default: []].insert(id)
152-
}
153-
return id
154-
}
155-
156-
internal func valueObservers(for keyPath: AnyKeyPath) -> Set<Int> {
157-
guard let ids = lookups[keyPath] else {
158-
return []
159-
}
160-
return ids.filter { observations[$0]?.isValueObserver == true }
161-
}
162-
163100
internal mutating func cancel(_ id: Int) {
164101
if let observation = observations.removeValue(forKey: id) {
165102
for keyPath in observation.properties {
@@ -170,14 +107,10 @@ public struct ObservationRegistrar: Sendable {
170107
}
171108
}
172109
}
173-
observation.cancel()
174110
}
175111
}
176112

177113
internal mutating func cancelAll() {
178-
for observation in observations.values {
179-
observation.cancel()
180-
}
181114
observations.removeAll()
182115
lookups.removeAll()
183116
}
@@ -194,29 +127,16 @@ public struct ObservationRegistrar: Sendable {
194127
return trackers
195128
}
196129

197-
internal mutating func didSet<Subject: Observable, Member>(keyPath: KeyPath<Subject, Member>) -> ([@Sendable (Any) -> Void], [@Sendable (AnyKeyPath) -> Void]) {
198-
var observers = [@Sendable (Any) -> Void]()
130+
internal mutating func didSet<Subject: Observable, Member>(keyPath: KeyPath<Subject, Member>) -> [@Sendable (AnyKeyPath) -> Void] {
199131
var trackers = [@Sendable (AnyKeyPath) -> Void]()
200132
if let ids = lookups[keyPath] {
201133
for id in ids {
202-
if let observer = observations[id]?.observer {
203-
observers.append(observer)
204-
cancel(id)
205-
}
206134
if let tracker = observations[id]?.didSetTracker {
207135
trackers.append(tracker)
208136
}
209137
}
210138
}
211-
return (observers, trackers)
212-
}
213-
214-
internal mutating func emit<Element>(_ value: Element, ids: Set<Int>) {
215-
for id in ids {
216-
if observations[id]?.emit(value) == true {
217-
cancel(id)
218-
}
219-
}
139+
return trackers
220140
}
221141
}
222142

@@ -233,14 +153,6 @@ public struct ObservationRegistrar: Sendable {
233153
state.withCriticalRegion { $0.registerTracking(for: properties, didSet: observer) }
234154
}
235155

236-
internal func registerComputedValues(for properties: Set<AnyKeyPath>, observer: @Sendable @escaping (Any) -> Void) -> Int {
237-
state.withCriticalRegion { $0.registerComputedValues(for: properties, observer: observer) }
238-
}
239-
240-
internal func registerValues(for properties: Set<AnyKeyPath>, storage: ValueObservationStorage) -> Int {
241-
state.withCriticalRegion { $0.registerValues(for: properties, storage: storage) }
242-
}
243-
244156
internal func cancel(_ id: Int) {
245157
state.withCriticalRegion { $0.cancel(id) }
246158
}
@@ -263,17 +175,10 @@ public struct ObservationRegistrar: Sendable {
263175
_ subject: Subject,
264176
keyPath: KeyPath<Subject, Member>
265177
) {
266-
let (ids, (actions, tracking)) = state.withCriticalRegion { ($0.valueObservers(for: keyPath), $0.didSet(keyPath: keyPath)) }
267-
if !ids.isEmpty {
268-
let value = subject[keyPath: keyPath]
269-
state.withCriticalRegion { $0.emit(value, ids: ids) }
270-
}
178+
let tracking = state.withCriticalRegion { $0.didSet(keyPath: keyPath) }
271179
for action in tracking {
272180
action(keyPath)
273181
}
274-
for action in actions {
275-
action(subject)
276-
}
277182
}
278183
}
279184

stdlib/public/Observation/Sources/Observation/ObservationTracking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public struct ObservationTracking: Sendable {
115115
})
116116
}
117117

118-
struct State {
118+
struct State: @unchecked Sendable {
119119
var values = [ObjectIdentifier: ObservationTracking.Id]()
120120
var cancelled = false
121121
var changed: AnyKeyPath?

0 commit comments

Comments
 (0)