diff --git a/Sources/OpenSwiftUICore/Graph/GraphInputs.swift b/Sources/OpenSwiftUICore/Graph/GraphInputs.swift index ff887bbc..a0ce656b 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphInputs.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphInputs.swift @@ -114,13 +114,14 @@ public struct _GraphInputs { private mutating func recordReusableInput(_ input: T.Type) where T: GraphInput, T.Value: GraphReusable { let filter = BloomFilter(type: input) - let reusableInputs = customInputs[ReusableInputs.self] - if reusableInputs.stack.top == T.self { + let inputs = customInputs[ReusableInputs.self] + let stack = inputs.stack + guard stack.top != T.self else { return } customInputs[ReusableInputs.self] = ReusableInputStorage( - filter: reusableInputs.filter.union(filter), - stack: .node(value: T.self, next: reusableInputs.stack) + filter: inputs.filter.union(filter), + stack: .node(value: T.self, next: stack) ) } diff --git a/Sources/OpenSwiftUICore/Graph/GraphReuse.swift b/Sources/OpenSwiftUICore/Graph/GraphReuse.swift index 6cbfdaca..9fec68e6 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphReuse.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphReuse.swift @@ -3,23 +3,37 @@ // OpenSwiftUICore // // Audited for iOS 18.0 -// Status: Blocked by _GraphInputs +// Status: Complete // ID: 3E2D3733C4CBF57EC1EA761D02CE8317 (SwiftUICore) +import Foundation package import OpenGraphShims +#if OPENSWIFTUI_SWIFT_LOG +import Logging +#else +import os.log +#endif + +// MARK: - IndirectAttributeMap -public final class IndirectAttributeMap { +package final class IndirectAttributeMap { package final let subgraph: Subgraph + package final var map: [AnyAttribute: AnyAttribute] + package init(subgraph: Subgraph) { self.subgraph = subgraph self.map = [:] } } +// MARK: - GraphReusable + package protocol GraphReusable { static var isTriviallyReusable: Bool { get } + mutating func makeReusable(indirectMap: IndirectAttributeMap) + func tryToReuse(by other: Self, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool } @@ -28,6 +42,8 @@ extension GraphReusable { package static var isTriviallyReusable: Bool { false } } +// MARK: - _GraphValue + GraphReusable + extension _GraphValue: GraphReusable { package mutating func makeReusable(indirectMap: IndirectAttributeMap) { value.makeReusable(indirectMap: indirectMap) @@ -41,65 +57,137 @@ extension _GraphValue where Value: GraphReusable { package static var isTriviallyReusable: Bool { Value.isTriviallyReusable } } -//extension _GraphInputs : GraphReusable { -// package mutating func makeReusable(indirectMap: IndirectAttributeMap) -// package func tryToReuse(by other: _GraphInputs, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool -//} +// MARK: - _GraphInputs + GraphReusable [WIP] + +extension _GraphInputs: GraphReusable { + package mutating func makeReusable(indirectMap: IndirectAttributeMap) { + time.makeReusable(indirectMap: indirectMap) + phase.makeReusable(indirectMap: indirectMap) + changedDebugProperties.insert(.phase) + environment.makeReusable(indirectMap: indirectMap) + detachEnvironmentInputs() + changedDebugProperties.insert(.environment) + transaction.makeReusable(indirectMap: indirectMap) + func project(_ type: Input.Type) where Input: GraphInput { + guard !Input.isTriviallyReusable else { + return + } + var value = self[Input.self] + Input.makeReusable(indirectMap: indirectMap, value: &value) + self[Input.self] = value + } + let stack = customInputs[ReusableInputs.self].stack + for value in stack { + project(value) + } + } + + package func tryToReuse(by other: _GraphInputs, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool { + guard time.tryToReuse(by: other.time, indirectMap: indirectMap, testOnly: testOnly), + phase.tryToReuse(by: other.phase, indirectMap: indirectMap, testOnly: testOnly), + environment.tryToReuse(by: other.environment, indirectMap: indirectMap, testOnly: testOnly), + transaction.tryToReuse(by: other.transaction, indirectMap: indirectMap, testOnly: testOnly) + else { + Log.graphReuse("Reuse failed: standard inputs") + return false + } + return reuseCustomInputs(by: other, indirectMap: indirectMap, testOnly: testOnly) + } + + private func reuseCustomInputs(by other: _GraphInputs, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool { + let reusableInputs = customInputs[ReusableInputs.self] + let otherReusableInputs = other.customInputs[ReusableInputs.self] + guard reusableInputs.filter == otherReusableInputs.filter else { + return false + } + var reusableInputsArray: [ObjectIdentifier] = [] + for value in reusableInputs.stack { + reusableInputsArray.append(ObjectIdentifier(value)) + } + var otherReusableInputsArray: [ObjectIdentifier] = [] + for value in otherReusableInputs.stack { + otherReusableInputsArray.append(ObjectIdentifier(value)) + } + guard reusableInputsArray == otherReusableInputsArray else { + Log.graphReuse("Reuse failed: custom inputs type mismatch") + return false + } + var ignoredTypes = reusableInputsArray + [ObjectIdentifier(ReusableInputs.self)] + guard !customInputs.mayNotBeEqual(to: other.customInputs, ignoredTypes: &ignoredTypes) else { + Log.graphReuse("Reuse failed: custom inputs plist equality") + return false + } + func project(_ type: Input.Type) -> Bool where Input: GraphInput { + guard let index = ignoredTypes.firstIndex(of: ObjectIdentifier(type)) else { + return true + } + let lastIndex = ignoredTypes.count - 1 + ignoredTypes.swapAt(index, lastIndex) + guard !Input.isTriviallyReusable else { + return true + } + guard Input.tryToReuse(self[type], by: other[type], indirectMap: indirectMap, testOnly: testOnly) else { + Log.graphReuse("Reuse failed: custom input \(Input.self)") + return false + } + return true + } + let stack = reusableInputs.stack + for value in stack { + guard project(value) else { + return false + } + continue + } + return true + } +} -//extension _GraphInputs { -// private func reuseCustomInputs(by other: _GraphInputs, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool { -// Log.graphReuse("Reuse failed: custom input \(Self.self)") -// } -//} +// MARK: - Attribute + GraphReusable extension Attribute: GraphReusable { package mutating func makeReusable(indirectMap: IndirectAttributeMap) { + let indirect: AnyAttribute if let result = indirectMap.map[identifier] { - identifier = result + indirect = result } else { - let indirect = indirectMap.subgraph.apply { - IndirectAttribute(source: self) + indirect = indirectMap.subgraph.apply { + IndirectAttribute(source: self).identifier } - indirectMap.map[identifier] = indirect.identifier + indirectMap.map[identifier] = indirect } + identifier = indirect } - + package func tryToReuse(by other: Attribute, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool { - if let result = indirectMap.map[identifier] { - if testOnly { - return true - } else { - result.source = other.identifier - return true - } - } else { + guard let result = indirectMap.map[identifier] else { Log.graphReuse("Reuse failed: missing indirection for \(Value.self)") return false } + if !testOnly { + result.source = other.identifier + } + return true } } -import Foundation -#if canImport(Darwin) -import os.log -#endif - private struct EnableGraphReuseLogging: UserDefaultKeyedFeature { static var key: String { "org.OpenSwiftUIProject.OpenSwiftUI.GraphReuseLogging" } + static var cachedValue: Bool? } extension Log { - #if canImport(Darwin) private static let graphReuseLog: Logger = Logger(subsystem: Log.subsystem, category: "GraphReuse") - #endif - + static func graphReuse(_ message: @autoclosure () -> String) { - #if canImport(Darwin) if EnableGraphReuseLogging.isEnabled { let message = message() + #if OPENSWIFTUI_SWIFT_LOG + graphReuseLog.log(level: .info, "\(message)") + #else graphReuseLog.log("\(message)") + #endif } - #endif } } diff --git a/Sources/OpenSwiftUICore/Tracing/ReuseTrace.swift b/Sources/OpenSwiftUICore/Tracing/ReuseTrace.swift index f3e602f7..5c9708a4 100644 --- a/Sources/OpenSwiftUICore/Tracing/ReuseTrace.swift +++ b/Sources/OpenSwiftUICore/Tracing/ReuseTrace.swift @@ -51,6 +51,11 @@ package struct ReuseTrace { traceReuseFailure("reuse_bodyMismatched") } + @inline(__always) + package static func traceNeverMadeReusableFailure(_ valueType: (any Any.Type)?) { + // TODO + } + // TODO final package class Recorder { diff --git a/Sources/OpenSwiftUICore/View/Input/ViewList.swift b/Sources/OpenSwiftUICore/View/Input/ViewList.swift index ba3bc333..381873a6 100644 --- a/Sources/OpenSwiftUICore/View/Input/ViewList.swift +++ b/Sources/OpenSwiftUICore/View/Input/ViewList.swift @@ -1051,12 +1051,12 @@ extension ViewList.ID.Views: Sendable {} // MARK: - UnaryViewGenerator -public protocol UnaryViewGenerator { +private protocol UnaryViewGenerator { func makeView(inputs: _ViewInputs, indirectMap: IndirectAttributeMap?) -> _ViewOutputs func tryToReuse(by other: Self, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool } -public struct BodyUnaryViewGenerator: UnaryViewGenerator { +private struct BodyUnaryViewGenerator: UnaryViewGenerator { let body: ViewList.Elements.MakeElement public func makeView(inputs: _ViewInputs, indirectMap: IndirectAttributeMap?) -> _ViewOutputs { @@ -1073,10 +1073,10 @@ public struct BodyUnaryViewGenerator: UnaryViewGenerator { } } -public struct TypedUnaryViewGenerator: UnaryViewGenerator where V: View { +private struct TypedUnaryViewGenerator: UnaryViewGenerator where V: View { let view: WeakAttribute - public func makeView(inputs: _ViewInputs, indirectMap: IndirectAttributeMap?) -> _ViewOutputs { + func makeView(inputs: _ViewInputs, indirectMap: IndirectAttributeMap?) -> _ViewOutputs { guard var view = view.attribute else { return .init() } @@ -1086,7 +1086,7 @@ public struct TypedUnaryViewGenerator: UnaryViewGenerator where V: View { return V.makeDebuggableView(view: _GraphValue(view), inputs: inputs) } - public func tryToReuse(by other: TypedUnaryViewGenerator, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool { + func tryToReuse(by other: TypedUnaryViewGenerator, indirectMap: IndirectAttributeMap, testOnly: Bool) -> Bool { guard let view = view.attribute, let otherView = other.view.attribute else { Log.graphReuse("Reuse failed: missing attribute for \(V.self)") return false diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift index 9b020fc0..59966f54 100644 --- a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift +++ b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift @@ -1,8 +1,7 @@ // // GraphHostTests.swift -// OpenSwiftUITests +// OpenSwiftUICoreTests -import OpenSwiftUICore @_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore import Testing