Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 8cd540f

Browse files
authored
[performance] Process dirty nodes from top to bottom during paint to avoid unnecessary layer tree walks (#98219)
1 parent ae6ace3 commit 8cd540f

File tree

2 files changed

+87
-8
lines changed

2 files changed

+87
-8
lines changed

packages/flutter/lib/src/rendering/object.dart

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -987,8 +987,7 @@ class PipelineOwner {
987987
try {
988988
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
989989
_nodesNeedingPaint = <RenderObject>[];
990-
// Sort the dirty nodes in reverse order (deepest first).
991-
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
990+
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
992991
assert(node._layerHandle.layer != null);
993992
if (node._needsPaint && node.owner == this) {
994993
if (node._layerHandle.layer!.attached) {
@@ -2365,12 +2364,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
23652364
return true;
23662365
}());
23672366
// If we still need layout, then that means that we were skipped in the
2368-
// layout phase and therefore don't need painting. We might not know that
2369-
// yet (that is, our layer might not have been detached yet), because the
2370-
// same node that skipped us in layout is above us in the tree (obviously)
2371-
// and therefore may not have had a chance to paint yet (since the tree
2372-
// paints in reverse order). In particular this will happen if they have
2373-
// a different layer, because there's a repaint boundary between us.
2367+
// layout phase and therefore don't need painting. This can happen when an
2368+
// error occurred prior to our paint and our layer wasn't properly detached.
23742369
if (_needsLayout)
23752370
return;
23762371
if (!kReleaseMode && debugProfilePaintsEnabled) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/rendering.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
import 'rendering_tester.dart';
9+
10+
void main() {
11+
TestRenderingFlutterBinding.ensureInitialized();
12+
13+
test('paint RenderObjects from top to bottom to avoid duplicated layer tree walks', () {
14+
// Historic Background: The order in which dirty nodes are processed for paint
15+
// in [PipelineOwner.flushPaint] was changed from shallowest first to
16+
// deepest first in August 2015, see https://github.com/flutter/flutter/commit/654fc7346eb79780aadeb9c2883ea9938d5f0bd3#diff-d06e00032e4d722205a2189ffbab26c1d8f5e13652efebc849583d0a1359fec9R612.
17+
// The reasons for this change are lost in history. In February 2022 it
18+
// was determined that deepest first actually caused additional unnecessary
19+
// walks of the layer tree. To avoid those, the processing order was changed
20+
// back to deepest first (which is also the order used by all other flush-methods on
21+
// PipelineOwner). The test below encodes that the framework is not doing
22+
// these unnecessary layer tree walks during paint that would occur during
23+
// deepest first processing.
24+
25+
late RenderPositionedBox outer, inner;
26+
late TestRenderBox testBox;
27+
28+
outer = RenderPositionedBox(
29+
child: RenderRepaintBoundary(
30+
child: inner = RenderPositionedBox(
31+
child: testBox = TestRenderBox(),
32+
),
33+
),
34+
);
35+
36+
// Paint the tree for the first time; Our TestLayer is attached exactly once.
37+
expect((testBox.debugLayer! as TestLayer).attachCount, 0);
38+
expect((testBox.debugLayer! as TestLayer).detachCount, 0);
39+
layout(outer, phase: EnginePhase.paint);
40+
expect((testBox.debugLayer! as TestLayer).attachCount, 1);
41+
expect((testBox.debugLayer! as TestLayer).detachCount, 0);
42+
43+
// Repaint RenderObjects outside and inside the RepaintBoundary.
44+
outer.markNeedsPaint();
45+
inner.markNeedsPaint();
46+
expect((testBox.debugLayer! as TestLayer).attachCount, 1);
47+
expect((testBox.debugLayer! as TestLayer).detachCount, 0);
48+
pumpFrame(phase: EnginePhase.paint);
49+
50+
// The TestLayer should be detached and reattached exactly once during the
51+
// paint process. More attach/detach would indicate unnecessary
52+
// additional walks of the layer tree.
53+
expect((testBox.debugLayer! as TestLayer).attachCount, 2);
54+
expect((testBox.debugLayer! as TestLayer).detachCount, 1);
55+
});
56+
}
57+
58+
class TestRenderBox extends RenderProxyBoxWithHitTestBehavior {
59+
TestRenderBox() {
60+
layer = TestLayer();
61+
}
62+
63+
@override
64+
void paint(PaintingContext context, Offset offset) {
65+
context.addLayer(layer!);
66+
}
67+
}
68+
69+
class TestLayer extends OffsetLayer {
70+
int attachCount = 0;
71+
int detachCount = 0;
72+
73+
@override
74+
void attach(Object owner) {
75+
super.attach(owner);
76+
attachCount++;
77+
}
78+
79+
@override
80+
void detach() {
81+
super.detach();
82+
detachCount++;
83+
}
84+
}

0 commit comments

Comments
 (0)