Skip to content

Commit 0d07788

Browse files
authored
Add benchmark reproducing large static scrolling content (flutter#53686)
1 parent bb5c340 commit 0d07788

File tree

5 files changed

+173
-44
lines changed

5 files changed

+173
-44
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 'dart:ui';
6+
7+
import 'recorder.dart';
8+
import 'test_data.dart';
9+
10+
/// The height of each row.
11+
const double kRowHeight = 20.0;
12+
13+
/// Number of rows.
14+
const int kRows = 100;
15+
16+
/// Number of columns.
17+
const int kColumns = 10;
18+
19+
/// The amount the picture is scrolled on every iteration of the benchmark.
20+
const double kScrollDelta = 2.0;
21+
22+
/// Draws one complex picture, then moves a clip around it simulating scrolling
23+
/// large static content.
24+
///
25+
/// This benchmark measures how efficient we are at taking advantage of the
26+
/// static picture when all that changes is the clip.
27+
///
28+
/// See also:
29+
///
30+
/// * `bench_text_out_of_picture_bounds.dart`, which measures a volatile
31+
/// picture with a static clip.
32+
/// * https://github.com/flutter/flutter/issues/42987, which this benchmark is
33+
/// based on.
34+
class BenchDynamicClipOnStaticPicture extends SceneBuilderRecorder {
35+
BenchDynamicClipOnStaticPicture() : super(name: benchmarkName) {
36+
// If the scrollable extent is too small, the benchmark may end up
37+
// scrolling the picture out of the clip area entirely, resulting in
38+
// bogus metric vaules.
39+
const double maxScrollExtent = kMaxSampleCount * kScrollDelta;
40+
const double pictureHeight = kRows * kRowHeight;
41+
if (maxScrollExtent > pictureHeight) {
42+
throw Exception(
43+
'Bad combination of constant values kRowHeight, kRows, and '
44+
'kScrollData. With these numbers there is risk that the picture '
45+
'will scroll out of the clip entirely. To fix the issue reduce '
46+
'kScrollDelta, or increase either kRows or kRowHeight.'
47+
);
48+
}
49+
50+
// Create one static picture, then never change it again.
51+
const Color black = Color.fromARGB(255, 0, 0, 0);
52+
final PictureRecorder pictureRecorder = PictureRecorder();
53+
final Canvas canvas = Canvas(pictureRecorder);
54+
screenSize = window.physicalSize / window.devicePixelRatio;
55+
clipSize = Size(
56+
screenSize.width / 2,
57+
screenSize.height / 5,
58+
);
59+
final double cellWidth = screenSize.width / kColumns;
60+
61+
final List<Paragraph> paragraphs = generateLaidOutParagraphs(
62+
paragraphCount: 500,
63+
minWordCountPerParagraph: 3,
64+
maxWordCountPerParagraph: 3,
65+
widthConstraint: cellWidth,
66+
color: black,
67+
);
68+
69+
int paragraphCounter = 0;
70+
double yOffset = 0.0;
71+
for (int row = 0; row < kRows; row += 1) {
72+
for (int column = 0; column < kColumns; column += 1) {
73+
final double left = cellWidth * column;
74+
canvas.save();
75+
canvas.clipRect(Rect.fromLTWH(
76+
left,
77+
yOffset,
78+
cellWidth,
79+
20.0,
80+
));
81+
canvas.drawParagraph(
82+
paragraphs[paragraphCounter % paragraphs.length],
83+
Offset(left, yOffset),
84+
);
85+
canvas.restore();
86+
paragraphCounter += 1;
87+
}
88+
yOffset += kRowHeight;
89+
}
90+
91+
picture = pictureRecorder.endRecording();
92+
}
93+
94+
static const String benchmarkName = 'dynamic_clip_on_static_picture';
95+
96+
Size screenSize;
97+
Size clipSize;
98+
Picture picture;
99+
double pictureVerticalOffset = 0.0;
100+
101+
@override
102+
void onDrawFrame(SceneBuilder sceneBuilder) {
103+
// Render the exact same picture, but offset it as if it's being scrolled.
104+
// This will move the clip along the Y axis in picture's local coordinates
105+
// causing a repaint. If we're not efficient at managing clips and/or
106+
// repaints this will jank (see https://github.com/flutter/flutter/issues/42987).
107+
final Rect clip = Rect.fromLTWH(0.0, 0.0, clipSize.width, clipSize.height);
108+
sceneBuilder.pushClipRect(clip);
109+
sceneBuilder.pushOffset(0.0, pictureVerticalOffset);
110+
sceneBuilder.addPicture(Offset.zero, picture);
111+
sceneBuilder.pop();
112+
sceneBuilder.pop();
113+
pictureVerticalOffset -= kScrollDelta;
114+
}
115+
}

dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ class BenchTextOutOfPictureBounds extends SceneBuilderRecorder {
3333
const Color green = Color.fromARGB(255, 0, 255, 0);
3434

3535
// We don't want paragraph generation and layout to pollute benchmark numbers.
36-
singleLineParagraphs = _generateParagraphs(
36+
singleLineParagraphs = generateLaidOutParagraphs(
3737
paragraphCount: 500,
3838
minWordCountPerParagraph: 2,
39-
maxWordCountPerParagraph: 5,
39+
maxWordCountPerParagraph: 4,
40+
widthConstraint: window.physicalSize.width / 2,
4041
color: red,
4142
);
42-
multiLineParagraphs = _generateParagraphs(
43+
multiLineParagraphs = generateLaidOutParagraphs(
4344
paragraphCount: 50,
4445
minWordCountPerParagraph: 30,
45-
maxWordCountPerParagraph: 50,
46+
maxWordCountPerParagraph: 49,
47+
widthConstraint: window.physicalSize.width / 2,
4648
color: green,
4749
);
4850
}
@@ -116,38 +118,4 @@ class BenchTextOutOfPictureBounds extends SceneBuilderRecorder {
116118
sceneBuilder.addPicture(Offset.zero, picture);
117119
sceneBuilder.pop();
118120
}
119-
120-
/// Generates strings and builds pre-laid out paragraphs to be used by the
121-
/// benchmark.
122-
List<Paragraph> _generateParagraphs({
123-
int paragraphCount,
124-
int minWordCountPerParagraph,
125-
int maxWordCountPerParagraph,
126-
Color color,
127-
}) {
128-
final List<Paragraph> strings = <Paragraph>[];
129-
int wordPointer = 0; // points to the next word in lipsum to extract
130-
for (int i = 0; i < paragraphCount; i++) {
131-
final int wordCount = minWordCountPerParagraph +
132-
_random.nextInt(maxWordCountPerParagraph - minWordCountPerParagraph);
133-
final List<String> string = <String>[];
134-
for (int j = 0; j < wordCount; j++) {
135-
string.add(lipsum[wordPointer]);
136-
wordPointer = (wordPointer + 1) % lipsum.length;
137-
}
138-
139-
final ParagraphBuilder builder =
140-
ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif'))
141-
..pushStyle(TextStyle(color: color, fontSize: 18.0))
142-
..addText(string.join(' '))
143-
..pop();
144-
final Paragraph paragraph = builder.build();
145-
146-
// Fill half the screen.
147-
paragraph
148-
.layout(ParagraphConstraints(width: window.physicalSize.width / 2));
149-
strings.add(paragraph);
150-
}
151-
return strings;
152-
}
153121
}

dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import 'package:flutter/widgets.dart';
1717

1818
/// Minimum number of samples collected by a benchmark irrespective of noise
1919
/// levels.
20-
const int _kMinSampleCount = 50;
20+
const int kMinSampleCount = 50;
2121

2222
/// Maximum number of samples collected by a benchmark irrespective of noise
2323
/// levels.
2424
///
2525
/// If the noise doesn't settle down before we reach the max we'll report noisy
2626
/// results assuming the benchmarks is simply always noisy.
27-
const int _kMaxSampleCount = 10 * _kMinSampleCount;
27+
const int kMaxSampleCount = 10 * kMinSampleCount;
2828

2929
/// The number of samples used to extract metrics, such as noise, means,
3030
/// max/min values.
@@ -513,7 +513,7 @@ class Profile {
513513
final Timeseries timeseries = scoreData[key];
514514

515515
// Collect enough data points before considering to stop.
516-
if (timeseries.count < _kMinSampleCount) {
516+
if (timeseries.count < kMinSampleCount) {
517517
return true;
518518
}
519519

@@ -522,11 +522,11 @@ class Profile {
522522
// If the timeseries has enough data, stop it, even if it's noisy under
523523
// the assumption that this benchmark is always noisy and there's nothing
524524
// we can do about it.
525-
if (timeseries.count > _kMaxSampleCount) {
525+
if (timeseries.count > kMaxSampleCount) {
526526
buffer.writeln(
527527
'WARNING: Noise of benchmark "$name.$key" did not converge below '
528528
'${_ratioToPercent(_kNoiseThreshold)}. Stopping because it reached the '
529-
'maximum number of samples $_kMaxSampleCount. Noise level is '
529+
'maximum number of samples $kMaxSampleCount. Noise level is '
530530
'${_ratioToPercent(timeseries.noise)}.',
531531
);
532532
return false;

dev/benchmarks/macrobenchmarks/lib/src/web/test_data.dart

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:math' as math;
6+
import 'dart:ui';
7+
8+
import 'package:meta/meta.dart';
9+
10+
// Used to randomize data.
11+
//
12+
// Using constant seed for reproducibility.
13+
final math.Random _random = math.Random(0);
14+
515
/// Random words used by benchmarks that contain text.
616
final List<String> lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing '
717
'elit. Vivamus ut ligula a neque mattis posuere. Sed suscipit lobortis '
@@ -11,7 +21,7 @@ final List<String> lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing
1121
'odio vestibulum ultricies. Nunc dolor libero, hendrerit eu urna sit '
1222
'amet, pretium iaculis nulla. Ut porttitor nisl et leo iaculis, vel '
1323
'fringilla odio pulvinar. Ut eget ligula id odio auctor egestas nec a '
14-
'nisl. Aliquam luctus dolor et magna posuere mattis.'
24+
'nisl. Aliquam luctus dolor et magna posuere mattis. '
1525
'Suspendisse fringilla nisl et massa congue, eget '
1626
'imperdiet lectus porta. Vestibulum sed dui sed dui porta imperdiet ut in risus. '
1727
'Fusce diam purus, faucibus id accumsan sit amet, semper a sem. Sed aliquam '
@@ -20,3 +30,37 @@ final List<String> lipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing
2030
'pulvinar rhoncus tellus. Nullam vel mauris semper, volutpat tellus at, sagittis '
2131
'lectus. Donec vitae nibh mauris. Morbi posuere sem id eros tristique tempus. '
2232
'Vivamus lacinia sapien neque, eu semper purus gravida ut.'.split(' ');
33+
34+
/// Generates strings and builds pre-laid out paragraphs to be used by
35+
/// benchmarks.
36+
List<Paragraph> generateLaidOutParagraphs({
37+
@required int paragraphCount,
38+
@required int minWordCountPerParagraph,
39+
@required int maxWordCountPerParagraph,
40+
@required double widthConstraint,
41+
@required Color color,
42+
}) {
43+
final List<Paragraph> strings = <Paragraph>[];
44+
int wordPointer = 0; // points to the next word in lipsum to extract
45+
for (int i = 0; i < paragraphCount; i++) {
46+
final int wordCount = minWordCountPerParagraph +
47+
_random.nextInt(maxWordCountPerParagraph - minWordCountPerParagraph + 1);
48+
final List<String> string = <String>[];
49+
for (int j = 0; j < wordCount; j++) {
50+
string.add(lipsum[wordPointer]);
51+
wordPointer = (wordPointer + 1) % lipsum.length;
52+
}
53+
54+
final ParagraphBuilder builder =
55+
ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif'))
56+
..pushStyle(TextStyle(color: color, fontSize: 18.0))
57+
..addText(string.join(' '))
58+
..pop();
59+
final Paragraph paragraph = builder.build();
60+
61+
// Fill half the screen.
62+
paragraph.layout(ParagraphConstraints(width: widthConstraint));
63+
strings.add(paragraph);
64+
}
65+
return strings;
66+
}

dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:macrobenchmarks/src/web/bench_text_out_of_picture_bounds.dart';
1212
import 'src/web/bench_build_material_checkbox.dart';
1313
import 'src/web/bench_card_infinite_scroll.dart';
1414
import 'src/web/bench_draw_rect.dart';
15+
import 'src/web/bench_dynamic_clip_on_static_picture.dart';
1516
import 'src/web/bench_simple_lazy_text_scroll.dart';
1617
import 'src/web/bench_text_out_of_picture_bounds.dart';
1718
import 'src/web/recorder.dart';
@@ -30,6 +31,7 @@ final Map<String, RecorderFactory> benchmarks = <String, RecorderFactory>{
3031
BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(),
3132
BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(),
3233
BenchBuildMaterialCheckbox.benchmarkName: () => BenchBuildMaterialCheckbox(),
34+
BenchDynamicClipOnStaticPicture.benchmarkName: () => BenchDynamicClipOnStaticPicture(),
3335

3436
// Benchmarks that we don't want to run using CanvasKit.
3537
if (!isCanvasKit) ...<String, RecorderFactory>{

0 commit comments

Comments
 (0)