Skip to content

Commit c4ad1dc

Browse files
authored
const vs. non-const widget build benchmark (flutter#148261)
This benchmark illustrates the impact that const'ness has on widget build times by building a widget tree with all const-widgets and then building the same widget tree without any const'ness. Local testing with this benchmark running on a Moto G4 shows that const'ness is 13-16% faster. I'd like to check this benchmark in because the question of how important widget const'ness is comes up every so often. With this benchmark we have up-to-date data to point people to.
1 parent 54830cf commit c4ad1dc

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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/material.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
import '../common.dart';
9+
10+
const Duration kBenchmarkTime = Duration(seconds: 15);
11+
12+
Future<List<double>> runBuildBenchmark(ValueGetter<Widget> buildApp) async {
13+
assert(false, "Don't run benchmarks in debug mode! Use 'flutter run --release'.");
14+
15+
// We control the framePolicy below to prevent us from scheduling frames in
16+
// the engine, so that the engine does not interfere with our timings.
17+
final LiveTestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as LiveTestWidgetsFlutterBinding;
18+
19+
final Stopwatch watch = Stopwatch();
20+
int iterations = 0;
21+
final List<double> values = <double>[];
22+
23+
await benchmarkWidgets((WidgetTester tester) async {
24+
await tester.pumpWidget(buildApp());
25+
await tester.pump();
26+
await tester.pumpAndSettle(const Duration(seconds: 1));
27+
28+
// Cannot use expects/asserts here since this is running outside of a test
29+
// in release mode.
30+
final int numberOfSizedBoxes = find.byType(SizedBox).evaluate().length;
31+
if (numberOfSizedBoxes != 30) {
32+
throw StateError('Expected 30 SizedBox widgets, but only found $numberOfSizedBoxes.');
33+
}
34+
if (find.text('testToken').evaluate().length != 1) {
35+
throw StateError('Did not find expected leaf widget.');
36+
}
37+
38+
final Element rootWidget = tester.element(find.byKey(rootKey));
39+
Duration elapsed = Duration.zero;
40+
41+
final LiveTestWidgetsFlutterBindingFramePolicy defaultPolicy = binding.framePolicy;
42+
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark;
43+
44+
while (elapsed < kBenchmarkTime) {
45+
watch.reset();
46+
watch.start();
47+
rootWidget.markNeedsBuild();
48+
await tester.pumpBenchmark(Duration(milliseconds: iterations * 16));
49+
watch.stop();
50+
iterations += 1;
51+
elapsed += Duration(microseconds: watch.elapsedMicroseconds);
52+
values.add(watch.elapsedMicroseconds.toDouble());
53+
}
54+
55+
binding.framePolicy = defaultPolicy;
56+
});
57+
return values;
58+
}
59+
60+
double calculateMean(List<double> values) {
61+
return values.reduce((double x, double y) => x + y) / values.length;
62+
}
63+
64+
Future<void> main() async {
65+
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
66+
final double constMean = calculateMean(await runBuildBenchmark(() => ConstApp(key: rootKey)));
67+
final double nonConstMean = calculateMean(await runBuildBenchmark(() => NonConstApp(key: rootKey)));
68+
printer.addResult(
69+
description: 'const app build',
70+
value: constMean,
71+
unit: 'µs per iteration',
72+
name: 'const_app_build_iteration',
73+
);
74+
printer.addResult(
75+
description: 'non-const app build',
76+
value: nonConstMean,
77+
unit: 'µs per iteration',
78+
name: 'non_const_app_build_iteration',
79+
);
80+
printer.addResult(
81+
description: 'const speed-up (vs. non-const)',
82+
value: ((nonConstMean - constMean) / constMean) * 100,
83+
unit: '%',
84+
name: 'const_speed_up',
85+
);
86+
printer.printToStdout();
87+
}
88+
89+
final Key rootKey = UniqueKey();
90+
91+
class ConstApp extends StatelessWidget {
92+
const ConstApp({super.key});
93+
94+
@override
95+
Widget build(BuildContext context) {
96+
return const SizedBox(
97+
child: SizedBox(
98+
child: SizedBox(
99+
child: SizedBox(
100+
child: SizedBox(
101+
child: SizedBox(
102+
child: SizedBox(
103+
child: SizedBox(
104+
child: SizedBox(
105+
child: SizedBox(
106+
child: SizedBox(
107+
child: SizedBox(
108+
child: SizedBox(
109+
child: SizedBox(
110+
child: SizedBox(
111+
child: SizedBox(
112+
child: SizedBox(
113+
child: SizedBox(
114+
child: SizedBox(
115+
child: SizedBox(
116+
child: SizedBox(
117+
child: SizedBox(
118+
child: SizedBox(
119+
child: SizedBox(
120+
child: SizedBox(
121+
child: SizedBox(
122+
child: SizedBox(
123+
child: SizedBox(
124+
child: SizedBox(
125+
child: SizedBox(
126+
child: Text('testToken', textDirection: TextDirection.ltr),
127+
),
128+
),
129+
),
130+
),
131+
),
132+
),
133+
),
134+
),
135+
),
136+
),
137+
),
138+
),
139+
),
140+
),
141+
),
142+
),
143+
),
144+
),
145+
),
146+
),
147+
),
148+
),
149+
),
150+
),
151+
),
152+
),
153+
),
154+
),
155+
),
156+
);
157+
}
158+
}
159+
160+
class NonConstApp extends StatelessWidget {
161+
const NonConstApp({super.key});
162+
163+
@override
164+
Widget build(BuildContext context) {
165+
// The explicit goal is to test the performance of non-const widgets,
166+
// hence all these ignores.
167+
return SizedBox( // ignore: prefer_const_constructors
168+
child: SizedBox( // ignore: prefer_const_constructors
169+
child: SizedBox( // ignore: prefer_const_constructors
170+
child: SizedBox( // ignore: prefer_const_constructors
171+
child: SizedBox( // ignore: prefer_const_constructors
172+
child: SizedBox( // ignore: prefer_const_constructors
173+
child: SizedBox( // ignore: prefer_const_constructors
174+
child: SizedBox( // ignore: prefer_const_constructors
175+
child: SizedBox( // ignore: prefer_const_constructors
176+
child: SizedBox( // ignore: prefer_const_constructors
177+
child: SizedBox( // ignore: prefer_const_constructors
178+
child: SizedBox( // ignore: prefer_const_constructors
179+
child: SizedBox( // ignore: prefer_const_constructors
180+
child: SizedBox( // ignore: prefer_const_constructors
181+
child: SizedBox( // ignore: prefer_const_constructors
182+
child: SizedBox( // ignore: prefer_const_constructors
183+
child: SizedBox( // ignore: prefer_const_constructors
184+
child: SizedBox( // ignore: prefer_const_constructors
185+
child: SizedBox( // ignore: prefer_const_constructors
186+
child: SizedBox( // ignore: prefer_const_constructors
187+
child: SizedBox( // ignore: prefer_const_constructors
188+
child: SizedBox( // ignore: prefer_const_constructors
189+
child: SizedBox( // ignore: prefer_const_constructors
190+
child: SizedBox( // ignore: prefer_const_constructors
191+
child: SizedBox( // ignore: prefer_const_constructors
192+
child: SizedBox( // ignore: prefer_const_constructors
193+
child: SizedBox( // ignore: prefer_const_constructors
194+
child: SizedBox( // ignore: prefer_const_constructors
195+
child: SizedBox( // ignore: prefer_const_constructors
196+
child: SizedBox( // ignore: prefer_const_constructors
197+
child: Text('testToken', textDirection: TextDirection.ltr), // ignore: prefer_const_constructors
198+
),
199+
),
200+
),
201+
),
202+
),
203+
),
204+
),
205+
),
206+
),
207+
),
208+
),
209+
),
210+
),
211+
),
212+
),
213+
),
214+
),
215+
),
216+
),
217+
),
218+
),
219+
),
220+
),
221+
),
222+
),
223+
),
224+
),
225+
),
226+
),
227+
);
228+
}
229+
}

dev/devicelab/lib/tasks/microbenchmarks.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ TaskFunction createMicrobenchmarkTask({
7575
...await runMicrobench('lib/stocks/layout_bench.dart'),
7676
...await runMicrobench('lib/ui/image_bench.dart'),
7777
...await runMicrobench('lib/layout/text_intrinsic_bench.dart'),
78+
...await runMicrobench('lib/building/const_vs_non_const_bench.dart'),
7879
};
7980

8081
return TaskResult.success(allResults,

0 commit comments

Comments
 (0)