From 02b300d99e729cfb59a282bb1b71dc2839c22eae Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Fri, 29 Apr 2022 16:41:37 -0700 Subject: [PATCH] Revert "delete fast reassemble code (#102842)" (#102856) This reverts commit ec8693e80bf2c9f31c06d9832ebb4bccc5274440. --- .../flutter/lib/src/foundation/binding.dart | 27 ++++ .../flutter/lib/src/rendering/binding.dart | 16 ++- packages/flutter/lib/src/widgets/binding.dart | 15 ++- .../flutter/lib/src/widgets/framework.dart | 21 ++- .../lib/src/widgets/widget_inspector.dart | 2 +- .../test/widgets/fast_reassemble_test.dart | 123 ++++++++++++++++++ .../widgets/widget_inspector_test_utils.dart | 2 +- 7 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 packages/flutter/test/widgets/fast_reassemble_test.dart diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart index 4be9bb0e06..b40934bf73 100644 --- a/packages/flutter/lib/src/foundation/binding.dart +++ b/packages/flutter/lib/src/foundation/binding.dart @@ -157,6 +157,13 @@ abstract class BindingBase { static Type? _debugInitializedType; static bool _debugServiceExtensionsRegistered = false; + /// Additional configuration used by the framework during hot reload. + /// + /// See also: + /// + /// * [DebugReassembleConfig], which describes the configuration. + static DebugReassembleConfig? debugReassembleConfig; + /// The main window to which this binding is bound. /// /// A number of additional bindings are defined as extensions of @@ -871,3 +878,23 @@ abstract class BindingBase { Future _exitApplication() async { exit(0); } + +/// Additional configuration used for hot reload reassemble optimizations. +/// +/// Do not extend, implement, or mixin this class. This may only be instantiated +/// in debug mode. +class DebugReassembleConfig { + /// Create a new [DebugReassembleConfig]. + /// + /// Throws a [FlutterError] if this is called in profile or release mode. + DebugReassembleConfig({ + this.widgetName, + }) { + if (!kDebugMode) { + throw FlutterError('Cannot instantiate DebugReassembleConfig in profile or release mode.'); + } + } + + /// The name of the widget that was modified, or `null` if the change was elsewhere. + final String? widgetName; +} diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index a55ef98424..ed1a0e28f5 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -514,14 +514,16 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @override Future performReassemble() async { await super.performReassemble(); - if (!kReleaseMode) { - Timeline.startSync('Preparing Hot Reload (layout)'); - } - try { - renderView.reassemble(); - } finally { + if (BindingBase.debugReassembleConfig?.widgetName == null) { if (!kReleaseMode) { - Timeline.finishSync(); + Timeline.startSync('Preparing Hot Reload (layout)'); + } + try { + renderView.reassemble(); + } finally { + if (!kReleaseMode) { + Timeline.finishSync(); + } } } scheduleWarmUpFrame(); diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 46d8b2aa45..6ba28b0f76 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -412,7 +412,16 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB registerServiceExtension( name: 'fastReassemble', callback: (Map params) async { - await reassembleApplication(); + // This mirrors the implementation of the 'reassemble' callback registration + // in lib/src/foundation/binding.dart, but with the extra binding config used + // to skip some reassemble work. + final String? className = params['className'] as String?; + BindingBase.debugReassembleConfig = DebugReassembleConfig(widgetName: className); + try { + await reassembleApplication(); + } finally { + BindingBase.debugReassembleConfig = null; + } return {'type': 'Success'}; }, ); @@ -469,7 +478,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB Future _forceRebuild() { if (renderViewElement != null) { - buildOwner!.reassemble(renderViewElement!); + buildOwner!.reassemble(renderViewElement!, null); return endOfFrame; } return Future.value(); @@ -936,7 +945,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB }()); if (renderViewElement != null) { - buildOwner!.reassemble(renderViewElement!); + buildOwner!.reassemble(renderViewElement!, BindingBase.debugReassembleConfig); } return super.performReassemble(); } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index d14466daf3..cc42fc59f6 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -3028,13 +3028,14 @@ class BuildOwner { /// changed implementations. /// /// This is expensive and should not be called except during development. - void reassemble(Element root) { + void reassemble(Element root, DebugReassembleConfig? reassembleConfig) { if (!kReleaseMode) { Timeline.startSync('Preparing Hot Reload (widgets)'); } try { assert(root._parent == null); assert(root.owner == this); + root._debugReassembleConfig = reassembleConfig; root.reassemble(); } finally { if (!kReleaseMode) { @@ -3142,6 +3143,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { _widget = widget; Element? _parent; + DebugReassembleConfig? _debugReassembleConfig; _NotificationNode? _notificationTree; /// Compare two widgets for equality. @@ -3271,10 +3273,15 @@ abstract class Element extends DiagnosticableTree implements BuildContext { @mustCallSuper @protected void reassemble() { - markNeedsBuild(); + if (_debugShouldReassemble(_debugReassembleConfig, _widget)) { + markNeedsBuild(); + _debugReassembleConfig = null; + } visitChildren((Element child) { + child._debugReassembleConfig = _debugReassembleConfig; child.reassemble(); }); + _debugReassembleConfig = null; } bool _debugIsInScope(Element target) { @@ -4918,7 +4925,9 @@ class StatefulElement extends ComponentElement { @override void reassemble() { - state.reassemble(); + if (_debugShouldReassemble(_debugReassembleConfig, _widget)) { + state.reassemble(); + } super.reassemble(); } @@ -6454,3 +6463,9 @@ class _NullWidget extends Widget { @override Element createElement() => throw UnimplementedError(); } + +// Whether a [DebugReassembleConfig] indicates that an element holding [widget] can skip +// a reassemble. +bool _debugShouldReassemble(DebugReassembleConfig? config, Widget? widget) { + return config == null || config.widgetName == null || widget?.runtimeType.toString() == config.widgetName; +} diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index cfd1a2873e..690d3f8ae5 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -911,7 +911,7 @@ mixin WidgetInspectorService { Future forceRebuild() { final WidgetsBinding binding = WidgetsBinding.instance; if (binding.renderViewElement != null) { - binding.buildOwner!.reassemble(binding.renderViewElement!); + binding.buildOwner!.reassemble(binding.renderViewElement!, null); return binding.endOfFrame; } return Future.value(); diff --git a/packages/flutter/test/widgets/fast_reassemble_test.dart b/packages/flutter/test/widgets/fast_reassemble_test.dart new file mode 100644 index 0000000000..128b5a561a --- /dev/null +++ b/packages/flutter/test/widgets/fast_reassemble_test.dart @@ -0,0 +1,123 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('reassemble with a className only marks subtrees from the first matching element as dirty', (WidgetTester tester) async { + await tester.pumpWidget( + const Foo(Bar(Fizz(SizedBox()))) + ); + + expect(Foo.count, 0); + expect(Bar.count, 0); + expect(Fizz.count, 0); + + DebugReassembleConfig config = DebugReassembleConfig(widgetName: 'Bar'); + WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.renderViewElement!, config); + + expect(Foo.count, 0); + expect(Bar.count, 1); + expect(Fizz.count, 1); + + config = DebugReassembleConfig(widgetName: 'Fizz'); + WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.renderViewElement!, config); + + expect(Foo.count, 0); + expect(Bar.count, 1); + expect(Fizz.count, 2); + + config = DebugReassembleConfig(widgetName: 'NoMatch'); + WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.renderViewElement!, config); + + expect(Foo.count, 0); + expect(Bar.count, 1); + expect(Fizz.count, 2); + + config = DebugReassembleConfig(); + WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.renderViewElement!, config); + + expect(Foo.count, 1); + expect(Bar.count, 2); + expect(Fizz.count, 3); + + WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.renderViewElement!, null); + + expect(Foo.count, 2); + expect(Bar.count, 3); + expect(Fizz.count, 4); + }); +} + +class Foo extends StatefulWidget { + const Foo(this.child, {super.key}); + + final Widget child; + static int count = 0; + + @override + State createState() => _FooState(); +} + +class _FooState extends State { + @override + void reassemble() { + Foo.count += 1; + super.reassemble(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} + + +class Bar extends StatefulWidget { + const Bar(this.child, {super.key}); + + final Widget child; + static int count = 0; + + @override + State createState() => _BarState(); +} + +class _BarState extends State { + @override + void reassemble() { + Bar.count += 1; + super.reassemble(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +class Fizz extends StatefulWidget { + const Fizz(this.child, {super.key}); + + final Widget child; + static int count = 0; + + @override + State createState() => _FizzState(); +} + +class _FizzState extends State { + @override + void reassemble() { + Fizz.count += 1; + super.reassemble(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/packages/flutter/test/widgets/widget_inspector_test_utils.dart b/packages/flutter/test/widgets/widget_inspector_test_utils.dart index 55c206e112..00e99ee09b 100644 --- a/packages/flutter/test/widgets/widget_inspector_test_utils.dart +++ b/packages/flutter/test/widgets/widget_inspector_test_utils.dart @@ -59,7 +59,7 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { final WidgetsBinding binding = WidgetsBinding.instance; if (binding.renderViewElement != null) { - binding.buildOwner!.reassemble(binding.renderViewElement!); + binding.buildOwner!.reassemble(binding.renderViewElement!, null); } }