Add a mechanism to observe layer tree composition.#103378
Add a mechanism to observe layer tree composition.#103378fluttergithubbot merged 18 commits intoflutter:masterfrom
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.
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.
https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/array.dart#L169
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.
| /// 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.
nit: this sentence is missing an is, I think.
There was a problem hiding this comment.
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.
nit: add blank line before this
| child._fireCompositionCallbacks(includeChildren); | ||
| child = child.nextSibling; | ||
| } | ||
|
|
| 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.
nit: make the argument a required optional parameter to make callsites easier to read.
|
|
||
| } | ||
|
|
||
| /// Adds a callback for when the layer tree that this layer is part of gets |
There was a problem hiding this comment.
Conceptually, why is this a feature of ContainerLayer over the Layer base class?
There was a problem hiding this comment.
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.
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.
That will then require casts to get to parents, but I guess that's ok.
There was a problem hiding this comment.
oh, it won't. I see now.
There was a problem hiding this comment.
It will but it's probably ok.
| child = child.nextSibling; | ||
| } | ||
| // Children fired them already in child.detach(). | ||
| _fireCompositionCallbacks(false); |
There was a problem hiding this comment.
Why do we need to fire on detach?
There was a problem hiding this comment.
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.
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...
| ui.Scene buildScene(ui.SceneBuilder builder) { | ||
| updateSubtreeNeedsAddToScene(); | ||
| addToScene(builder); | ||
| _fireCompositionCallbacks(true); |
There was a problem hiding this comment.
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.
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.
Ideally if there are no callbacks this has zero cost. maybe we do that with a single pristine check for now and optimize later?
goderbauer
left a comment
There was a problem hiding this comment.
Overall, I think this looks reasonable.
| markNeedsAddToScene(); | ||
| } | ||
|
|
||
| _incrementSubtreeCompositionObserverCount(-(child as Layer)._childrenWithCompositionCallbacks); |
There was a problem hiding this comment.
We can probably type the "child" argument as Layer in the method signature, right?
There was a problem hiding this comment.
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); |
| } | ||
| } | ||
|
|
||
| /// Adds a callback for when the layer tree that this layer is part of gets |
There was a problem hiding this comment.
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.
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.
nit: maybe this should be called update instead of increment?
| // 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.
something is odd here. Is the sentence on the previous line incomplete?
| /// | ||
| /// 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.
We should probably document what happens if new callbacks are added/removed within the callback.
| /// 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.
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.
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. |
| markNeedsAddToScene(); | ||
| } | ||
| if (child._childrenWithCompositionCallbacks != 0) { | ||
| _updateSubtreeCompositionObserverCount(child._childrenWithCompositionCallbacks); |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
Also moved describeClipBounds to layer.
| } | ||
|
|
||
| /// The signature of the callback added in [Layer.addCompositionCallback]. | ||
| typedef CompositionCallback = void Function(ContainerLayer); |
There was a problem hiding this comment.
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.
nit: some of these docs seem duplicated from Layer.addCompositionCallback. Consider using a macro.
| }; | ||
| return () { | ||
| _callbacks.remove(callbackId); | ||
| if (_callbacks.isEmpty) { |
There was a problem hiding this comment.
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.
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.
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.
Added a test, unconditionally call -1 here, and renamed var.
| }()); | ||
| }; | ||
| return () { | ||
| _callbacks.remove(callbackId); |
There was a problem hiding this comment.
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.
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.