Add benchmark reproducing large static scrolling content (#53686)
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 0000000..4f319bc
--- /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<Paragraph> 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 dfa7d67..d1a8c3d 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 @@
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 @@
sceneBuilder.addPicture(Offset.zero, picture);
sceneBuilder.pop();
}
-
- /// Generates strings and builds pre-laid out paragraphs to be used by the
- /// benchmark.
- List<Paragraph> _generateParagraphs({
- int paragraphCount,
- int minWordCountPerParagraph,
- int maxWordCountPerParagraph,
- Color color,
- }) {
- final List<Paragraph> strings = <Paragraph>[];
- 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> string = <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 b7165d9..5170fa7 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
@@ -17,14 +17,14 @@
/// 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 @@
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 @@
// 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 2a173e0..688dbca 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<String> lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing '
'elit. Vivamus ut ligula a neque mattis posuere. Sed suscipit lobortis '
@@ -11,7 +21,7 @@
'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 @@
'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<Paragraph> generateLaidOutParagraphs({
+ @required int paragraphCount,
+ @required int minWordCountPerParagraph,
+ @required int maxWordCountPerParagraph,
+ @required double widthConstraint,
+ @required Color color,
+}) {
+ final List<Paragraph> strings = <Paragraph>[];
+ 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> string = <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 6866ea4..434a7ca 100644
--- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
@@ -12,6 +12,7 @@
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 @@
BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(),
BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(),
BenchBuildMaterialCheckbox.benchmarkName: () => BenchBuildMaterialCheckbox(),
+ BenchDynamicClipOnStaticPicture.benchmarkName: () => BenchDynamicClipOnStaticPicture(),
// Benchmarks that we don't want to run using CanvasKit.
if (!isCanvasKit) ...<String, RecorderFactory>{