Skip to content

Conversation

@fzyzcjy
Copy link
Contributor

@fzyzcjy fzyzcjy commented Oct 21, 2022

  1. I will finish code details, refine code, add tests, make tests pass, etc, after a code review that thinks the rough idea is acceptable. It is because, from my past experience, reviews may request changing a lot. If the general idea is to be changed, all detailed implementation efforts are wasted :)
  2. The PR has an already-working counterpart, and it produces ~60FPS smooth experimental results. The benchmark results and detailed analysis is in chapter https://cjycode.com/flutter_smooth/benchmark/. All the source code is in https://github.com/fzyzcjy/engine/tree/flutter-smooth and https://github.com/fzyzcjy/flutter/tree/flutter-smooth.
  3. Possibly useful as a context to this PR, there is a whole chapter discussing the internals - how flutter_smooth is implemented. (Link: https://cjycode.com/flutter_smooth/design/)

Close #114002

We all know that, Flutter allows us to create our own BuildOwner and PipelineOwner, and here is even an official example: https://github.com/flutter/flutter/blob/master/examples/api/lib/widgets/framework/build_owner.0.dart. The doc also agrees with that. For example:

You can create other pipeline owners to manage off-screen objects, which can flush their pipelines independently of the on-screen render objects. (https://api.flutter.dev/flutter/rendering/PipelineOwner-class.html)

And

Additional build owners can be built to manage off-screen widget trees. (https://api.flutter.dev/flutter/widgets/BuildOwner-class.html)

Therefore, theoretically, we should be able to happily use our own BuildOwner and PipelineOwner anywhere freely. However, it has a bug as follows: If I call pipelineOwner.flushPaint(); (and sibling methods) inside the layout phase of the main PipelineOwner, then I get an assertion error in debug mode.

The root cause is that, even though the self-managed PipelineOwner is isolated from the flutter-managed PipelineOwner, the debug variable RenderObject.debugActiveLayout is shared. Therefore, when calling flushPaint on self-manged PipelineOwner within a call of flushLayout of flutter-managed PipelineOwner, the assertions get confused and wrongly throws.

My hack can be seen in https://github.com/fzyzcjy/flutter_smooth/blob/0c5db0ff270aa0c8cff28ea19055999627a8df6d/packages/smooth/lib/src/infra/auxiliary_tree_pack.dart#L214. Copy it here for completeness:

...
        _temporarilyRemoveDebugActiveLayout(() {
          pipelineOwner.flushPaint();
        });
...

void _temporarilyRemoveDebugActiveLayout(VoidCallback f) {
  // NOTE we have to temporarily remove debugActiveLayout
  // b/c [SecondTreeRootView.paint] is called inside [preemptRender]
  // which is inside main tree's build/layout.
  // thus, if not set it to null we will see error
  // https://github.com/fzyzcjy/yplusplus/issues/5783#issuecomment-1254974511
  // In short, this is b/c [debugActiveLayout] is global variable instead
  // of per-tree variable
  // and also
  // https://github.com/fzyzcjy/yplusplus/issues/5793#issuecomment-1256095858
  final oldDebugActiveLayout = RenderObject.debugActiveLayout;
  RenderObject.debugActiveLayout = null;
  try {
    f();
  } finally {
    RenderObject.debugActiveLayout = oldDebugActiveLayout;
  }
}

However, for this to work, the debugActiveLayout setter must be public.

The most naive solution is to make it public, but that may violate encapsulation. Thus, in the proposed PR, I create a method to wrap that.

Reproduction code and output

Details
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('When call PipelineOwner.flushPaint inside another PipelineOwner.flushLayout', (tester) async {
    int onPerformLayoutCount = 0;
    await tester.pumpWidget(_SpyLayoutBuilder(onPerformLayout: () {
      onPerformLayoutCount++;

      const Widget widget = ColoredBox(color: Colors.green, child: SizedBox(width: 100, height: 100));

      // mimic https://github.com/flutter/flutter/blob/master/examples/api/lib/widgets/framework/build_owner.0.dart
      final PipelineOwner pipelineOwner = PipelineOwner();
      final MeasurementView rootView = pipelineOwner.rootNode = MeasurementView();
      final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
      final RenderObjectToWidgetElement<RenderBox> element = RenderObjectToWidgetAdapter<RenderBox>(
        container: rootView,
        debugShortDescription: '[root]',
        child: widget,
      ).attachToRenderTree(buildOwner);

      rootView.scheduleInitialLayout();
      rootView.scheduleInitialPaint(TransformLayer(transform: Matrix4.identity())..attach(rootView));
      buildOwner.buildScope(element);
      pipelineOwner.flushLayout();
      pipelineOwner.flushPaint();
    }));

    expect(onPerformLayoutCount, 1);
  });
}

class MeasurementView extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
  @override
  void performLayout() {
    assert(child != null);
    child!.layout(const BoxConstraints(), parentUsesSize: true);
    size = child!.size;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    print('hi ${describeIdentity(this)}.paint');
    context.paintChild(child!, offset);
  }

  @override
  bool get isRepaintBoundary => true;

  @override
  Rect get paintBounds => Offset.zero & size;

  @override
  void debugAssertDoesMeetConstraints() => true;
}

class _SpyLayoutBuilder extends SingleChildRenderObjectWidget {
  final VoidCallback onPerformLayout;

  const _SpyLayoutBuilder({required this.onPerformLayout});

  @override
  _RenderSpyLayoutBuilder createRenderObject(BuildContext context) => _RenderSpyLayoutBuilder(
        onPerformLayout: onPerformLayout,
      );

  @override
  void updateRenderObject(BuildContext context, _RenderSpyLayoutBuilder renderObject) {
    renderObject.onPerformLayout = onPerformLayout;
  }
}

class _RenderSpyLayoutBuilder extends RenderProxyBox {
  _RenderSpyLayoutBuilder({
    required this.onPerformLayout,
    RenderBox? child,
  }) : super(child);

  VoidCallback onPerformLayout;

  @override
  void performLayout() {
    super.performLayout();
    onPerformLayout();
  }
}

yields

Details
00:08 +0: When call PipelineOwner.flushPaint inside another PipelineOwner.flushLayout                                                  
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown during performLayout():
RenderBox.size accessed beyond the scope of resize, layout, or permitted parent access. RenderBox
can always access its own size, otherwise, the only object that is allowed to read RenderBox.size is
its parent, if they have said they will. It you hit this assert trying to access a child's size,
pass "parentUsesSize: true" to that child's layout().
'package:flutter/src/rendering/box.dart':
Failed assertion: line 2009 pos 13: 'debugDoingThisResize || debugDoingThisLayout ||
_computingThisDryLayout ||
              (RenderObject.debugActiveLayout == parent && size._canBeUsedByParent)'

Either the assertion indicates an error in the framework itself, or we should provide substantially
more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.md

The relevant error-causing widget was:
  _SpyLayoutBuilder
  _SpyLayoutBuilder:file:///Users/tom/Main/yplusplus/frontend/yplusplus/test/a.dart:9:29

When the exception was thrown, this was the stack:
#2      RenderBox.size.<anonymous closure> (package:flutter/src/rendering/box.dart:2009:13)
#3      RenderBox.size (package:flutter/src/rendering/box.dart:2022:6)
#4      MeasurementView.paintBounds (file:///Users/tom/Main/yplusplus/frontend/yplusplus/test/a.dart:53:41)
#5      PaintingContext._repaintCompositedChild (package:flutter/src/rendering/object.dart:154:56)
#6      PaintingContext.repaintCompositedChild (package:flutter/src/rendering/object.dart:98:5)
#7      PipelineOwner.flushPaint (package:flutter/src/rendering/object.dart:1116:31)
#8      main.<anonymous closure>.<anonymous closure> (file:///Users/tom/Main/yplusplus/frontend/yplusplus/test/a.dart:28:21)
#9      _RenderSpyLayoutBuilder.performLayout (file:///Users/tom/Main/yplusplus/frontend/yplusplus/test/a.dart:86:20)
#10     RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7)
#11     RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11)
#12     RenderView.performLayout (package:flutter/src/rendering/view.dart:170:14)
#13     RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1973:7)
#14     PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:999:18)
#15     AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1194:23)
#16     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5)
#17     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15)
#18     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1104:9)
#19     AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1057:9)
#22     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#23     AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1043:27)
#24     WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:554:22)
#27     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#28     WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:551:27)
#29     main.<anonymous closure> (file:///Users/tom/Main/yplusplus/frontend/yplusplus/test/a.dart:9:18)
#30     testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:171:29)
<asynchronous suspension>
<asynchronous suspension>
(elided 7 frames from class _AssertionError, dart:async, and package:stack_trace)

The following RenderObject was being processed when the exception was fired: _RenderSpyLayoutBuilder#f19a8:
  creator: _SpyLayoutBuilder ← [root]
  parentData: <none>
  constraints: BoxConstraints(w=800.0, h=600.0)
  size: Size(800.0, 600.0)
This RenderObject has no descendants.
════════════════════════════════════════════════════════════════════════════════════════════════════
00:08 +0 -1: When call PipelineOwner.flushPaint inside another PipelineOwner.flushLayout [E]                                           
  Test failed. See exception logs above.
  The test description was: When call PipelineOwner.flushPaint inside another PipelineOwner.flushLayout
  

To run this test again: /Users/tom/fvm/versions/3.3.5/bin/cache/dart-sdk/bin/dart test /Users/tom/Main/yplusplus/frontend/yplusplus/test/a.dart -p vm --plain-name 'When call PipelineOwner.flushPaint inside another PipelineOwner.flushLayout'
00:08 +0 -1: Some tests failed.                                  

Performance overhead

Using compiler explorer, we can see that it does not generate worse assembly (as long as we use the prefer-inline pragma)

https://godbolt.org/z/EoznoWex7

image

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt. -- see above
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard
Copy link

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.

@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label Oct 21, 2022
static RenderObject? get debugActiveLayout => _debugActiveLayout;
static RenderObject? _debugActiveLayout;

/// Set [debugActiveLayout] to null when [inner] callback is called.
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this change the same as #114003?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Almost same (as mentioned there), but with one more thing:

This PR makes this function public instead of private. Then, users of the secondary pipeline owners can call that function. If only have that PR, then cannot.

@goderbauer
Copy link
Member

Can you update this PR with the latest master to make sure what is actually changed by this?

# Conflicts:
#	packages/flutter/lib/src/rendering/object.dart
@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Nov 1, 2022

Done merging (wait for ci though, but the idea is clear)

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Nov 2, 2022

ping :) @CaseyHillers, since

If you still see failures, feel free to ping a Googler for help. You're welcome to ping me on any PRs (my GitHub is @CaseyHillers) and I'll take a look. There's an internal switch to mark it as passing as if rebasing isn't working, it likely indicates a separate PR is causing the failures. https://discord.com/channels/608014603317936148/608018585025118217/1037051780443414628

@CaseyHillers
Copy link
Contributor

ping :) @CaseyHillers

You can ignore this failure for now. Google Testing can only run if a Flutter hacker approves your PR. Internally, it says there's no LGTMs, and it marks the status as failed (there's an internal tracking bug for making this status show pending instead of failing)

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Nov 2, 2022

@CaseyHillers Thanks, get it

@goderbauer goderbauer requested a review from chunhtai November 15, 2022 23:24
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

Since the layout/paint cycle is managed by pipeline owner, should the _debugActiveLayout property in the PipelineOwner instead? that way we don't need to open up dangerous API to clear the value.

@fzyzcjy
Copy link
Contributor Author

fzyzcjy commented Nov 30, 2022

Not very sure, it was at RenderObject previously so I think there may be some design reasons?

@chunhtai
Copy link
Contributor

Not very sure, it was at RenderObject previously so I think there may be some design reasons?

Since this will may also affect multi window, so cc @goderbauer. I think the current logic(that it uses static variable on RenderObject) is wrong it basically eliminate the possibility to have two pipelineowners to run their cycle in parallel. This pr feels like a bandage and may be hard to maintain

@goderbauer
Copy link
Member

I agree with @chunhtai here. We would need to design a more maintainable solution around this. I am going to close this one, looks like we already have an issue on file for this problem.

@goderbauer goderbauer closed this Jan 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RenderBox.size accessed beyond the scope of ... when using custom PipelineOwner

4 participants