diff --git a/dev/benchmarks/microbenchmarks/lib/foundation/all_elements_bench.dart b/dev/benchmarks/microbenchmarks/lib/foundation/all_elements_bench.dart new file mode 100644 index 0000000000..6d9148b4a9 --- /dev/null +++ b/dev/benchmarks/microbenchmarks/lib/foundation/all_elements_bench.dart @@ -0,0 +1,77 @@ +// 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 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.generate(100, (int index) { + return Center( + child: Scaffold( + appBar: AppBar( + title: Text('App $index'), + actions: const [ + Icon(Icons.help), + Icon(Icons.add), + Icon(Icons.ac_unit), + ], + ), + body: Column( + children: const [ + 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 < 6242) { + await Future.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 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(); +} diff --git a/dev/devicelab/lib/tasks/microbenchmarks.dart b/dev/devicelab/lib/tasks/microbenchmarks.dart index 889dea1843..f6f1024abb 100644 --- a/dev/devicelab/lib/tasks/microbenchmarks.dart +++ b/dev/devicelab/lib/tasks/microbenchmarks.dart @@ -55,6 +55,7 @@ TaskFunction createMicrobenchmarkTask() { ...await _runMicrobench('lib/stocks/animation_bench.dart'), ...await _runMicrobench('lib/language/sync_star_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'), }; diff --git a/packages/flutter_test/lib/src/all_elements.dart b/packages/flutter_test/lib/src/all_elements.dart index 9d0db74eee..a191041ef3 100644 --- a/packages/flutter_test/lib/src/all_elements.dart +++ b/packages/flutter_test/lib/src/all_elements.dart @@ -23,15 +23,39 @@ Iterable collectAllElementsFrom( return CachingIterable(_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 { - _DepthFirstChildIterator(Element rootElement, this.skipOffstage) - : _stack = _reverseChildrenOf(rootElement, skipOffstage).toList(); + _DepthFirstChildIterator(Element rootElement, this.skipOffstage) { + _fillChildren(rootElement); + } final bool skipOffstage; late Element _current; - final List _stack; + final List _stack = []; @override Element get current => _current; @@ -42,20 +66,26 @@ class _DepthFirstChildIterator implements Iterator { return false; _current = _stack.removeLast(); - // Stack children in reverse order to traverse first branch first - _stack.addAll(_reverseChildrenOf(_current, skipOffstage)); + _fillChildren(_current); return true; } - static Iterable _reverseChildrenOf(Element element, bool skipOffstage) { + void _fillChildren(Element element) { assert(element != null); - final List children = []; + // 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 reversed = []; if (skipOffstage) { - element.debugVisitOnstageChildren(children.add); + element.debugVisitOnstageChildren(reversed.add); } 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; } } diff --git a/packages/flutter_test/test/all_elements_test.dart b/packages/flutter_test/test/all_elements_test.dart new file mode 100644 index 0000000000..edb925a95a --- /dev/null +++ b/packages/flutter_test/test/all_elements_test.dart @@ -0,0 +1,34 @@ +// 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: [ + RichText(text: const TextSpan(text: 'a')), + RichText(text: const TextSpan(text: 'b')), + ], + ), + )); + + final List elements = collectAllElementsFrom( + key.currentContext! as Element, + skipOffstage: false, + ).toList(); + + expect(elements.length, 3); + expect(elements[0].widget, isA()); + expect(elements[1].widget, isA()); + expect(((elements[1].widget as RichText).text as TextSpan).text, 'a'); + expect(elements[2].widget, isA()); + expect(((elements[2].widget as RichText).text as TextSpan).text, 'b'); + }); +}