Skip to content

Conversation

@yjbanov
Copy link
Contributor

@yjbanov yjbanov commented Jul 17, 2019

Description

Teach Layer and its implementations, RenderObject and its implementations, and PaintingContext to reuse engine layers. The idea is that a concrete RenderObject creates a Layer and holds on to it as long as it needs it (i.e. when it is composited, and the layer type does not change). In return, each Layer object holds on to an EngineLayer and reports it to the engine via addRetained and oldLayer. This allows the Web engine to reuse DOM elements across frames. Without it, each frame drops all previously rendered HTML and regenerates it from scratch.

Because render objects can manage their own layers, RenderObject.layer may now be called any time. This renders RenderObject.debugLayer unnecessary, and therefore is marked as @Deprecated. This getter can stay in the API indefinitely for backwards-compatibility as it is not hard to maintain.

Issues

Fixes #37256

Tests

I added the following tests:

  • New tests in object_test.dart that test that layers are created and updated properly by PaintingContext.
  • New tests in proxy_box_test.dart and color_filter_test.dart that test that all render objects that use oldLayer create and update layers correctly.
  • New tests in repaint_boundary_test.dart that test that the framework manages repaint boundary OffsetLayer correctly.
  • Changed rendering_tester.dart to fail test by default when FlutterError catches errors. Previously, a test would pass in the presence of these errors.

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I signed the [CLA].
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (announcement)
  • No, this is not a breaking change.

What broke?

Layer.addToScene now returns void. Previously it returned EngineLayer. It is now the responsibility of the Layer implementations to create and manage engine layers. For example, a container layer that pushes opacity previously would do this:

  @override
  ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
    ui.EngineLayer newEngineLayer = builder.pushOpacity(alpha, offset: offset + layerOffset, oldLayer: _engineLayer);
    addChildrenToScene(builder);
    builder.pop();
    return newEngineLayer;
  }

After this change, it would change its signature to void and assign the layer using the engineLayer setter instead of returning it:

  @override
  void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
    engineLayer = builder.pushOpacity(alpha, offset: offset + layerOffset, oldLayer: _engineLayer);
    addChildrenToScene(builder);
    builder.pop();
  }

@fluttergithubbot fluttergithubbot added d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. c: contributor-productivity Team-specific productivity, code health, technical debt. customer: gallery Relating to flutter/gallery repository. Please transfer non-framework issues there. labels Jul 17, 2019
@yjbanov yjbanov requested review from Hixie, ferhatb and liyuqian July 17, 2019 22:51
Copy link
Contributor

@liyuqian liyuqian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only reviewed 4 files... Will resume reviewing tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. Some inherited classes may need to update their signatures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted the change. I only changed it because it didn't feel right. It puts the responsibility of setting the _engineLayer field on the caller of addToScene. This means that the caller always has to do this:

childLayer._engineLayer = childLayer.addToScene(builder);

There are two issues with this. One is that _engineLayer is private and so a custom layer implementation won't be able to do this correctly. The second is that the caller needs to remember to do this. They also need to make sure they do this correctly. I found a bug in _ProxyLayer that returned another layer's engine layer, which is wrong. It only manifested after my change because my change exercises the new assertions I added in the SceneBuilder API.

However, there's no rush to fix this. We can do this as a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remembered that we've had this conversation before. Maybe let's create a Github issue to track our discussions and decisions so we won't forget it again in a few months :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the document if the return type is changed to void.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted the change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we currently have use cases for engineLayer in Leader/FollowerLayers? I remember that I explicitly set engine layer to null because Leader/FollowerLayers break assumptions of the retained rendering. Hence I don't want someone to accidentally use retained rendering for them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, when a leader/follower layer is moving without changing its children, we want to reuse the engine layer DOM nodes associated with pushTransform.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Maybe add a comment somewhere like "The engine layer's resources may be reused through oldLayer, but do not reuse the engine layer's rasterized pixels through addRetained directly because there's no guarantee that the pixels would stay the same even if the whole subtree is unchanged."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's plenty of documentation on the oldLayer parameter about what it does. It might be overkill to document it again for every invocation of push* method, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specific to the Leader/FolllowerLayer. Previously, it's impossible to re-use the engineLayer of a Leader/FollowerLayer for addRetained because we return null.

Now we return the engineLayer for oldLayer, but we still don't want developers to use it for addRetained. I don't think that we need to document every push* method. Just documenting Leader/FollowerLayer.addToScene would be enough. Maybe something like "Only use the returned engineLayer for oldLayer in .... Do not use it for addRetained".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we also invalidate the layer's engineLayer at this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's layer-specific. The old engineLayer may still be needed to be passed as oldLayer during compositeFrame.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. So engineLayer could either be:

  1. in a state that's synchronized with the layer so we can directly use it in addRetained,
  2. in a stale state where one can only use it as oldLayer.

Is there some mechanism that we can enforce the developer to not misuse such two states of the engineLayer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added a bunch of assertions in the SceneBuilder in the engine code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LeaderLayer shouldn't even have an engine layer, should it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LeaderLayer sometimes creates a TransformEngineLayer:

builder.pushTransform(Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a breaking change as we're changing the return type. Maybe we can just assert(_layer is OffsetLayer) and return an OffsetLayer as this is just for backward compatibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Interesting idea. Since I'm deprecating it, might as well keep the same type. I'll give it a shot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@Hixie
Copy link
Contributor

Hixie commented Jul 22, 2019

cc @goderbauer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be protected. Nobody outside this object should be reading it as far as I can tell.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 41 unprotected usages. Most are in tests (which can be fixed by making it @protected @visibleForTesting). However, there are 18 usages in package:flutter_test, widget_inspector.dart, and rendering/binding.dart, that need this to remain public.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How many of these are for debug purposes (for which we could use a debugLayer) vs production?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to convert all but 5 to debugLayer. Here's the production code that uses layer:

rendering/binding.dart:

return renderView.layer.findAll<MouseTrackerAnnotation>(offset * window.devicePixelRatio);

multiple places in widget_inspector.dart (although I'm not sure if widget inspector is considered debug code):

data.containerLayer.append(_ProxyLayer(repaintBoundary.layer));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking the surrounding code in widget_inspector.dart it's all debug code, so I was able to convert it to debugLayer as well. So, the only remaining production usage of layer is the MouseTracker in rendering/binding.dart.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move that closure into a method on RenderView, and then pass that method in rather than using a closure. That'll be generally cleaner and will allow us to make this protected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want to remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because any render object can create and keep a layer the normal layer getter no longer requires isRepaintBoundary and !_needsPaint. So this getter no longer adds any value to tests. They can as successfully use the layer getter (in fact, I moved all our existing tests to it).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that this should still require needsPaint. All the more so now than before. Indeed the setter probably should have asserts too: we presumably should never change our layer except during paint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past it required !_needsPaint:

With this change it no longer requires it, because, as you say we're producing layers during paint. Are you suggesting that we should flip the requirement to require _needsPaint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we're clearing _needsPaint before calling paint, so requiring _needsPaint in the layer getter won't work:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that layer should require !needsPaint, sorry for the confusion above.

If we make layer protected I guess I'm not as worried about this. Though it sounds like we still want the debugLayer for those places outside of RenderObject that want to read the layer during development.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here and in other places, might it be better to just create the layer using oldLayer ??= PhysicalModelLayer() and then set the attributes unconditionally? Do the constructors do anything more efficient than that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't worried about efficiency as much as I was about API compatibility. Most layer constructors either have @required parameters, or they assume non-null arguments. I can change the constructors to allow blank objects. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can give them default values, or we can allow null and document they have to be non-null before adding to scene and add the relevant asserts (we have some cases like that already). I think the ergonomics will be nicer if we remove the @requireds and just have the one way of doing this, though. Having both ways is going to result in bugs when we accidentally only fix one code path.

Copy link
Contributor

@liyuqian liyuqian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining files scanned :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a null layer here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. It doesn't seem to me that layer could be null at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@HansMuller HansMuller removed the f: material design flutter/packages/flutter/material repository. label Jul 24, 2019
@dnfield dnfield removed d: examples Sample code and demos c: contributor-productivity Team-specific productivity, code health, technical debt. customer: gallery Relating to flutter/gallery repository. Please transfer non-framework issues there. labels Jul 24, 2019
@yjbanov yjbanov requested review from Hixie, ferhatb and liyuqian July 30, 2019 20:50
@yjbanov
Copy link
Contributor Author

yjbanov commented Jul 31, 2019

@liyuqian @Hixie, I responded to your comments. PTAL.

@Piinks Piinks added c: performance Relates to speed or footprint issues (see "perf:" labels) platform-web Web applications specifically labels Jul 31, 2019
@yjbanov yjbanov merged commit 34c6926 into flutter:master Aug 16, 2019
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

c: API break Backwards-incompatible API changes c: performance Relates to speed or footprint issues (see "perf:" labels) framework flutter/packages/flutter repository. See also f: labels. platform-web Web applications specifically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the layer API suitable for both Flutter for mobile and for web

9 participants