From c4ad1dc1e9011d4b42739392e33e4f2cad2fbc2a Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 16 May 2024 16:23:14 -0700 Subject: [PATCH] const vs. non-const widget build benchmark (#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. --- .../building/const_vs_non_const_bench.dart | 229 ++++++++++++++++++ dev/devicelab/lib/tasks/microbenchmarks.dart | 1 + 2 files changed, 230 insertions(+) create mode 100644 dev/benchmarks/microbenchmarks/lib/building/const_vs_non_const_bench.dart diff --git a/dev/benchmarks/microbenchmarks/lib/building/const_vs_non_const_bench.dart b/dev/benchmarks/microbenchmarks/lib/building/const_vs_non_const_bench.dart new file mode 100644 index 0000000000..be29047986 --- /dev/null +++ b/dev/benchmarks/microbenchmarks/lib/building/const_vs_non_const_bench.dart @@ -0,0 +1,229 @@ +// 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'; + +import '../common.dart'; + +const Duration kBenchmarkTime = Duration(seconds: 15); + +Future> runBuildBenchmark(ValueGetter buildApp) async { + assert(false, "Don't run benchmarks in debug mode! Use 'flutter run --release'."); + + // We control the framePolicy below to prevent us from scheduling frames in + // the engine, so that the engine does not interfere with our timings. + final LiveTestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as LiveTestWidgetsFlutterBinding; + + final Stopwatch watch = Stopwatch(); + int iterations = 0; + final List values = []; + + await benchmarkWidgets((WidgetTester tester) async { + await tester.pumpWidget(buildApp()); + await tester.pump(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Cannot use expects/asserts here since this is running outside of a test + // in release mode. + final int numberOfSizedBoxes = find.byType(SizedBox).evaluate().length; + if (numberOfSizedBoxes != 30) { + throw StateError('Expected 30 SizedBox widgets, but only found $numberOfSizedBoxes.'); + } + if (find.text('testToken').evaluate().length != 1) { + throw StateError('Did not find expected leaf widget.'); + } + + final Element rootWidget = tester.element(find.byKey(rootKey)); + Duration elapsed = Duration.zero; + + final LiveTestWidgetsFlutterBindingFramePolicy defaultPolicy = binding.framePolicy; + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark; + + while (elapsed < kBenchmarkTime) { + watch.reset(); + watch.start(); + rootWidget.markNeedsBuild(); + await tester.pumpBenchmark(Duration(milliseconds: iterations * 16)); + watch.stop(); + iterations += 1; + elapsed += Duration(microseconds: watch.elapsedMicroseconds); + values.add(watch.elapsedMicroseconds.toDouble()); + } + + binding.framePolicy = defaultPolicy; + }); + return values; +} + +double calculateMean(List values) { + return values.reduce((double x, double y) => x + y) / values.length; +} + +Future main() async { + final BenchmarkResultPrinter printer = BenchmarkResultPrinter(); + final double constMean = calculateMean(await runBuildBenchmark(() => ConstApp(key: rootKey))); + final double nonConstMean = calculateMean(await runBuildBenchmark(() => NonConstApp(key: rootKey))); + printer.addResult( + description: 'const app build', + value: constMean, + unit: 'µs per iteration', + name: 'const_app_build_iteration', + ); + printer.addResult( + description: 'non-const app build', + value: nonConstMean, + unit: 'µs per iteration', + name: 'non_const_app_build_iteration', + ); + printer.addResult( + description: 'const speed-up (vs. non-const)', + value: ((nonConstMean - constMean) / constMean) * 100, + unit: '%', + name: 'const_speed_up', + ); + printer.printToStdout(); +} + +final Key rootKey = UniqueKey(); + +class ConstApp extends StatelessWidget { + const ConstApp({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: SizedBox( + child: Text('testToken', textDirection: TextDirection.ltr), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class NonConstApp extends StatelessWidget { + const NonConstApp({super.key}); + + @override + Widget build(BuildContext context) { + // The explicit goal is to test the performance of non-const widgets, + // hence all these ignores. + return SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: SizedBox( // ignore: prefer_const_constructors + child: Text('testToken', textDirection: TextDirection.ltr), // ignore: prefer_const_constructors + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/dev/devicelab/lib/tasks/microbenchmarks.dart b/dev/devicelab/lib/tasks/microbenchmarks.dart index aa38ab9a57..40dbe1c4cf 100644 --- a/dev/devicelab/lib/tasks/microbenchmarks.dart +++ b/dev/devicelab/lib/tasks/microbenchmarks.dart @@ -75,6 +75,7 @@ TaskFunction createMicrobenchmarkTask({ ...await runMicrobench('lib/stocks/layout_bench.dart'), ...await runMicrobench('lib/ui/image_bench.dart'), ...await runMicrobench('lib/layout/text_intrinsic_bench.dart'), + ...await runMicrobench('lib/building/const_vs_non_const_bench.dart'), }; return TaskResult.success(allResults,