-
Notifications
You must be signed in to change notification settings - Fork 48
CacheStorage (2/3) #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
maxg-square
wants to merge
10
commits into
maxg/cache_1_equivalency
Choose a base branch
from
maxg/cache_2_envcache
base: maxg/cache_1_equivalency
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
CacheStorage (2/3) #569
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
84fe9bb
Squash
maxg-square 6ad49cb
Merge branch 'maxg/cache_1_equivalency' into maxg/cache_2_envcache
maxg-square fa89474
Merge forward changes from pr 1
maxg-square dc4f9de
PR feedback cleanup
maxg-square f615bb3
PR feedback cleanup
maxg-square 50fee4e
Fix tests
maxg-square 62c9d44
Merge branch 'maxg/cache_1_equivalency' into maxg/cache_2_envcache
maxg-square 72a7cec
Move cache
maxg-square 1ba5ca4
Ref semantics for caches
maxg-square 9df57ad
Ref semantics for caches
maxg-square File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
...es/CrossLayoutCaching/Cache/CacheImplementations/EnvironmentAndValueValidatingCache.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import Foundation | ||
|
|
||
| /// A convenience wrapper around ValidatingCache which ensures that only values which were cached in equivalent environments are returned, and allows for additional data to be stored to be validated. | ||
| @_spi(HostingViewContext) public final class EnvironmentAndValueValidatingCache<Key, Value, AdditionalValidationData>: Sendable where Key: Hashable & Sendable { | ||
|
|
||
| private var backing = ValidatingCache<Key, Value, (EnvironmentAccessList, AdditionalValidationData)>() | ||
|
|
||
| public init() {} | ||
|
|
||
| /// Retrieves or creates a value based on a key and a validation function, alongside environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment should be evaluated. | ||
| /// - validate: A function that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| /// - Note: Generally, prefer the `validationValue` versions of this method if the validation value conforms to ContextuallyEquivalent or Equatable. | ||
| func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| context: CrossLayoutCacheableContext, | ||
| validate: (AdditionalValidationData) -> Bool, | ||
| create: (Environment) -> (Value, AdditionalValidationData) | ||
| ) -> Value { | ||
| backing.retrieveOrCreate(key: key) { | ||
| environment.isCacheablyEquivalent(to: $0.0, in: context) && validate($0.1) | ||
| } create: { | ||
| let ((value, additional), accessList) = environment.observingAccess { environment in | ||
| create(environment) | ||
| } | ||
| return (value, (accessList, additional)) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| @_spi(HostingViewContext) extension EnvironmentAndValueValidatingCache where AdditionalValidationData: CrossLayoutCacheable { | ||
|
|
||
| /// Retrieves or creates a value based on a key and a validation value, alongside environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment and validation values should be evaluated. | ||
| /// - validationValue: A value that will be compared using contextual equivalence that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| public func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| validationValue: AdditionalValidationData, | ||
| context: CrossLayoutCacheableContext, | ||
| create: (Environment) -> (Value) | ||
| ) -> Value { | ||
| retrieveOrCreate(key: key, environment: environment, context: context) { | ||
| $0.isCacheablyEquivalent(to: validationValue, in: context) | ||
| } create: { | ||
| (create($0), validationValue) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
| @_spi(HostingViewContext) extension EnvironmentAndValueValidatingCache where AdditionalValidationData: Equatable { | ||
|
|
||
| /// Retrieves or creates a value based on a key and a validation value, alongside environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment should be evaluated. | ||
| /// - validationValue: A value that will be compared using strict equality that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| @_disfavoredOverload public func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| validationValue: AdditionalValidationData, | ||
| context: CrossLayoutCacheableContext, | ||
| create: (Environment) -> (Value) | ||
| ) -> Value { | ||
| retrieveOrCreate(key: key, environment: environment, context: context) { | ||
| $0 == validationValue | ||
| } create: { | ||
| (create($0), validationValue) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
33 changes: 33 additions & 0 deletions
33
...UI/Sources/CrossLayoutCaching/Cache/CacheImplementations/EnvironmentValidatingCache.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import Foundation | ||
|
|
||
| /// A convenience wrapper around ValidatingCache which ensures that only values which were cached in equivalent environments are returned. | ||
| @_spi(HostingViewContext) public final class EnvironmentValidatingCache<Key, Value>: Sendable where Key: Hashable & Sendable { | ||
|
|
||
| private var backing = ValidatingCache<Key, Value, EnvironmentAccessList>() | ||
|
|
||
| public init() {} | ||
|
|
||
| /// Retrieves or creates a value based on a key and environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment should be evaluated. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| context: CrossLayoutCacheableContext, | ||
| create: (Environment) -> Value | ||
| ) -> Value { | ||
| backing.retrieveOrCreate(key: key) { | ||
| environment.isCacheablyEquivalent(to: $0, in: context) | ||
| } create: { | ||
| environment.observingAccess { environment in | ||
| create(environment) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
77 changes: 77 additions & 0 deletions
77
BlueprintUI/Sources/CrossLayoutCaching/Cache/CacheImplementations/ValidatingCache.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import Foundation | ||
|
|
||
| /// Validating cache is a cache which, if it has a value for a key, runs a closure to verify that the cache value is still relevant and not state. | ||
| /// This is useful for cases when you might otherwise wish to store the validation data as a key, but it does not conform to Hashable, or its hashability properties do not neccessarily affect the validity of the cached data. | ||
| @_spi(HostingViewContext) public final class ValidatingCache<Key, Value, ValidationData>: Sendable where Key: Hashable & Sendable { | ||
|
|
||
| private var storage: [Key: ValueStorage] = [:] | ||
|
|
||
| private struct ValueStorage { | ||
| let value: Value | ||
| let validationData: ValidationData | ||
| } | ||
|
|
||
| public init() {} | ||
|
|
||
| /// Retrieves the value for a given key, without evaluating any validation conditions. | ||
| public subscript(uncheckedKey key: Key) -> Value? { | ||
| storage[key]?.value | ||
| } | ||
|
|
||
| /// Retrieves or creates a value based on a key and validation function. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - validate: A function that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| public func retrieveOrCreate( | ||
| key: Key, | ||
| validate: (ValidationData) -> Bool, | ||
| create: () -> (Value, ValidationData) | ||
| ) -> Value { | ||
| if let valueStorage = storage[key] { | ||
| Logger.logValidatingCrossLayoutCacheKeyHit(key: key) | ||
| let validationToken = Logger.logValidatingCacheValidationStart(key: key) | ||
| if validate(valueStorage.validationData) { | ||
| Logger.logValidatingCacheHitAndValidationSuccess(key: key) | ||
| Logger.logValidatingCacheValidationEnd(validationToken, key: key) | ||
| return valueStorage.value | ||
| #if DEBUG | ||
| // FIXME: WAY TO MAKE SURE THIS DOESN'T SHIP ON. | ||
| // Enable this to always evaluate the create block to assert that the caching is producing the expected value. | ||
| // if let stored = valueStorage.value as? (any Equatable) { | ||
| // let fresh = create().0 as! Equatable | ||
| // assert(stored.isEqual(fresh)) | ||
| // } | ||
| // return valueStorage.value | ||
| #endif | ||
| } else { | ||
| Logger.logValidatingCacheHitAndValidationFailure(key: key) | ||
| Logger.logValidatingCacheValidationEnd(validationToken, key: key) | ||
| } | ||
| } else { | ||
| Logger.logValidatingCrossLayoutCacheKeyMiss(key: key) | ||
| } | ||
| let createToken = Logger.logValidatingCacheFreshValueCreationStart(key: key) | ||
| let (fresh, validationData) = create() | ||
| Logger.logValidatingCacheFreshValueCreationEnd(createToken, key: key) | ||
| storage[key] = ValueStorage(value: fresh, validationData: validationData) | ||
| return fresh | ||
| } | ||
|
|
||
| public func removeValue(forKey key: Key) -> Value? { | ||
| storage.removeValue(forKey: key)?.value | ||
| } | ||
|
|
||
| } | ||
|
|
||
| extension Equatable { | ||
|
|
||
| fileprivate func isEqual(_ other: any Equatable) -> Bool { | ||
| guard let other = other as? Self else { | ||
| return false | ||
| } | ||
| return self == other | ||
| } | ||
|
|
||
| } |
27 changes: 27 additions & 0 deletions
27
BlueprintUI/Sources/CrossLayoutCaching/Cache/CacheKey.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import Foundation | ||
|
|
||
| /// Types conforming to this protocol can be used as keys in `HostingViewContext`. | ||
| /// | ||
| /// Using a type as the key allows us to strongly type each value, with the | ||
| /// key's `CrossLayoutCacheKey.Value` associated value. | ||
| /// | ||
| /// ## Example | ||
| /// | ||
| /// Usually a key is implemented with an uninhabited type, such an empty enum. | ||
| /// | ||
| /// enum WidgetCountsKey: CrossLayoutCacheKey { | ||
| /// static let emptyValue: [WidgetID: Int] = [:] | ||
| /// } | ||
| /// | ||
| /// You can write a small extension on `HostingViewContext` to make it easier to use your key. | ||
| /// | ||
| /// extension HostingViewContext { | ||
| /// var widgetCounts: [WidgetID: Int] { | ||
| /// get { self[WidgetCountsKey.self] } | ||
| /// set { self[WidgetCountsKey.self] = newValue } | ||
| /// } | ||
| /// } | ||
| public protocol CrossLayoutCacheKey { | ||
| associatedtype Value | ||
| static var emptyValue: Self.Value { get } | ||
| } |
85 changes: 85 additions & 0 deletions
85
BlueprintUI/Sources/CrossLayoutCaching/Cache/HostingViewContext.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import Foundation | ||
| import UIKit | ||
|
|
||
| /// Environment-associated storage used to cache types used across layout passes (eg, size calculations). | ||
| /// The storage itself is type-agnostic, requiring only that its keys and values conform to the `CrossLayoutCacheKey` protocol | ||
| /// Caches are responsible for managing their own lifetimes and eviction strategies. | ||
| @_spi(HostingViewContext) public final class HostingViewContext: Sendable, CustomDebugStringConvertible { | ||
|
|
||
| // Optional name to distinguish between instances for debugging purposes. | ||
| public var name: String? = nil | ||
| fileprivate var storage: [ObjectIdentifier: Any] = [:] | ||
|
|
||
| init() { | ||
| NotificationCenter.default.addObserver( | ||
| forName: UIApplication.didReceiveMemoryWarningNotification, | ||
| object: nil, | ||
| queue: .main | ||
| ) { [weak self] _ in | ||
| self?.storage.removeAll() | ||
| } | ||
| } | ||
|
|
||
| public subscript<KeyType>(key: KeyType.Type) -> KeyType.Value where KeyType: CrossLayoutCacheKey { | ||
| get { | ||
| storage[ObjectIdentifier(key), default: KeyType.emptyValue] as! KeyType.Value | ||
| } | ||
| set { | ||
| storage[ObjectIdentifier(key)] = newValue | ||
| } | ||
| } | ||
|
|
||
| public var debugDescription: String { | ||
| let debugName = if let name { | ||
| "HostingViewContext (\(name))" | ||
| } else { | ||
| "HostingViewContext" | ||
| } | ||
| return "\(debugName): \(storage.count) entries" | ||
| } | ||
|
|
||
| } | ||
|
|
||
| extension Environment { | ||
|
|
||
| struct HostingViewContextKey: InternalEnvironmentKey { | ||
| static var defaultValue = HostingViewContext() | ||
| } | ||
|
|
||
|
|
||
| @_spi(HostingViewContext) public var hostingViewContext: HostingViewContext { | ||
| get { self[HostingViewContextKey.self] } | ||
| set { self[HostingViewContextKey.self] = newValue } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /// A UUID that changes based on value changes of the containing type. | ||
| /// Two fingerprinted objects may be quickly compared for equality by comparing their fingerprints. | ||
| /// This is roughly analagous to a hash, although with inverted properties: Two objects with the same fingerprint can be trivially considered equal, but two otherwise equal objects may have different fingerprint. | ||
| /// - Note: This type is deliberately NOT equatable – this is to prevent accidental inclusion of it when its containing type is equatable. | ||
| struct CacheComparisonFingerprint: CrossLayoutCacheable, CustomStringConvertible { | ||
|
|
||
| typealias Value = UUID | ||
|
|
||
| var value: Value | ||
|
|
||
| init() { | ||
| value = Value() | ||
| } | ||
|
|
||
| mutating func modified() { | ||
| value = Value() | ||
| } | ||
|
|
||
| /// - Note: This is a duplicate message but: this type is deliberately NOT equatable – this is to prevent accidental inclusion of it when its containing type is equatable. Use this instead. | ||
| func isCacheablyEquivalent(to other: CacheComparisonFingerprint?, in context: CrossLayoutCacheableContext) -> Bool { | ||
| value == other?.value | ||
| } | ||
|
|
||
| var description: String { | ||
| value.uuidString | ||
| } | ||
|
|
||
| } | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plucking this question out of an old thread:
Can we avoid the need for
InternalEnvironmentKeyby making this key use a reference comparison ofHostingViewContext, or hardcodedtrue?