-
Notifications
You must be signed in to change notification settings - Fork 46
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
[Caching] Adds changes to Environment and EnvironmentKey needed for cross-render caching #382
Conversation
741de67
to
71d1175
Compare
35641f0
to
4c97689
Compare
4c97689
to
43c95c5
Compare
…r caching * Adds equatability to Environment and EnvironmentKey * Adds ability to subscribe to environment reads
43c95c5
to
c3480c2
Compare
} | ||
} | ||
|
||
struct StorageKey: Hashable { |
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.
Are we able to just use any EnvronmentKey.Type
once we bump to Xcode 14.1?
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 don't think so, since .Type
s aren't Hashable
. We can likely heavily simplify this type though!
private var values: [ObjectIdentifier: Any] = [:] | ||
/// When enabled, notify any subscribers when a value is read | ||
/// from the environment | ||
var readNotificationsEnabled: Bool = true |
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.
When would you want to disable this? Could we just rely on the presence of subscriptions instead?
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.
We turn this off when adapting the environment so erroneous reads aren't fired; which is coming in a later PR: https://github.com/square/Blueprint/pull/367/files#diff-07c71c5dcdc5da4c2ddf51b560695e562af396d4afafe3908b35656a0907459aR568
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.
Update: Since this a relatively small optimization; we just removed this in a later PR.
} | ||
|
||
extension EnvironmentKey { | ||
static func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool { |
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.
Nit/thought:
static func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool { | |
@_disfavoredOverload | |
static func isEquivalent(_ lhs: Any?, _ rhs: Any?) -> Bool { |
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.
We think it's important to include the word Value
in here, since the type signature is just two Any
s, which could be construed to be the keys. Perhaps areValuesEquivalent
?
enum LayoutPass: Equatable { | ||
case measurement | ||
case layout | ||
} |
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.
We can make this change later if we decide it's worth it, but if we wanted to promote these changes to MarketEnvironment
it would be nice if this was an extendible "scope" instead of being tied to measure and layout.
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.
Had the same thought; it should be easy enough to do this when we extract the environment, but for now I think this is sufficient.
BlueprintUICommonControls/Sources/AttributedLabel+Environment.swift
Outdated
Show resolved
Hide resolved
@@ -41,4 +50,8 @@ struct ClosureURLHandler: URLHandler { | |||
func onTap(url: URL) { | |||
onTap(url) | |||
} | |||
|
|||
func isEquivalent(to other: URLHandler) -> Bool { |
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.
If I change the logic for the onTap
between updates, will this handler still be re-applied to the backing view (even though it's considered equal)?
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.
Yes it will get re-applied. The equality check is only used to cache the ElementState
however the viewDescription
will always be called regardless of the element's cache state.
|
||
/// Equivalency check on the `Value`s of `EnvironmentKey`s. This should return false if the | ||
/// difference in values affects the measurement or layout of consuming elements. | ||
static func isEquivalent(_ lhs: Value, _ rhs: Value) -> Bool |
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.
Thought: Should we name these functions so that they describe that they're comparing only for layout/measurement purposes and not for strict content equability? isEquivalentForLayout(...)
? I suppose not as I can't think of a good name...
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 suppose this question might be more relevant in the next PR (for the ComparableElement
function)
…-environment-equatability * origin/feature/caching: Add debugDescription tests ElementIdentifier test updates per code review Add caching for ElementIdentifier
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.
Approving merge to feature branch to help make iterating on further work easier, but I'd like to do a full pass at the feature branch before it lands!
|
||
/// Gets or sets an environment value by its key. | ||
public subscript<Key>(key: Key.Type) -> Key.Value where Key: EnvironmentKey { | ||
public subscript<KeyType: EnvironmentKey>(key: KeyType.Type) -> KeyType.Value { |
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.
Nit: prefer Key
over KeyType
(Key.Type
is the Type
)
private var measurementDidRead: OnDidRead? | ||
private var layoutDidRead: [OnDidRead] = [] |
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.
Feeling pretty dumb not being able to keep the whole picture in my head, but I'll just ask the stupid questions anyway:
Why do we use an array of read observations for measurement and not layout? I naively expected the opposite because measurement can happen many times while layout should happen once per pass...
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.
A very good question!
We differentiate these because measurements are "flat", eg we only care about the dependencies read during the direct element's measurement – because every layer of the tree that can caches its own measurements. However for layouts, we cache entire trees of layout – including our children – so we want to record and track the environment dependencies of our children as well, and re-perform the layouts if those values change as well.
Does that make sense? I feel like I should probably describe this both better in the code docs here, and in some explanatory tests
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.
Does that make sense?
I think so!
I feel like I should probably describe this both better in the code docs here, and in some explanatory tests
- 1
If we used an array to describe the measurement subscriptions (just to simplify the implementation a bit within environment and perhaps prepare the shape better for supporting "scopes" rather than enum LayoutPass
) would we need to manually clear them out for some reason, or would there just end up being only 1 subscription of that scope?
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 can simplify this to a single array!
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.
Thinking forward through this more, we might actually want to simplify measurement to be append as well. In theory, once we allow measurements and layouts to read the environment, downstream elements could pull a value from the environment that would affect their size, which when the parent measures its children, would affect its final size.
This PR adds updates to
Environment
andEnvironmentKey
that needed for cross-render caching.Summary of changes:
Environment
equatable and adds equivalency toEnvironmentKey
Environment
andEnvironmentKeys
Subset
of keys fromEnvironment
and compare anEnvironment
with aSubset
Environment
reads throughsubscribeToReads()