-
Notifications
You must be signed in to change notification settings - Fork 28.5k
Add a mechanism to observe layer tree composition. #103378
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
Conversation
removeCallback case: call it when RO gets disposed. Argument against: RO can track when it's disposed and just short circuit the callback. RO also shoul dnever really get disposed when its tree is compositing anyway...? |
|
||
void _fireCompositionCallbacks(bool includeChildren) { | ||
for (final VoidCallback callback in List<VoidCallback>.of(_callbacks)) { | ||
for (final VoidCallback callback in List<VoidCallback>.of(_callbacks.values)) { |
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 .toList is the fast one since the exact type of the receiver is known in the implementation
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.
toList calls of
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.
🤦♂️
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.
Looks like if you give List.of an iterable that isn't backed by a list it first converts it to a list. so calling toList should be universally better
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.
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.
RIP
/// Adds a [CompositionCallback] for the current [ContainerLayer] used by this | ||
/// context. | ||
/// | ||
/// Composition callbacks are called whenever the layer tree a container layer |
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: this sentence is missing an is
, I think.
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.
Reworded - I changed my mind on how this was going to sound and forgot to finish rewriting it ahah
/// | ||
/// Calling the returned callback will remove [callback] from the composition | ||
/// callbacks. | ||
/// See also: |
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: add blank line before this
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.
Done
child._fireCompositionCallbacks(includeChildren); | ||
child = child.nextSibling; | ||
} | ||
|
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: remove blank line
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.
Done
@@ -136,6 +136,16 @@ class AnnotationResult<T> { | |||
/// * [RenderView.compositeFrame], which implements this recomposition protocol | |||
/// for painting [RenderObject] trees on the display. | |||
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { | |||
final Map<int, VoidCallback> _callbacks = <int, VoidCallback>{}; | |||
|
|||
void _fireCompositionCallbacks(bool includeChildren) { |
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: make the argument a required optional parameter to make callsites easier to read.
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.
Done
|
||
} | ||
|
||
/// Adds a callback for when the layer tree that this layer is part of gets |
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.
Conceptually, why is this a feature of ContainerLayer over the Layer base class?
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.
Mainly because RenderObjects are the normal clients for this via the PaintingContext, and the PaintingContext only knows its container layer - picture layers might not ever be created for it.
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.
Since the backing storage is in the Layer class, it feels like this method should also be defined there? PaintingContext could still use it on the ContainerLayer, right?
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.
That will then require casts to get to parents, but I guess that's ok.
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.
oh, it won't. I see now.
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.
It will but it's probably ok.
super.detach(); | ||
Layer? child = firstChild; | ||
while (child != null) { | ||
child.detach(); | ||
child = child.nextSibling; | ||
} | ||
// Children fired them already in child.detach(). | ||
_fireCompositionCallbacks(false); |
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.
Why do we need to fire on detach?
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.
Detach indicates we're not going to get drawn again. If we don't fire here, we risk telling clients that we rendered something and never updating when we got dropped.
Attach shouldn't be needed because if you get composed after attach buildScene will be called.
I'll update the comment.
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.
Maybe also update the doc comment explaining when the callback will be called. From the current description my understanding was it would only happen when the layer is still part of the scene, not when it is going away...
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.
Done
@@ -936,6 +1013,7 @@ class ContainerLayer extends Layer { | |||
ui.Scene buildScene(ui.SceneBuilder builder) { | |||
updateSubtreeNeedsAddToScene(); | |||
addToScene(builder); | |||
_fireCompositionCallbacks(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.
This adds a walk of the entire layer tree every time we build a new scene (i.e. every single frame) - irregardless of whether any callbacks are registered or not and people not using this feature at all would also pay the cost?
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.
So one thought I had is that we could mark the root of a tree about whether any callbacks are actually present or not. I started working on that but abandoned it because of added complexity and I just wanted to have something working. I can take a look at it again.
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.
Ideally if there are no callbacks this has zero cost. maybe we do that with a single pristine check for now and optimize 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.
Overall, I think this looks reasonable.
if (!alwaysNeedsAddToScene) { | ||
markNeedsAddToScene(); | ||
} | ||
|
||
_incrementSubtreeCompositionObserverCount(-(child as Layer)._childrenWithCompositionCallbacks); |
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 probably type the "child" argument as Layer in the method signature, right?
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 that will cause breakage in implementations that override these methods today, but it's probably worth it...
if (!alwaysNeedsAddToScene) { | ||
markNeedsAddToScene(); | ||
} | ||
_incrementSubtreeCompositionObserverCount((child as Layer)._childrenWithCompositionCallbacks); |
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.
same
} | ||
} | ||
|
||
/// Adds a callback for when the layer tree that this layer is part of gets |
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.
it also gets called when the layer is no longer part of the tree, right?
|
||
} | ||
|
||
/// Adds a callback for when the layer tree that this layer is part of gets |
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.
Since the backing storage is in the Layer class, it feels like this method should also be defined there? PaintingContext could still use it on the ContainerLayer, right?
return () { | ||
_callbacks.remove(_nextCallbackId); | ||
if (_callbacks.isEmpty) { | ||
_incrementSubtreeCompositionObserverCount(-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.
nit: maybe this should be called update instead of increment?
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.
Done
// interested in observing composition will need to get updated to show | ||
// Children fired them already in child.detach(). |
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.
something is odd here. Is the sentence on the previous line incomplete?
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, fixed that.
/// | ||
/// The callback receives a reference to this layer. The recipient must not | ||
/// mutate the layer during the scope of the callback, but may traverse the | ||
/// tree to find information about the current transform or clip. |
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 should probably document what happens if new callbacks are added/removed within the callback.
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.
Done
/// layers. | ||
/// | ||
/// The callback receives a reference to this layer. The recipient must not | ||
/// mutate the layer during the scope of the callback, but may traverse the |
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: if the callback is received do to a detach there will be nothing to traverse. Somewhere this doc should talk about that case.
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.
Detached != not in a tree, it just means there's no owner.
You can be in a state where !attached && parent != null
.
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
Added some tests. |
if (!alwaysNeedsAddToScene) { | ||
markNeedsAddToScene(); | ||
} | ||
if (child._childrenWithCompositionCallbacks != 0) { | ||
_updateSubtreeCompositionObserverCount(child._childrenWithCompositionCallbacks); |
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.
isn't this missing a -
? The count needs to be reduced by child._childrenWithCompositionCallbacks
right?
If there is no failing test for this case, let's add one! :)
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.
You are right. I thought I had a test covering this but did not. Added one.
_debugMutationsLocked = true; | ||
return true; | ||
}()); | ||
callback(this is ContainerLayer ? this as ContainerLayer : parent!); |
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.
Why is this not just callback(this)
?
From the docs, that's what I would have expected here and the type checking is kinda odd here. Also, what's the guarantee that a non-Container Layer will always have a non-null parent?
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.
The callback should probably just always receive a reference to "this" - if the callback wants, it can then check whether it was given a non-ContainerLayer and access the parent.
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.
The callback was written to receive a ContainerLayer
, which has all the interesting methods for traversal on it.
In the layer tree, leaf Layer
objects always have a parent at compositing time - in fact, this method is called from buildScene
, which only exists on ContainerLayer
. If there was no container layer parent, then this method would never get called.
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 the callback was typed to receive a Layer
, for example, and a caller wanted to start reviewing the clip bounds applied, it woud have to do something like ContainerLayer container = layer is ContainerLayer ? layer : layer.parent!
.
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'll ggo ahead and change this now - I suppose if the implementation ever shifted or some new more interesting method for this is added on Layer
it'd be better to not have to refactor this API 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.
Also moved describeClipBounds
to layer.
@@ -910,12 +1006,28 @@ class PerformanceOverlayLayer extends Layer { | |||
} | |||
} | |||
|
|||
/// The signature of the callback added in [Layer.addCompositionCallback]. | |||
typedef CompositionCallback = void Function(ContainerLayer); |
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.
Related to the above, why is the argument not typed as ContainerLayer and not just Layer?
/// Adds a [CompositionCallback] for the current [ContainerLayer] used by this | ||
/// context. | ||
/// | ||
/// Composition callbacks are called whenever the layer tree containing the |
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: some of these docs seem duplicated from Layer.addCompositionCallback. Consider using a macro.
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.
Done
}; | ||
return () { | ||
_callbacks.remove(callbackId); | ||
if (_callbacks.isEmpty) { |
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.
This seems incorrect? We are increasing the count unconditionally in line 203 whenever a new callback is added, but we are only decreasing the count by one when the last callback is removed. So if I add 2 and remove both of them the _childrenWithCompositionCallbacks
would remain at 1 + 1 - 1 = 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.
Not sure what the desired behavior for the count is. _updateSubtreeCompositionObserverCount
makes it sound like we want to count observers, so increasing for each callback sounds right. But _childrenWithCompositionCallbacks
makes it sound like we are counting the children that have observers meaning we should only increase the count for the first observer added to a child. We should probably unify the naming...
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.
Ahh yes, I mixed up two different approaches I was trying. I'll write some more tests for this and fix.
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.
Added a test, unconditionally call -1
here, and renamed var.
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.
LGTM
}()); | ||
}; | ||
return () { | ||
_callbacks.remove(callbackId); |
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.
Maybe assert that the callback is actually still in _callbacks before attempting to remove it to avoid that somebody accidentally calls the remover twice?
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.
Done.
Calling it twice is wasted work but it shouldn't cause any program incorrectness at least. But assert sgtm.
This is WIP to enable a refactor of visibility_detector that does not affect compositing.
Currently the way visibility_detector works is use a render object that always requires composition so that it can push a layer that always re-composites its part of the tree so that it can observe composition.
This has several major drawbacks:
This patch does not have tests yet, and in particular it's not clear to me whether we'd need some kind of removeCallback mechanism - if we do, the API will have to look different.
Looking for some early feedback while I continue to work on the visibility_detector change.