Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 45a8b3d

Browse files
authoredMar 27, 2020
complete text layout benchmark (flutter#53295)
1 parent ac3b77b commit 45a8b3d

File tree

4 files changed

+333
-24
lines changed

4 files changed

+333
-24
lines changed
 

‎dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart

Lines changed: 299 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import 'dart:html' as html;
66
import 'dart:js_util' as js_util;
77
import 'dart:math';
8-
import 'dart:ui';
8+
import 'dart:ui' as ui;
99

10+
import 'package:flutter/material.dart';
1011
import 'package:meta/meta.dart';
1112

1213
import 'recorder.dart';
@@ -29,18 +30,18 @@ class ParagraphGenerator {
2930

3031
/// Randomizes the given [text] and creates a paragraph with a unique
3132
/// font-size so that the engine doesn't reuse a cached ruler.
32-
Paragraph generate(
33+
ui.Paragraph generate(
3334
String text, {
3435
int maxLines,
3536
bool hasEllipsis = false,
3637
}) {
37-
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
38+
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle(
3839
fontFamily: 'sans-serif',
3940
maxLines: maxLines,
4041
ellipsis: hasEllipsis ? '...' : null,
4142
))
4243
// Start from a font-size of 8.0 and go up by 0.01 each time.
43-
..pushStyle(TextStyle(fontSize: 8.0 + _counter * 0.01))
44+
..pushStyle(ui.TextStyle(fontSize: 8.0 + _counter * 0.01))
4445
..addText(_randomize(text));
4546
_counter++;
4647
return builder.build();
@@ -57,6 +58,11 @@ void _useCanvasText(bool useCanvasText) {
5758
);
5859
}
5960

61+
typedef OnBenchmark = void Function(String name, num value);
62+
void _onBenchmark(OnBenchmark listener) {
63+
js_util.setProperty(html.window, '_flutter_internal_on_benchmark', listener);
64+
}
65+
6066
/// Repeatedly lays out a paragraph using the DOM measurement approach.
6167
///
6268
/// Creates a different paragraph each time in order to avoid hitting the cache.
@@ -119,13 +125,13 @@ class BenchTextLayout extends RawRecorder {
119125

120126
void recordParagraphOperations({
121127
@required Profile profile,
122-
@required Paragraph paragraph,
128+
@required ui.Paragraph paragraph,
123129
@required String text,
124130
@required String keyPrefix,
125131
@required double maxWidth,
126132
}) {
127133
profile.record('$keyPrefix.layout', () {
128-
paragraph.layout(ParagraphConstraints(width: maxWidth));
134+
paragraph.layout(ui.ParagraphConstraints(width: maxWidth));
129135
});
130136
profile.record('$keyPrefix.getBoxesForRange', () {
131137
for (int start = 0; start < text.length; start += 3) {
@@ -159,9 +165,9 @@ class BenchTextCachedLayout extends RawRecorder {
159165
/// Whether to use the new canvas-based text measurement implementation.
160166
final bool useCanvas;
161167

162-
final ParagraphBuilder builder =
163-
ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif'))
164-
..pushStyle(TextStyle(fontSize: 12.0))
168+
final ui.ParagraphBuilder builder =
169+
ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: 'sans-serif'))
170+
..pushStyle(ui.TextStyle(fontSize: 12.0))
165171
..addText(
166172
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
167173
'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
@@ -170,10 +176,292 @@ class BenchTextCachedLayout extends RawRecorder {
170176
@override
171177
void body(Profile profile) {
172178
_useCanvasText(useCanvas);
173-
final Paragraph paragraph = builder.build();
179+
final ui.Paragraph paragraph = builder.build();
174180
profile.record('layout', () {
175-
paragraph.layout(const ParagraphConstraints(width: double.infinity));
181+
paragraph.layout(const ui.ParagraphConstraints(width: double.infinity));
182+
});
183+
_useCanvasText(null);
184+
}
185+
}
186+
187+
/// Global counter incremented every time the benchmark is asked to
188+
/// [createWidget].
189+
///
190+
/// The purpose of this counter is to make sure the rendered paragraphs on each
191+
/// build are unique.
192+
int _counter = 0;
193+
194+
/// Measures how expensive it is to construct material checkboxes.
195+
///
196+
/// Creates a 10x10 grid of tristate checkboxes.
197+
class BenchBuildColorsGrid extends WidgetBuildRecorder {
198+
BenchBuildColorsGrid({@required this.useCanvas})
199+
: super(name: useCanvas ? canvasBenchmarkName : domBenchmarkName);
200+
201+
static const String domBenchmarkName = 'text_dom_color_grid';
202+
static const String canvasBenchmarkName = 'text_canvas_color_grid';
203+
204+
/// Whether to use the new canvas-based text measurement implementation.
205+
final bool useCanvas;
206+
207+
num _textLayoutMicros = 0;
208+
209+
@override
210+
void setUpAll() {
211+
_useCanvasText(useCanvas);
212+
_onBenchmark((String name, num value) {
213+
_textLayoutMicros += value;
176214
});
215+
}
216+
217+
@override
218+
void tearDownAll() {
177219
_useCanvasText(null);
220+
_onBenchmark(null);
221+
}
222+
223+
@override
224+
void frameWillDraw() {
225+
super.frameWillDraw();
226+
_textLayoutMicros = 0;
227+
}
228+
229+
@override
230+
void frameDidDraw() {
231+
// We need to do this before calling [super.frameDidDraw] because the latter
232+
// updates the value of [showWidget] in preparation for the next frame.
233+
if (showWidget) {
234+
profile.addDataPoint(
235+
'text_layout',
236+
Duration(microseconds: _textLayoutMicros.toInt()),
237+
);
238+
}
239+
super.frameDidDraw();
240+
}
241+
242+
@override
243+
Widget createWidget() {
244+
_counter++;
245+
return MaterialApp(home: ColorsDemo());
246+
}
247+
}
248+
249+
// The code below was copied from `colors_demo.dart` in the `flutter_gallery`
250+
// example.
251+
252+
const double kColorItemHeight = 48.0;
253+
254+
class Palette {
255+
Palette({this.name, this.primary, this.accent, this.threshold = 900});
256+
257+
final String name;
258+
final MaterialColor primary;
259+
final MaterialAccentColor accent;
260+
final int
261+
threshold; // titles for indices > threshold are white, otherwise black
262+
263+
bool get isValid => name != null && primary != null && threshold != null;
264+
}
265+
266+
final List<Palette> allPalettes = <Palette>[
267+
Palette(
268+
name: 'RED',
269+
primary: Colors.red,
270+
accent: Colors.redAccent,
271+
threshold: 300),
272+
Palette(
273+
name: 'PINK',
274+
primary: Colors.pink,
275+
accent: Colors.pinkAccent,
276+
threshold: 200),
277+
Palette(
278+
name: 'PURPLE',
279+
primary: Colors.purple,
280+
accent: Colors.purpleAccent,
281+
threshold: 200),
282+
Palette(
283+
name: 'DEEP PURPLE',
284+
primary: Colors.deepPurple,
285+
accent: Colors.deepPurpleAccent,
286+
threshold: 200),
287+
Palette(
288+
name: 'INDIGO',
289+
primary: Colors.indigo,
290+
accent: Colors.indigoAccent,
291+
threshold: 200),
292+
Palette(
293+
name: 'BLUE',
294+
primary: Colors.blue,
295+
accent: Colors.blueAccent,
296+
threshold: 400),
297+
Palette(
298+
name: 'LIGHT BLUE',
299+
primary: Colors.lightBlue,
300+
accent: Colors.lightBlueAccent,
301+
threshold: 500),
302+
Palette(
303+
name: 'CYAN',
304+
primary: Colors.cyan,
305+
accent: Colors.cyanAccent,
306+
threshold: 600),
307+
Palette(
308+
name: 'TEAL',
309+
primary: Colors.teal,
310+
accent: Colors.tealAccent,
311+
threshold: 400),
312+
Palette(
313+
name: 'GREEN',
314+
primary: Colors.green,
315+
accent: Colors.greenAccent,
316+
threshold: 500),
317+
Palette(
318+
name: 'LIGHT GREEN',
319+
primary: Colors.lightGreen,
320+
accent: Colors.lightGreenAccent,
321+
threshold: 600),
322+
Palette(
323+
name: 'LIME',
324+
primary: Colors.lime,
325+
accent: Colors.limeAccent,
326+
threshold: 800),
327+
Palette(name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent),
328+
Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent),
329+
Palette(
330+
name: 'ORANGE',
331+
primary: Colors.orange,
332+
accent: Colors.orangeAccent,
333+
threshold: 700),
334+
Palette(
335+
name: 'DEEP ORANGE',
336+
primary: Colors.deepOrange,
337+
accent: Colors.deepOrangeAccent,
338+
threshold: 400),
339+
Palette(name: 'BROWN', primary: Colors.brown, threshold: 200),
340+
Palette(name: 'GREY', primary: Colors.grey, threshold: 500),
341+
Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500),
342+
];
343+
344+
class ColorItem extends StatelessWidget {
345+
const ColorItem({
346+
Key key,
347+
@required this.index,
348+
@required this.color,
349+
this.prefix = '',
350+
}) : assert(index != null),
351+
assert(color != null),
352+
assert(prefix != null),
353+
super(key: key);
354+
355+
final int index;
356+
final Color color;
357+
final String prefix;
358+
359+
String colorString() =>
360+
"$_counter:#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
361+
362+
@override
363+
Widget build(BuildContext context) {
364+
return Semantics(
365+
container: true,
366+
child: Container(
367+
height: kColorItemHeight,
368+
padding: const EdgeInsets.symmetric(horizontal: 16.0),
369+
color: color,
370+
child: SafeArea(
371+
top: false,
372+
bottom: false,
373+
child: Row(
374+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
375+
crossAxisAlignment: CrossAxisAlignment.center,
376+
children: <Widget>[
377+
Text('$_counter:$prefix$index'),
378+
Text(colorString()),
379+
],
380+
),
381+
),
382+
),
383+
);
384+
}
385+
}
386+
387+
class PaletteTabView extends StatelessWidget {
388+
PaletteTabView({
389+
Key key,
390+
@required this.colors,
391+
}) : assert(colors != null && colors.isValid),
392+
super(key: key);
393+
394+
final Palette colors;
395+
396+
static const List<int> primaryKeys = <int>[
397+
50,
398+
100,
399+
200,
400+
300,
401+
400,
402+
500,
403+
600,
404+
700,
405+
800,
406+
900
407+
];
408+
static const List<int> accentKeys = <int>[100, 200, 400, 700];
409+
410+
@override
411+
Widget build(BuildContext context) {
412+
final TextTheme textTheme = Theme.of(context).textTheme;
413+
final TextStyle whiteTextStyle =
414+
textTheme.bodyText2.copyWith(color: Colors.white);
415+
final TextStyle blackTextStyle =
416+
textTheme.bodyText2.copyWith(color: Colors.black);
417+
return Scrollbar(
418+
child: ListView(
419+
itemExtent: kColorItemHeight,
420+
children: <Widget>[
421+
...primaryKeys.map<Widget>((int index) {
422+
return DefaultTextStyle(
423+
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
424+
child: ColorItem(index: index, color: colors.primary[index]),
425+
);
426+
}),
427+
if (colors.accent != null)
428+
...accentKeys.map<Widget>((int index) {
429+
return DefaultTextStyle(
430+
style:
431+
index > colors.threshold ? whiteTextStyle : blackTextStyle,
432+
child: ColorItem(
433+
index: index, color: colors.accent[index], prefix: 'A'),
434+
);
435+
}),
436+
],
437+
),
438+
);
439+
}
440+
}
441+
442+
class ColorsDemo extends StatelessWidget {
443+
@override
444+
Widget build(BuildContext context) {
445+
return DefaultTabController(
446+
length: allPalettes.length,
447+
child: Scaffold(
448+
appBar: AppBar(
449+
elevation: 0.0,
450+
title: const Text('Colors'),
451+
bottom: TabBar(
452+
isScrollable: true,
453+
tabs: allPalettes
454+
.map<Widget>(
455+
(Palette swatch) => Tab(text: '$_counter:${swatch.name}'))
456+
.toList(),
457+
),
458+
),
459+
body: TabBarView(
460+
children: allPalettes.map<Widget>((Palette colors) {
461+
return PaletteTabView(colors: colors);
462+
}).toList(),
463+
),
464+
),
465+
);
178466
}
179467
}

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,14 @@ abstract class WidgetRecorder extends Recorder
268268
Stopwatch _drawFrameStopwatch;
269269

270270
@override
271-
void _frameWillDraw() {
271+
@mustCallSuper
272+
void frameWillDraw() {
272273
_drawFrameStopwatch = Stopwatch()..start();
273274
}
274275

275276
@override
276-
void _frameDidDraw() {
277+
@mustCallSuper
278+
void frameDidDraw() {
277279
profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed);
278280

279281
if (profile.shouldContinue()) {
@@ -321,6 +323,18 @@ abstract class WidgetBuildRecorder extends Recorder
321323
/// consider using [WidgetRecorder].
322324
Widget createWidget();
323325

326+
/// Called once before all runs of this benchmark recorder.
327+
///
328+
/// This is useful for doing one-time setup work that's needed for the
329+
/// benchmark.
330+
void setUpAll() {}
331+
332+
/// Called once after all runs of this benchmark recorder.
333+
///
334+
/// This is useful for doing one-time clean up work after the benchmark is
335+
/// complete.
336+
void tearDownAll() {}
337+
324338
@override
325339
Profile profile;
326340

@@ -331,33 +345,35 @@ abstract class WidgetBuildRecorder extends Recorder
331345
/// Whether in this frame we should call [createWidget] and render it.
332346
///
333347
/// If false, then this frame will clear the screen.
334-
bool _showWidget = true;
348+
bool showWidget = true;
335349

336350
/// The state that hosts the widget under test.
337351
_WidgetBuildRecorderHostState _hostState;
338352

339353
Widget _getWidgetForFrame() {
340-
if (_showWidget) {
354+
if (showWidget) {
341355
return createWidget();
342356
} else {
343357
return null;
344358
}
345359
}
346360

347361
@override
348-
void _frameWillDraw() {
362+
@mustCallSuper
363+
void frameWillDraw() {
349364
_drawFrameStopwatch = Stopwatch()..start();
350365
}
351366

352367
@override
353-
void _frameDidDraw() {
368+
@mustCallSuper
369+
void frameDidDraw() {
354370
// Only record frames that show the widget.
355-
if (_showWidget) {
371+
if (showWidget) {
356372
profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed);
357373
}
358374

359375
if (profile.shouldContinue()) {
360-
_showWidget = !_showWidget;
376+
showWidget = !showWidget;
361377
_hostState._setStateTrampoline();
362378
} else {
363379
_profileCompleter.complete(profile);
@@ -372,11 +388,13 @@ abstract class WidgetBuildRecorder extends Recorder
372388
@override
373389
Future<Profile> run() {
374390
profile = Profile(name: name);
391+
setUpAll();
375392
final _RecordingWidgetsBinding binding =
376393
_RecordingWidgetsBinding.ensureInitialized();
377394
binding._beginRecording(this, _WidgetBuildRecorderHost(this));
378395

379396
_profileCompleter.future.whenComplete(() {
397+
tearDownAll();
380398
profile = null;
381399
});
382400
return _profileCompleter.future;
@@ -561,7 +579,7 @@ class Profile {
561579
buffer.writeln('name: $name');
562580
for (final String key in scoreData.keys) {
563581
final Timeseries timeseries = scoreData[key];
564-
buffer.writeln('$key:');
582+
buffer.writeln('$key: (samples=${timeseries.count})');
565583
buffer.writeln(' | average: ${timeseries.average} μs');
566584
buffer.writeln(' | noise: ${_ratioToPercent(timeseries.noise)}');
567585
}
@@ -613,10 +631,10 @@ abstract class RecordingWidgetsBindingListener {
613631
Profile profile;
614632

615633
/// Called just before calling [SchedulerBinding.handleDrawFrame].
616-
void _frameWillDraw();
634+
void frameWillDraw();
617635

618636
/// Called immediately after calling [SchedulerBinding.handleDrawFrame].
619-
void _frameDidDraw();
637+
void frameDidDraw();
620638

621639
/// Reports an error.
622640
///
@@ -697,8 +715,8 @@ class _RecordingWidgetsBinding extends BindingBase
697715
if (_hasErrored) {
698716
return;
699717
}
700-
_listener._frameWillDraw();
718+
_listener.frameWillDraw();
701719
super.handleDrawFrame();
702-
_listener._frameDidDraw();
720+
_listener.frameDidDraw();
703721
}
704722
}

‎dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ final Map<String, RecorderFactory> benchmarks = <String, RecorderFactory>{
3737
BenchTextLayout.canvasBenchmarkName: () => BenchTextLayout(useCanvas: true),
3838
BenchTextCachedLayout.domBenchmarkName: () => BenchTextCachedLayout(useCanvas: false),
3939
BenchTextCachedLayout.canvasBenchmarkName: () => BenchTextCachedLayout(useCanvas: true),
40+
BenchBuildColorsGrid.domBenchmarkName: () => BenchBuildColorsGrid(useCanvas: false),
41+
BenchBuildColorsGrid.canvasBenchmarkName: () => BenchBuildColorsGrid(useCanvas: true),
4042
}
4143
};
4244

‎dev/devicelab/lib/tasks/web_benchmarks.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
2424
return await inDirectory(macrobenchmarksDirectory, () async {
2525
await evalFlutter('build', options: <String>[
2626
'web',
27+
'--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
2728
if (useCanvasKit)
2829
'--dart-define=FLUTTER_WEB_USE_SKIA=true',
2930
'--profile',

0 commit comments

Comments
 (0)
Please sign in to comment.