From d95a1a70a24542c3e75c5d122aeac72c7408fba9 Mon Sep 17 00:00:00 2001 From: Yegor Date: Mon, 24 Feb 2020 16:08:40 -0800 Subject: [PATCH] add WidgetBuildRecorder for benchmarking building widgets (#51088) --- .../web/bench_build_material_checkbox.dart | 51 ++++++++ .../macrobenchmarks/lib/src/web/recorder.dart | 118 ++++++++++++++++-- .../macrobenchmarks/lib/web_benchmarks.dart | 2 + 3 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart new file mode 100644 index 0000000000..94b018b8d7 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_build_material_checkbox.dart @@ -0,0 +1,51 @@ +// 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 'recorder.dart'; + +/// Measures how expensive it is to construct material checkboxes. +/// +/// Creates a 10x10 grid of tristate checkboxes. +class BenchBuildMaterialCheckbox extends WidgetBuildRecorder { + BenchBuildMaterialCheckbox() : super(name: benchmarkName); + + static const String benchmarkName = 'build_material_checkbox'; + + static bool _isChecked; + + @override + Widget createWidget() { + return Column( + children: List.generate(10, (int i) { + return _buildRow(); + }), + ); + } + + Row _buildRow() { + if (_isChecked == null) { + _isChecked = true; + } else if (_isChecked) { + _isChecked = false; + } else { + _isChecked = null; + } + + return Row( + children: List.generate(10, (int i) { + return Expanded( + child: Checkbox( + value: _isChecked, + tristate: true, + onChanged: (bool newValue) { + // Intentionally empty. + }, + ), + ); + }), + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart index 3155a66e9b..d47e4cba09 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart @@ -183,7 +183,7 @@ abstract class RawRecorder extends Recorder { /// } /// } /// ``` -abstract class WidgetRecorder extends Recorder { +abstract class WidgetRecorder extends Recorder implements _RecordingWidgetsBindingListener { WidgetRecorder({@required String name}) : super._(name); /// Creates a widget to be benchmarked. @@ -198,10 +198,12 @@ abstract class WidgetRecorder extends Recorder { Stopwatch _drawFrameStopwatch; + @override void _frameWillDraw() { _drawFrameStopwatch = Stopwatch()..start(); } + @override void _frameDidDraw() { _frames.add(FrameMetrics._( drawFrameDuration: _drawFrameStopwatch.elapsed, @@ -226,6 +228,100 @@ abstract class WidgetRecorder extends Recorder { } } +/// A recorder for measuring the performance of building a widget from scratch +/// starting from an empty frame. +/// +/// The recorder will call [createWidget] and render it, then it will pump +/// another frame that clears the screen. It repeats this process, measuring the +/// performance of frames that render the widget and ignoring the frames that +/// clear the screen. +abstract class WidgetBuildRecorder extends Recorder implements _RecordingWidgetsBindingListener { + WidgetBuildRecorder({@required String name}) : super._(name); + + /// Creates a widget to be benchmarked. + /// + /// The widget is not expected to animate as we only care about construction + /// of the widget. If you are interested in benchmarking an animation, + /// consider using [WidgetRecorder]. + Widget createWidget(); + + final Completer _profileCompleter = Completer(); + + Stopwatch _drawFrameStopwatch; + + /// Whether in this frame we should call [createWidget] and render it. + /// + /// If false, then this frame will clear the screen. + bool _showWidget = true; + + /// The state that hosts the widget under test. + _WidgetBuildRecorderHostState _hostState; + + Widget _getWidgetForFrame() { + if (_showWidget) { + return createWidget(); + } else { + return null; + } + } + + @override + void _frameWillDraw() { + _drawFrameStopwatch = Stopwatch()..start(); + } + + @override + void _frameDidDraw() { + // Only record frames that show the widget. + if (_showWidget) { + _frames.add(FrameMetrics._( + drawFrameDuration: _drawFrameStopwatch.elapsed, + sceneBuildDuration: null, + windowRenderDuration: null, + )); + } + if (_shouldContinue()) { + _showWidget = !_showWidget; + _hostState._setStateTrampoline(); + } else { + final Profile profile = _generateProfile(); + _profileCompleter.complete(profile); + } + } + + @override + Future run() { + final _RecordingWidgetsBinding binding = + _RecordingWidgetsBinding.ensureInitialized(); + binding._beginRecording(this, _WidgetBuildRecorderHost(this)); + return _profileCompleter.future; + } +} + +/// Hosts widgets created by [WidgetBuildRecorder]. +class _WidgetBuildRecorderHost extends StatefulWidget { + const _WidgetBuildRecorderHost(this.recorder); + + final WidgetBuildRecorder recorder; + + @override + State createState() => recorder._hostState = _WidgetBuildRecorderHostState(); +} + +class _WidgetBuildRecorderHostState extends State<_WidgetBuildRecorderHost> { + // This is just to bypass the @protected on setState. + void _setStateTrampoline() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: widget.recorder._getWidgetForFrame(), + ); + } +} + /// Pumps frames and records frame metrics. abstract class Recorder { Recorder._(this.name); @@ -413,6 +509,14 @@ double _computeStandardDeviationForPopulation(Iterable population) { return math.sqrt(sumOfSquaredDeltas / population.length); } +/// Implemented by recorders that use [_RecordingWidgetsBinding] to receive +/// frame life-cycle calls. +abstract class _RecordingWidgetsBindingListener { + bool _shouldContinue(); + void _frameWillDraw(); + void _frameDidDraw(); +} + /// A variant of [WidgetsBinding] that collaborates with a [Recorder] to decide /// when to stop pumping frames. /// @@ -438,10 +542,10 @@ class _RecordingWidgetsBinding extends BindingBase return WidgetsBinding.instance as _RecordingWidgetsBinding; } - WidgetRecorder _recorder; + _RecordingWidgetsBindingListener _listener; - void _beginRecording(WidgetRecorder recorder, Widget widget) { - _recorder = recorder; + void _beginRecording(_RecordingWidgetsBindingListener recorder, Widget widget) { + _listener = recorder; runApp(widget); } @@ -451,7 +555,7 @@ class _RecordingWidgetsBinding extends BindingBase @override void handleBeginFrame(Duration rawTimeStamp) { - _benchmarkStopped = !_recorder._shouldContinue(); + _benchmarkStopped = !_listener._shouldContinue(); super.handleBeginFrame(rawTimeStamp); } @@ -464,8 +568,8 @@ class _RecordingWidgetsBinding extends BindingBase @override void handleDrawFrame() { - _recorder._frameWillDraw(); + _listener._frameWillDraw(); super.handleDrawFrame(); - _recorder._frameDidDraw(); + _listener._frameDidDraw(); } } diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart index f6af3a5f92..d2a050d5b4 100644 --- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart +++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart @@ -8,6 +8,7 @@ import 'dart:html' as html; import 'package:macrobenchmarks/src/web/bench_text_out_of_picture_bounds.dart'; +import 'src/web/bench_build_material_checkbox.dart'; import 'src/web/bench_card_infinite_scroll.dart'; import 'src/web/bench_draw_rect.dart'; import 'src/web/bench_simple_lazy_text_scroll.dart'; @@ -21,6 +22,7 @@ final Map benchmarks = { BenchDrawRect.benchmarkName: () => BenchDrawRect(), BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(), BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(), + BenchBuildMaterialCheckbox.benchmarkName: () => BenchBuildMaterialCheckbox(), }; /// Whether we fell back to manual mode.