Unverified Commit 12f54e17 authored by Dan Field's avatar Dan Field Committed by GitHub

reland List queue search optimization (#68214)

* Revert "Revert "Improve performance of collectAllElements (#68065)" (#68207)"

This reverts commit 46ff57d6.

* use fewer elements for benchmark
parent 0f886448
// 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/material.dart';
import 'package:flutter/scheduler.dart';
// ignore: implementation_imports
import 'package:flutter_test/src/all_elements.dart';
import '../common.dart';
const int _kNumIters = 10000;
Future<void> main() async {
assert(false, "Don't run benchmarks in debug mode! Use 'flutter run --release'.");
runApp(MaterialApp(
home: Scaffold(
body: GridView.count(
crossAxisCount: 5,
children: List<Widget>.generate(25, (int index) {
return Center(
child: Scaffold(
appBar: AppBar(
title: Text('App $index'),
actions: const <Widget>[
Icon(Icons.help),
Icon(Icons.add),
Icon(Icons.ac_unit),
],
),
body: Column(
children: const <Widget>[
Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
Text('Item 4'),
],
),
),
);
}),
),
),
));
await SchedulerBinding.instance.endOfFrame;
final Stopwatch watch = Stopwatch();
print('flutter_test allElements benchmark... (${WidgetsBinding.instance.renderViewElement})');
// Make sure we get enough elements to process for consistent benchmark runs
int elementCount = collectAllElementsFrom(WidgetsBinding.instance.renderViewElement, skipOffstage: false).length;
while (elementCount < 2482) {
await Future<void>.delayed(Duration.zero);
elementCount = collectAllElementsFrom(WidgetsBinding.instance.renderViewElement, skipOffstage: false).length;
}
print('element count: $elementCount');
watch.start();
for (int i = 0; i < _kNumIters; i += 1) {
final List<Element> allElements = collectAllElementsFrom(
WidgetsBinding.instance.renderViewElement,
skipOffstage: false,
).toList();
allElements.clear();
}
watch.stop();
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
printer.addResult(
description: 'All elements iterate',
value: watch.elapsedMicroseconds / _kNumIters,
unit: 'µs per iteration',
name: 'all_elements_iteration',
);
printer.printToStdout();
}
...@@ -55,6 +55,7 @@ TaskFunction createMicrobenchmarkTask() { ...@@ -55,6 +55,7 @@ TaskFunction createMicrobenchmarkTask() {
...await _runMicrobench('lib/stocks/animation_bench.dart'), ...await _runMicrobench('lib/stocks/animation_bench.dart'),
...await _runMicrobench('lib/language/sync_star_bench.dart'), ...await _runMicrobench('lib/language/sync_star_bench.dart'),
...await _runMicrobench('lib/language/sync_star_semantics_bench.dart'), ...await _runMicrobench('lib/language/sync_star_semantics_bench.dart'),
...await _runMicrobench('lib/foundation/all_elements_bench.dart'),
...await _runMicrobench('lib/foundation/change_notifier_bench.dart'), ...await _runMicrobench('lib/foundation/change_notifier_bench.dart'),
}; };
......
...@@ -23,15 +23,39 @@ Iterable<Element> collectAllElementsFrom( ...@@ -23,15 +23,39 @@ Iterable<Element> collectAllElementsFrom(
return CachingIterable<Element>(_DepthFirstChildIterator(rootElement, skipOffstage)); return CachingIterable<Element>(_DepthFirstChildIterator(rootElement, skipOffstage));
} }
/// Provides a recursive, efficient, depth first search of an element tree.
///
/// [Element.visitChildren] does not guarnatee order, but does guarnatee stable
/// order. This iterator also guarantees stable order, and iterates in a left
/// to right order:
///
/// 1
/// / \
/// 2 3
/// / \ / \
/// 4 5 6 7
///
/// Will iterate in order 2, 4, 5, 3, 6, 7.
///
/// Performance is important here because this method is on the critical path
/// for flutter_driver and package:integration_test performance tests.
/// Performance is measured in the all_elements_bench microbenchmark.
/// Any changes to this implementation should check the before and after numbers
/// on that benchmark to avoid regressions in general performance test overhead.
///
/// If we could use RTL order, we could save on performance, but numerous tests
/// have been written (and developers clearly expect) that LTR order will be
/// respected.
class _DepthFirstChildIterator implements Iterator<Element> { class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement, this.skipOffstage) _DepthFirstChildIterator(Element rootElement, this.skipOffstage) {
: _stack = _reverseChildrenOf(rootElement, skipOffstage).toList(); _fillChildren(rootElement);
}
final bool skipOffstage; final bool skipOffstage;
late Element _current; late Element _current;
final List<Element> _stack; final List<Element> _stack = <Element>[];
@override @override
Element get current => _current; Element get current => _current;
...@@ -42,20 +66,26 @@ class _DepthFirstChildIterator implements Iterator<Element> { ...@@ -42,20 +66,26 @@ class _DepthFirstChildIterator implements Iterator<Element> {
return false; return false;
_current = _stack.removeLast(); _current = _stack.removeLast();
// Stack children in reverse order to traverse first branch first _fillChildren(_current);
_stack.addAll(_reverseChildrenOf(_current, skipOffstage));
return true; return true;
} }
static Iterable<Element> _reverseChildrenOf(Element element, bool skipOffstage) { void _fillChildren(Element element) {
assert(element != null); assert(element != null);
final List<Element> children = <Element>[]; // If we did not have to follow LTR order and could instead use RTL,
// we could avoid reversing this and the operation would be measurably
// faster. Unfortunately, a lot of tests depend on LTR order.
final List<Element> reversed = <Element>[];
if (skipOffstage) { if (skipOffstage) {
element.debugVisitOnstageChildren(children.add); element.debugVisitOnstageChildren(reversed.add);
} else { } else {
element.visitChildren(children.add); element.visitChildren(reversed.add);
}
// This is faster than _stack.addAll(reversed.reversed), presumably since
// we don't actually care about maintaining an iteration pointer.
while (reversed.isNotEmpty) {
_stack.add(reversed.removeLast());
} }
return children.reversed;
} }
} }
// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('collectAllElements goes in LTR DFS', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(Directionality(
key: key,
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
RichText(text: const TextSpan(text: 'a')),
RichText(text: const TextSpan(text: 'b')),
],
),
));
final List<Element> elements = collectAllElementsFrom(
key.currentContext! as Element,
skipOffstage: false,
).toList();
expect(elements.length, 3);
expect(elements[0].widget, isA<Row>());
expect(elements[1].widget, isA<RichText>());
expect(((elements[1].widget as RichText).text as TextSpan).text, 'a');
expect(elements[2].widget, isA<RichText>());
expect(((elements[2].widget as RichText).text as TextSpan).text, 'b');
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment