-
Notifications
You must be signed in to change notification settings - Fork 49
[DNR] All the caching #574
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
base: main
Are you sure you want to change the base?
Changes from all commits
a63d20d
3abebaf
9b87e2a
6802a22
6429e56
76aee10
44aa682
10d11e9
ee32e1c
23a1dee
dad612b
0e78167
8a39fce
7c6b2dc
e46e778
5faf9e1
5ef770e
6dafc50
56b751d
4a126e7
499ee96
15d0c5d
0d413d3
94e9b23
1c45f7f
9afcdd1
deb85cb
42104f9
398dc40
22ed032
be9dd21
942c1fa
6519418
e8c27c0
872bcaa
afbbc3c
f5ea2eb
f4c5c2e
c2e2ddc
9c0da9f
1842de6
0f19b47
707cc8d
3ec654b
76cac01
ea63760
f35c210
a599c4a
993355b
094dbcd
33979ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,18 @@ struct MeasurableStorage: ContentStorage { | |
|
||
let childCount = 0 | ||
|
||
let validationKey: AnyContextuallyEquivalent? | ||
let measurer: (SizeConstraint, Environment) -> CGSize | ||
|
||
init(validationKey: some ContextuallyEquivalent, measurer: @escaping (SizeConstraint, Environment) -> CGSize) { | ||
self.validationKey = AnyContextuallyEquivalent(validationKey) | ||
self.measurer = measurer | ||
} | ||
|
||
init(measurer: @escaping (SizeConstraint, Environment) -> CGSize) { | ||
validationKey = nil | ||
self.measurer = measurer | ||
} | ||
} | ||
|
||
extension MeasurableStorage: CaffeinatedContentStorage { | ||
|
@@ -17,7 +28,19 @@ extension MeasurableStorage: CaffeinatedContentStorage { | |
environment: Environment, | ||
node: LayoutTreeNode | ||
) -> CGSize { | ||
measurer(proposal, environment) | ||
guard environment.layoutMode.options.measureableStorageCache, let validationKey else { | ||
return measurer(proposal, environment) | ||
} | ||
|
||
let key = MeasurableSizeKey(path: node.path, max: proposal.maximum) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated to your PR, but TIL we store the path as a string, I thought we'd keep the fully qualified |
||
return environment.cacheStorage.measurableStorageCache.retrieveOrCreate( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thought: Could we stick making the key and looking up the value on |
||
key: key, | ||
environment: environment, | ||
validationValue: validationKey, | ||
context: .elementSizing, | ||
) { environment in | ||
measurer(proposal, environment) | ||
} | ||
} | ||
|
||
func performCaffeinatedLayout( | ||
|
@@ -28,3 +51,40 @@ extension MeasurableStorage: CaffeinatedContentStorage { | |
[] | ||
} | ||
} | ||
|
||
extension MeasurableStorage { | ||
|
||
fileprivate struct MeasurableSizeKey: Hashable { | ||
|
||
let path: String | ||
let max: CGSize | ||
|
||
func hash(into hasher: inout Hasher) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Shouldn't need this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Artifact of me precalculating the hash in an earlier rev |
||
path.hash(into: &hasher) | ||
max.hash(into: &hasher) | ||
} | ||
|
||
} | ||
|
||
} | ||
|
||
extension CacheStorage { | ||
|
||
private struct MeasurableStorageCacheKey: CacheStorage.Key { | ||
static var emptyValue = EnvironmentAndValueValidatingCache< | ||
maxg-square marked this conversation as resolved.
Show resolved
Hide resolved
|
||
MeasurableStorage.MeasurableSizeKey, | ||
CGSize, | ||
AnyContextuallyEquivalent | ||
>() | ||
} | ||
|
||
fileprivate var measurableStorageCache: EnvironmentAndValueValidatingCache< | ||
MeasurableStorage.MeasurableSizeKey, | ||
CGSize, | ||
AnyContextuallyEquivalent | ||
> { | ||
get { self[MeasurableStorageCacheKey.self] } | ||
set { self[MeasurableStorageCacheKey.self] = newValue } | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import Foundation | ||
|
||
/// Types conforming to this protocol can be used as keys in `CacheStorage`. | ||
/// | ||
/// Using a type as the key allows us to strongly type each value, with the | ||
/// key's `CacheStorage.Key.Value` associated value. | ||
/// | ||
/// ## Example | ||
/// | ||
/// Usually a key is implemented with an uninhabited type, such an empty enum. | ||
/// | ||
/// enum WidgetCountsKey: CacheStorage.Key { | ||
/// static let emptyValue: [WidgetID: Int] = [:] | ||
/// } | ||
/// | ||
/// You can write a small extension on `CacheStorage` to make it easier to use your key. | ||
/// | ||
/// extension CacheStorage { | ||
/// var widgetCounts: [WidgetID: Int] { | ||
/// get { self[WidgetCountsKey.self] } | ||
/// set { self[WidgetCountsKey.self] = newValue } | ||
/// } | ||
/// } | ||
/// | ||
extension CacheStorage { | ||
|
||
public protocol Key { | ||
associatedtype Value | ||
static var emptyValue: Self.Value { get } | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import Foundation | ||
#if canImport(UIKit) | ||
import UIKit | ||
#endif | ||
|
||
/// 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 `CacheKey` protocol | ||
/// Caches are responsible for managing their own lifetimes and eviction strategies. | ||
@_spi(CacheStorage) public final class CacheStorage: Sendable, CustomDebugStringConvertible { | ||
|
||
// Optional name to distinguish between instances for debugging purposes. | ||
public var name: String? = nil | ||
fileprivate var storage: [ObjectIdentifier: Any] = [:] | ||
|
||
init() { | ||
#if canImport(UIKit) | ||
NotificationCenter.default.addObserver( | ||
forName: UIApplication.didReceiveMemoryWarningNotification, | ||
object: nil, | ||
queue: .main | ||
) { [weak self] _ in | ||
self?.storage.removeAll() | ||
} | ||
#endif | ||
} | ||
|
||
public subscript<KeyType>(key: KeyType.Type) -> KeyType.Value where KeyType: CacheStorage.Key { | ||
get { | ||
storage[ObjectIdentifier(key), default: KeyType.emptyValue] as! KeyType.Value | ||
} | ||
set { | ||
storage[ObjectIdentifier(key)] = newValue | ||
} | ||
} | ||
|
||
public var debugDescription: String { | ||
let debugName = if let name { | ||
"CacheStorage (\(name))" | ||
} else { | ||
"CacheStorage" | ||
} | ||
return "\(debugName): \(storage.count) entries" | ||
} | ||
|
||
} | ||
|
||
extension Environment { | ||
|
||
struct CacheStorageEnvironmentKey: InternalEnvironmentKey { | ||
static var defaultValue = CacheStorage() | ||
} | ||
|
||
|
||
@_spi(CacheStorage) public var cacheStorage: CacheStorage { | ||
get { self[CacheStorageEnvironmentKey.self] } | ||
set { self[CacheStorageEnvironmentKey.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 ComparableFingerprint: ContextuallyEquivalent, 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 isEquivalent(to other: ComparableFingerprint?, in context: EquivalencyContext) -> Bool { | ||
value == other?.value | ||
} | ||
|
||
var description: String { | ||
value.uuidString | ||
} | ||
|
||
} | ||
|
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.
I think we need to do some sort of garbage collection on this, right? Eg, evict paths that are no longer present after a layout update?
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.
I experimented a bit with this – (basically just tweaking the
traverse
method here to also include paths, comparing before/after update) – in the sample app it didn't seem to make much of a difference, and actually seemed to produce some false positives (especially in list contexts) – relying onBlueprintView
deinit seemed much more consistent with the eviction behavior I'd expect.