diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_dynamic_clip_on_static_picture.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_dynamic_clip_on_static_picture.dart new file mode 100644 index 00000000000..4f319bc4717 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_dynamic_clip_on_static_picture.dart @@ -0,0 +1,115 @@ +// 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 'dart:ui'; + +import 'recorder.dart'; +import 'test_data.dart'; + +/// The height of each row. +const double kRowHeight = 20.0; + +/// Number of rows. +const int kRows = 100; + +/// Number of columns. +const int kColumns = 10; + +/// The amount the picture is scrolled on every iteration of the benchmark. +const double kScrollDelta = 2.0; + +/// Draws one complex picture, then moves a clip around it simulating scrolling +/// large static content. +/// +/// This benchmark measures how efficient we are at taking advantage of the +/// static picture when all that changes is the clip. +/// +/// See also: +/// +/// * `bench_text_out_of_picture_bounds.dart`, which measures a volatile +/// picture with a static clip. +/// * https://github.com/flutter/flutter/issues/42987, which this benchmark is +/// based on. +class BenchDynamicClipOnStaticPicture extends SceneBuilderRecorder { + BenchDynamicClipOnStaticPicture() : super(name: benchmarkName) { + // If the scrollable extent is too small, the benchmark may end up + // scrolling the picture out of the clip area entirely, resulting in + // bogus metric vaules. + const double maxScrollExtent = kMaxSampleCount * kScrollDelta; + const double pictureHeight = kRows * kRowHeight; + if (maxScrollExtent > pictureHeight) { + throw Exception( + 'Bad combination of constant values kRowHeight, kRows, and ' + 'kScrollData. With these numbers there is risk that the picture ' + 'will scroll out of the clip entirely. To fix the issue reduce ' + 'kScrollDelta, or increase either kRows or kRowHeight.' + ); + } + + // Create one static picture, then never change it again. + const Color black = Color.fromARGB(255, 0, 0, 0); + final PictureRecorder pictureRecorder = PictureRecorder(); + final Canvas canvas = Canvas(pictureRecorder); + screenSize = window.physicalSize / window.devicePixelRatio; + clipSize = Size( + screenSize.width / 2, + screenSize.height / 5, + ); + final double cellWidth = screenSize.width / kColumns; + + final List paragraphs = generateLaidOutParagraphs( + paragraphCount: 500, + minWordCountPerParagraph: 3, + maxWordCountPerParagraph: 3, + widthConstraint: cellWidth, + color: black, + ); + + int paragraphCounter = 0; + double yOffset = 0.0; + for (int row = 0; row < kRows; row += 1) { + for (int column = 0; column < kColumns; column += 1) { + final double left = cellWidth * column; + canvas.save(); + canvas.clipRect(Rect.fromLTWH( + left, + yOffset, + cellWidth, + 20.0, + )); + canvas.drawParagraph( + paragraphs[paragraphCounter % paragraphs.length], + Offset(left, yOffset), + ); + canvas.restore(); + paragraphCounter += 1; + } + yOffset += kRowHeight; + } + + picture = pictureRecorder.endRecording(); + } + + static const String benchmarkName = 'dynamic_clip_on_static_picture'; + + Size screenSize; + Size clipSize; + Picture picture; + double pictureVerticalOffset = 0.0; + + @override + void onDrawFrame(SceneBuilder sceneBuilder) { + // Render the exact same picture, but offset it as if it's being scrolled. + // This will move the clip along the Y axis in picture's local coordinates + // causing a repaint. If we're not efficient at managing clips and/or + // repaints this will jank (see https://github.com/flutter/flutter/issues/42987). + final Rect clip = Rect.fromLTWH(0.0, 0.0, clipSize.width, clipSize.height); + sceneBuilder.pushClipRect(clip); + sceneBuilder.pushOffset(0.0, pictureVerticalOffset); + sceneBuilder.addPicture(Offset.zero, picture); + sceneBuilder.pop(); + sceneBuilder.pop(); + pictureVerticalOffset -= kScrollDelta; + } +} diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart index dfa7d674ce6..d1a8c3d462e 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart @@ -33,16 +33,18 @@ class BenchTextOutOfPictureBounds extends SceneBuilderRecorder { const Color green = Color.fromARGB(255, 0, 255, 0); // We don't want paragraph generation and layout to pollute benchmark numbers. - singleLineParagraphs = _generateParagraphs( + singleLineParagraphs = generateLaidOutParagraphs( paragraphCount: 500, minWordCountPerParagraph: 2, - maxWordCountPerParagraph: 5, + maxWordCountPerParagraph: 4, + widthConstraint: window.physicalSize.width / 2, color: red, ); - multiLineParagraphs = _generateParagraphs( + multiLineParagraphs = generateLaidOutParagraphs( paragraphCount: 50, minWordCountPerParagraph: 30, - maxWordCountPerParagraph: 50, + maxWordCountPerParagraph: 49, + widthConstraint: window.physicalSize.width / 2, color: green, ); } @@ -116,38 +118,4 @@ class BenchTextOutOfPictureBounds extends SceneBuilderRecorder { sceneBuilder.addPicture(Offset.zero, picture); sceneBuilder.pop(); } - - /// Generates strings and builds pre-laid out paragraphs to be used by the - /// benchmark. - List _generateParagraphs({ - int paragraphCount, - int minWordCountPerParagraph, - int maxWordCountPerParagraph, - Color color, - }) { - final List strings = []; - int wordPointer = 0; // points to the next word in lipsum to extract - for (int i = 0; i < paragraphCount; i++) { - final int wordCount = minWordCountPerParagraph + - _random.nextInt(maxWordCountPerParagraph - minWordCountPerParagraph); - final List string = []; - for (int j = 0; j < wordCount; j++) { - string.add(lipsum[wordPointer]); - wordPointer = (wordPointer + 1) % lipsum.length; - } - - final ParagraphBuilder builder = - ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif')) - ..pushStyle(TextStyle(color: color, fontSize: 18.0)) - ..addText(string.join(' ')) - ..pop(); - final Paragraph paragraph = builder.build(); - - // Fill half the screen. - paragraph - .layout(ParagraphConstraints(width: window.physicalSize.width / 2)); - strings.add(paragraph); - } - return strings; - } } diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart index b7165d90ade..5170fa7bc88 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart @@ -17,14 +17,14 @@ import 'package:flutter/widgets.dart'; /// Minimum number of samples collected by a benchmark irrespective of noise /// levels. -const int _kMinSampleCount = 50; +const int kMinSampleCount = 50; /// Maximum number of samples collected by a benchmark irrespective of noise /// levels. /// /// If the noise doesn't settle down before we reach the max we'll report noisy /// results assuming the benchmarks is simply always noisy. -const int _kMaxSampleCount = 10 * _kMinSampleCount; +const int kMaxSampleCount = 10 * kMinSampleCount; /// The number of samples used to extract metrics, such as noise, means, /// max/min values. @@ -513,7 +513,7 @@ class Profile { final Timeseries timeseries = scoreData[key]; // Collect enough data points before considering to stop. - if (timeseries.count < _kMinSampleCount) { + if (timeseries.count < kMinSampleCount) { return true; } @@ -522,11 +522,11 @@ class Profile { // If the timeseries has enough data, stop it, even if it's noisy under // the assumption that this benchmark is always noisy and there's nothing // we can do about it. - if (timeseries.count > _kMaxSampleCount) { + if (timeseries.count > kMaxSampleCount) { buffer.writeln( 'WARNING: Noise of benchmark "$name.$key" did not converge below ' '${_ratioToPercent(_kNoiseThreshold)}. Stopping because it reached the ' - 'maximum number of samples $_kMaxSampleCount. Noise level is ' + 'maximum number of samples $kMaxSampleCount. Noise level is ' '${_ratioToPercent(timeseries.noise)}.', ); return false; diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/test_data.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/test_data.dart index 2a173e091be..688dbca8b1a 100644 --- a/dev/benchmarks/macrobenchmarks/lib/src/web/test_data.dart +++ b/dev/benchmarks/macrobenchmarks/lib/src/web/test_data.dart @@ -2,6 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +// Used to randomize data. +// +// Using constant seed for reproducibility. +final math.Random _random = math.Random(0); + /// Random words used by benchmarks that contain text. final List lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing ' 'elit. Vivamus ut ligula a neque mattis posuere. Sed suscipit lobortis ' @@ -11,7 +21,7 @@ final List lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing 'odio vestibulum ultricies. Nunc dolor libero, hendrerit eu urna sit ' 'amet, pretium iaculis nulla. Ut porttitor nisl et leo iaculis, vel ' 'fringilla odio pulvinar. Ut eget ligula id odio auctor egestas nec a ' - 'nisl. Aliquam luctus dolor et magna posuere mattis.' + 'nisl. Aliquam luctus dolor et magna posuere mattis. ' 'Suspendisse fringilla nisl et massa congue, eget ' 'imperdiet lectus porta. Vestibulum sed dui sed dui porta imperdiet ut in risus. ' 'Fusce diam purus, faucibus id accumsan sit amet, semper a sem. Sed aliquam ' @@ -20,3 +30,37 @@ final List lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing 'pulvinar rhoncus tellus. Nullam vel mauris semper, volutpat tellus at, sagittis ' 'lectus. Donec vitae nibh mauris. Morbi posuere sem id eros tristique tempus. ' 'Vivamus lacinia sapien neque, eu semper purus gravida ut.'.split(' '); + +/// Generates strings and builds pre-laid out paragraphs to be used by +/// benchmarks. +List generateLaidOutParagraphs({ + @required int paragraphCount, + @required int minWordCountPerParagraph, + @required int maxWordCountPerParagraph, + @required double widthConstraint, + @required Color color, +}) { + final List strings = []; + int wordPointer = 0; // points to the next word in lipsum to extract + for (int i = 0; i < paragraphCount; i++) { + final int wordCount = minWordCountPerParagraph + + _random.nextInt(maxWordCountPerParagraph - minWordCountPerParagraph + 1); + final List string = []; + for (int j = 0; j < wordCount; j++) { + string.add(lipsum[wordPointer]); + wordPointer = (wordPointer + 1) % lipsum.length; + } + + final ParagraphBuilder builder = + ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif')) + ..pushStyle(TextStyle(color: color, fontSize: 18.0)) + ..addText(string.join(' ')) + ..pop(); + final Paragraph paragraph = builder.build(); + + // Fill half the screen. + paragraph.layout(ParagraphConstraints(width: widthConstraint)); + strings.add(paragraph); + } + return strings; +} diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart index 6866ea4bb34..434a7ca8e88 100644 --- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart +++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart @@ -12,6 +12,7 @@ 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_dynamic_clip_on_static_picture.dart'; import 'src/web/bench_simple_lazy_text_scroll.dart'; import 'src/web/bench_text_out_of_picture_bounds.dart'; import 'src/web/recorder.dart'; @@ -30,6 +31,7 @@ final Map benchmarks = { BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(), BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(), BenchBuildMaterialCheckbox.benchmarkName: () => BenchBuildMaterialCheckbox(), + BenchDynamicClipOnStaticPicture.benchmarkName: () => BenchDynamicClipOnStaticPicture(), // Benchmarks that we don't want to run using CanvasKit. if (!isCanvasKit) ...{