diff --git a/Sources/SwiftCrossUI/State/DynamicKeyPath.swift b/Sources/SwiftCrossUI/State/DynamicKeyPath.swift deleted file mode 100644 index ea3a247c526..00000000000 --- a/Sources/SwiftCrossUI/State/DynamicKeyPath.swift +++ /dev/null @@ -1,90 +0,0 @@ -#if canImport(Darwin) - import func Darwin.memcmp -#elseif canImport(Glibc) - import func Glibc.memcmp -#elseif canImport(WinSDK) - import func WinSDK.memcmp -#elseif canImport(Android) - import func Android.memcmp -#endif - -/// A type similar to KeyPath, but that can be constructed at run time given -/// an instance of a struct, and the value of the desired property. Construction -/// fails if the property's in-memory representation is not unique within the -/// struct. SwiftCrossUI only uses ``DynamicKeyPath`` in situations where it is -/// highly likely for properties to have unique in-memory representations, such -/// as when properties have internal storage pointers. -struct DynamicKeyPath { - /// The property's offset within instances of ``T``. - var offset: Int - - /// Constructs a key path given an instance of the base type, and the - /// value of the desired property. The initializer will search through - /// the base instance's in-memory representation to find the unique offset - /// that matches the representation of the given property value. If such an - /// offset can't be found or isn't unique, then the initialiser returns `nil`. - init?( - forProperty value: Value, - of base: Base, - label: String? = nil - ) { - let propertyAlignment = MemoryLayout.alignment - let propertySize = MemoryLayout.size - let baseStructSize = MemoryLayout.size - - var index = 0 - var matches: [Int] = [] - while index + propertySize <= baseStructSize { - let isMatch = - withUnsafeBytes(of: base) { viewPointer in - withUnsafeBytes(of: value) { valuePointer in - memcmp( - viewPointer.baseAddress!.advanced(by: index), - valuePointer.baseAddress!, - propertySize - ) - } - } == 0 - if isMatch { - matches.append(index) - } - index += propertyAlignment - } - - guard let offset = matches.first else { - logger.warning( - "no offset found for dynamic property", - metadata: ["property": "\(label ?? "")"] - ) - return nil - } - - guard matches.count == 1 else { - logger.warning( - "multiple offsets found for dynamic property", - metadata: ["property": "\(label ?? "")"] - ) - return nil - } - - self.offset = offset - } - - /// Gets the property's value on the given instance. - func get(_ base: Base) -> Value { - withUnsafeBytes(of: base) { buffer in - buffer.baseAddress!.advanced(by: offset) - .assumingMemoryBound(to: Value.self) - .pointee - } - } - - /// Sets the property's value to a new value on the given instance. - func set(_ base: inout Base, _ newValue: Value) { - withUnsafeMutableBytes(of: &base) { buffer in - buffer.baseAddress!.advanced(by: offset) - .assumingMemoryBound(to: Value.self) - .pointee = newValue - } - } -} diff --git a/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift b/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift index bab949376d4..f0281855f9f 100644 --- a/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift +++ b/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift @@ -1,211 +1,62 @@ -/// A cache for dynamic property updaters. The keys are the ObjectIdentifiers of -/// various Base types that we have already computed dynamic property updaters +/// A cache for dynamic property updaters. The keys are the `ObjectIdentifier`s of +/// various `Base` types that we have already computed dynamic property updaters /// for, and the elements are corresponding cached instances of -/// DynamicPropertyUpdater. +/// `DynamicPropertyUpdater`. /// /// From some basic testing, this caching seems to reduce layout times by 5-10% /// (at the time of implementation). @MainActor -var updaterCache: [ObjectIdentifier: Any] = [:] +private var updaterCache: [ObjectIdentifier: Any] = [:] /// A helper for updating the dynamic properties of a stateful struct (e.g. /// a View or App conforming struct). Dynamic properties are those that conform /// to ``DynamicProperty``, e.g. properties annotated with `@State`. /// -/// At initialisation the updater will attempt to determine the byte offset of -/// each stateful property in the struct. This is guaranteed to succeed if every -/// dynamic property in the provided struct instance contains internal mutable -/// storage, because the storage pointers will provide unique byte sequences. -/// Otherwise, offset discovery will fail when two dynamic properties share the -/// same pattern in memory. When offset discovery fails the updater will fall -/// back to using Mirrors each time `update` gets called, which can be 1500x -/// times slower when the view has 0 state properties, and 9x slower when the -/// view has 4 properties, with the factor slowly dropping as the number of -/// properties increases. +/// At initialisation the updater will determine the byte offset of each +/// stateful property in the struct. struct DynamicPropertyUpdater { - typealias PropertyUpdater = ( - _ old: Base?, - _ new: Base, - _ environment: EnvironmentValues - ) -> Void - - /// The updaters for each of Base's dynamic properties. If `nil`, then we - /// failed to compute - let propertyUpdaters: [PropertyUpdater]? + /// The offsets and types of each of `Base`'s dynamic properties. + private var propertyOffsets: [(offset: Int, type: any DynamicProperty.Type)] /// Creates a new dynamic property updater which can efficiently update - /// all dynamic properties on any value of type Base without creating - /// any mirrors after the initial creation of the updater. Pass in a - /// `mirror` of base if you already have one to save us creating another one. + /// all dynamic properties on any value of type `Base` without creating + /// any mirrors. @MainActor - init(for base: Base, mirror: Mirror? = nil) { + init(for _: Base.Type) { + self.propertyOffsets = [] + // Unlikely shortcut, but worthwhile when we can. if MemoryLayout.size == 0 { - self.propertyUpdaters = [] return } - if let cachedUpdater = updaterCache[ObjectIdentifier(Base.self)], - let cachedUpdater = cachedUpdater as? Self - { - self = cachedUpdater + if let cachedUpdater = updaterCache[ObjectIdentifier(Base.self)] { + self = cachedUpdater as! Self return } - var propertyUpdaters: [PropertyUpdater] = [] - - let mirror = mirror ?? Mirror(reflecting: base) - for child in mirror.children { - let label = child.label ?? "" - let value = child.value - - guard let value = value as? any DynamicProperty else { - continue + forEachField(of: Base.self) { _, offset, type in + if let type = type as? any DynamicProperty.Type { + propertyOffsets.append((offset, type)) } - - guard let updater = Self.getUpdater(for: value, base: base, label: label) else { - // We have failed to create the required property updaters. Fallback - // to using Mirrors to update all properties. - logger.warning( - """ - failed to produce DynamicPropertyUpdater; falling back to \ - slower Mirror-based property updating approach - """, - metadata: ["type": "\(Base.self)"] - ) - self.propertyUpdaters = nil - - // We intentionally return without caching the updaters here so - // that we if this failure is a fluke we can recover on a - // subsequent attempt for the same type. It may turn out that in - // practice types that fail are ones that always fail, in which - // case we should update this code to add the current updater to - // the cache. - return - } - - propertyUpdaters.append(updater) } - self.propertyUpdaters = propertyUpdaters - updaterCache[ObjectIdentifier(Base.self)] = self } /// Updates each dynamic property of the given value. func update(_ value: Base, with environment: EnvironmentValues, previousValue: Base?) { - guard let propertyUpdaters else { - // Fall back to our old dynamic property updating approach which involves a lot of - // Mirror overhead. This should be rare. - Self.updateFallback(of: value, previousValue: previousValue, environment: environment) - return - } - - for updater in propertyUpdaters { - updater(previousValue, value, environment) - } - } - - /// Gets an updater for the property of base with the given value. If multiple - /// properties exist matching the byte pattern of `value`, then `nil` is returned. - /// - /// The returned updater is reusable and doesn't use Mirror. - private static func getUpdater( - for value: T, - base: Base, - label: String - ) -> PropertyUpdater? { - guard let keyPath = DynamicKeyPath(forProperty: value, of: base, label: label) else { - return nil - } - - let updater = { (old: Base?, new: Base, environment: EnvironmentValues) in - let property = keyPath.get(new) - property.update( - with: environment, - previousValue: old.map(keyPath.get) - ) - } - - return updater - } - - /// Updates the dynamic properties of a value given a previous instance of the - /// type (if one exists) and the current environment. - private static func updateFallback( - of value: T, - previousValue: T?, - environment: EnvironmentValues - ) { - let newMirror = Mirror(reflecting: value) - let previousMirror = previousValue.map(Mirror.init(reflecting:)) - if let previousChildren = previousMirror?.children { - let propertySequence = zip(newMirror.children, previousChildren) - for (newProperty, previousProperty) in propertySequence { - guard - let newValue = newProperty.value as? any DynamicProperty, - let previousValue = previousProperty.value as? any DynamicProperty - else { - continue - } - - updateDynamicPropertyFallback( - newProperty: newValue, - previousProperty: previousValue, - environment: environment, - enclosingTypeName: "\(T.self)", - propertyName: newProperty.label - ) - } - } else { - for property in newMirror.children { - guard let newValue = property.value as? any DynamicProperty else { - continue - } - - updateDynamicPropertyFallback( - newProperty: newValue, - previousProperty: nil, - environment: environment, - enclosingTypeName: "\(T.self)", - propertyName: property.label - ) - } - } - } - - /// Updates a dynamic property. Required to unmask the concrete type of the - /// property. Since the two properties can technically be two different - /// types, Swift correctly wouldn't allow us to assume they're both the - /// same. So we unwrap one and then dynamically check whether the other - /// matches using a type cast. - private static func updateDynamicPropertyFallback( - newProperty: Property, - previousProperty: (any DynamicProperty)?, - environment: EnvironmentValues, - enclosingTypeName: String, - propertyName: String? - ) { - let castedPreviousProperty: Property? - if let previousProperty { - guard let previousProperty = previousProperty as? Property else { - fatalError( - """ - Supposedly unreachable... previous and current types of \ - \(enclosingTypeName).\(propertyName ?? "") \ - don't match. - """ + for (offset, type) in propertyOffsets { + update(type) + + func update(_: Property.Type) { + getProperty(Property.self, of: value, at: offset).update( + with: environment, + previousValue: previousValue.map { + getProperty(Property.self, of: $0, at: offset) + } ) } - - castedPreviousProperty = previousProperty - } else { - castedPreviousProperty = nil } - - newProperty.update( - with: environment, - previousValue: castedPreviousProperty - ) } } diff --git a/Sources/SwiftCrossUI/State/ForEachField.swift b/Sources/SwiftCrossUI/State/ForEachField.swift new file mode 100644 index 00000000000..756b2855233 --- /dev/null +++ b/Sources/SwiftCrossUI/State/ForEachField.swift @@ -0,0 +1,74 @@ +// Adapted from https://github.com/swiftlang/swift/blob/swift-6.2.3-RELEASE/stdlib/public/core/ReflectionMirror.swift + +// NB: I have absolutely zero clue why this is importable here. Xcode just shows +// me an empty file when I command-click it. Nevertheless, it works just fine and +// SourceKit seems to have no trouble finding stuff from here, so I'll roll +// with it for now. +private import SwiftShims + +@_silgen_name("swift_reflectionMirror_recursiveCount") +private func getRecursiveChildCount(of: Any.Type) -> Int + +@_silgen_name("swift_reflectionMirror_recursiveChildMetadata") +private func getChildMetadata( + of: Any.Type, + index: Int, + fieldMetadata: UnsafeMutablePointer<_FieldReflectionMetadata> +) -> Any.Type + +@_silgen_name("swift_reflectionMirror_recursiveChildOffset") +private func getChildOffset(of: Any.Type, index: Int) -> Int + +/// Calls the given closure on every field of the specified type. +/// +/// The standard library exposes a function named `_forEachField(of:options:body:)`, +/// which calls directly into the runtime's reflection facilities in order to +/// get the names and types of the provided value's stored properties, bypassing +/// most of the `Mirror` overhead. However, it's annotated with `@_spi(Reflection)`, +/// and most people's toolchain installations don't include the stdlib's SPI +/// interfaces. So we have to reimplement this function ourselves. +/// +/// There are three runtime functions used to implement this: +/// - `swift_reflectionMirror_recursiveCount(_:)` +/// - `swift_reflectionMirror_recursiveChildMetadata(_:index:fieldMetadata:)` +/// - `swift_reflectionMirror_recursiveChildOffset(_:index:)` +/// +/// All three of these have been present in the runtime for at least 6 years (probably +/// longer), and since `_forEachField(of:options:body:)` is used [within Combine] (and +/// presumably other Apple frameworks), it's fairly unlikely that either it or the +/// functions it depends on (which are the same as shown above) will be removed any +/// time soon. +/// +/// - SeeAlso: The [original implementation] as of Swift 6.2.3. +/// +/// [within Combine]: https://forums.swift.org/t/how-is-the-published-property-wrapper-implemented/58223/11 +/// [original implementation]: https://github.com/swiftlang/swift/blob/swift-6.2.3-RELEASE/stdlib/public/core/ReflectionMirror.swift +/// +/// - Parameters: +/// - type: The type to inspect. +/// - body: A closure to call with information about each field in `type`. +/// The parameters to `body` are the name of the field, the offset of the +/// field, and the type of the field. +func forEachField( + of type: Any.Type, + body: (_ name: String?, _ offset: Int, _ type: Any.Type) -> Void +) { + let childCount = getRecursiveChildCount(of: type) + for index in 0..(_: Property.Type, of base: Base, at offset: Int) -> Property { + withUnsafeBytes(of: base) { buffer in + buffer.baseAddress!.advanced(by: offset) + .assumingMemoryBound(to: Property.self) + .pointee + } +} diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index b84caa1a568..ac7c1aa10ac 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -83,8 +83,7 @@ public class ViewGraphNode: Sendable { parentEnvironment = environment cancellables = [] - let mirror = Mirror(reflecting: view) - dynamicPropertyUpdater = DynamicPropertyUpdater(for: view, mirror: mirror) + dynamicPropertyUpdater = DynamicPropertyUpdater(for: NodeView.self) let viewEnvironment = updateEnvironment(environment) @@ -108,6 +107,7 @@ public class ViewGraphNode: Sendable { backend.tag(widget: widget, as: tag) // Update the view and its children when state changes (children are always updated first). + let mirror = Mirror(reflecting: view) for property in mirror.children { if property.label == "state" && property.value is ObservableObject { logger.warning( @@ -123,13 +123,10 @@ public class ViewGraphNode: Sendable { continue } - cancellables.append( - value.didChange - .observeAsUIUpdater(backend: backend) { [weak self] in - guard let self else { return } - self.bottomUpUpdate() - } - ) + let cancellable = value.didChange.observeAsUIUpdater(backend: backend) { [weak self] in + self?.bottomUpUpdate() + } + cancellables.append(cancellable) } } diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index f5afa9ab4c4..5f047d2ec12 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -28,7 +28,7 @@ class _App { ) self.cancellables = [] - dynamicPropertyUpdater = DynamicPropertyUpdater(for: app) + dynamicPropertyUpdater = DynamicPropertyUpdater(for: AppRoot.self) } func refreshSceneGraph() {