flutter/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart
Yegor 07772a3d23
[framework,web] add FlutterTimeline and semantics benchmarks that use it (#128366)
## FlutterTimeline

Add a new class `FlutterTimeline` that's a drop-in replacement for `Timeline` from `dart:developer`. In addition to forwarding invocations of `startSync`, `finishSync`, `timeSync`, and `instantSync` to `dart:developer`, provides the following extra methods that make is easy to collect timings for code blocks on a frame-by-frame basis:

* `debugCollect()` - aggregates timings since the last reset, or since the app launched.
* `debugReset()` - forgets all data collected since the previous reset, or since the app launched. This allows clearing data from previous frames so timings can be attributed to the current frame.
* `now` - this was enhanced so that it works on the web by calling `window.performance.now` (in `Timeline` this is a noop in Dart web compilers).
* `collectionEnabled` - a field that controls whether `FlutterTimeline` stores timings in memory. By default this is disabled to avoid unexpected overhead (although the class is designed for minimal and predictable overhead). Specific benchmarks can enable collection to report to Skia Perf.

## Semantics benchmarks

Add `BenchMaterial3Semantics` that benchmarks the cost of semantics when constructing a screen full of Material 3 widgets from nothing. It is expected that semantics will have non-trivial cost in this case, but we should strive to keep it much lower than the rendering cost. This is the case already. This benchmark shows that the cost of semantics is <10%.

Add `BenchMaterial3ScrollSemantics` that benchmarks the cost of scrolling a previously constructed screen full of Material 3 widgets. The expectation should be that semantics will have trivial cost, since we're just shifting some widgets around. As of today, the numbers are not great, with semantics taking >50% of frame time, which is what prompted this PR in the first place. As we optimize this, we want to see this number improve.
2023-06-21 21:37:02 +00:00

459 lines
13 KiB
Dart

// 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:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'recorder.dart';
const String chars = '1234567890'
'abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'!@#%^&()[]{}<>,./?;:"`~-_=+|';
String _randomize(String text) {
return text.replaceAllMapped(
'*',
// Passing a seed so the results are reproducible.
(_) => chars[Random(0).nextInt(chars.length)],
);
}
class ParagraphGenerator {
int _counter = 0;
/// Randomizes the given [text] and creates a paragraph with a unique
/// font-size so that the engine doesn't reuse a cached ruler.
ui.Paragraph generate(
String text, {
int? maxLines,
bool hasEllipsis = false,
}) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle(
fontFamily: 'sans-serif',
maxLines: maxLines,
ellipsis: hasEllipsis ? '...' : null,
))
// Start from a font-size of 8.0 and go up by 0.01 each time.
..pushStyle(ui.TextStyle(fontSize: 8.0 + _counter * 0.01))
..addText(_randomize(text));
_counter++;
return builder.build();
}
}
/// Which mode to run [BenchBuildColorsGrid] in.
enum _TestMode {
/// Uses the HTML rendering backend with the canvas 2D text layout.
useCanvasTextLayout,
/// Uses CanvasKit for everything.
useCanvasKit,
}
/// Repeatedly lays out a paragraph.
///
/// Creates a different paragraph each time in order to avoid hitting the cache.
class BenchTextLayout extends RawRecorder {
BenchTextLayout.canvas()
: super(name: canvasBenchmarkName);
BenchTextLayout.canvasKit()
: super(name: canvasKitBenchmarkName);
static const String canvasBenchmarkName = 'text_canvas_layout';
static const String canvasKitBenchmarkName = 'text_canvaskit_layout';
final ParagraphGenerator generator = ParagraphGenerator();
static const String singleLineText = '*** ** ****';
static const String multiLineText = '*** ****** **** *** ******** * *** '
'******* **** ********** *** ******* '
'**** ***** *** ******** *** ********* '
'** * *** ******* ***********';
@override
void body(Profile profile) {
recordParagraphOperations(
profile: profile,
paragraph: generator.generate(singleLineText),
text: singleLineText,
keyPrefix: 'single_line',
maxWidth: 800.0,
);
recordParagraphOperations(
profile: profile,
paragraph: generator.generate(multiLineText),
text: multiLineText,
keyPrefix: 'multi_line',
maxWidth: 200.0,
);
recordParagraphOperations(
profile: profile,
paragraph: generator.generate(multiLineText, maxLines: 2),
text: multiLineText,
keyPrefix: 'max_lines',
maxWidth: 200.0,
);
recordParagraphOperations(
profile: profile,
paragraph: generator.generate(multiLineText, hasEllipsis: true),
text: multiLineText,
keyPrefix: 'ellipsis',
maxWidth: 200.0,
);
}
void recordParagraphOperations({
required Profile profile,
required ui.Paragraph paragraph,
required String text,
required String keyPrefix,
required double maxWidth,
}) {
profile.record('$keyPrefix.layout', () {
paragraph.layout(ui.ParagraphConstraints(width: maxWidth));
}, reported: true);
profile.record('$keyPrefix.getBoxesForRange', () {
for (int start = 0; start < text.length; start += 3) {
for (int end = start + 1; end < text.length; end *= 2) {
paragraph.getBoxesForRange(start, end);
}
}
}, reported: true);
profile.record('$keyPrefix.getPositionForOffset', () {
for (double dx = 0.0; dx < paragraph.width; dx += 10.0) {
for (double dy = 0.0; dy < paragraph.height; dy += 10.0) {
paragraph.getPositionForOffset(Offset(dx, dy));
}
}
}, reported: true);
}
}
/// Repeatedly lays out the same paragraph.
///
/// Uses the same paragraph content to make sure we hit the cache. It doesn't
/// use the same paragraph instance because the layout method will shortcircuit
/// in that case.
class BenchTextCachedLayout extends RawRecorder {
BenchTextCachedLayout.canvas()
: super(name: canvasBenchmarkName);
BenchTextCachedLayout.canvasKit()
: super(name: canvasKitBenchmarkName);
static const String canvasBenchmarkName = 'text_canvas_cached_layout';
static const String canvasKitBenchmarkName = 'text_canvas_kit_cached_layout';
@override
void body(Profile profile) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: 'sans-serif'))
..pushStyle(ui.TextStyle(fontSize: 12.0))
..addText(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
);
final ui.Paragraph paragraph = builder.build();
profile.record('layout', () {
paragraph.layout(const ui.ParagraphConstraints(width: double.infinity));
}, reported: true);
}
}
/// Global counter incremented every time the benchmark is asked to
/// [createWidget].
///
/// The purpose of this counter is to make sure the rendered paragraphs on each
/// build are unique.
int _counter = 0;
/// Measures how expensive it is to construct a realistic text-heavy piece of UI.
///
/// The benchmark constructs a tabbed view, where each tab displays a list of
/// colors. Each color's description is made of several [Text] nodes.
class BenchBuildColorsGrid extends WidgetBuildRecorder {
BenchBuildColorsGrid.canvas()
: _mode = _TestMode.useCanvasTextLayout, super(name: canvasBenchmarkName);
BenchBuildColorsGrid.canvasKit()
: _mode = _TestMode.useCanvasKit, super(name: canvasKitBenchmarkName);
/// Disables tracing for this benchmark.
///
/// When tracing is enabled, DOM layout takes longer to complete. This has a
/// significant effect on the benchmark since we do a lot of text layout
/// operations that trigger synchronous DOM layout.
@override
bool get isTracingEnabled => false;
static const String canvasBenchmarkName = 'text_canvas_color_grid';
static const String canvasKitBenchmarkName = 'text_canvas_kit_color_grid';
/// Whether to use the new canvas-based text measurement implementation.
final _TestMode _mode;
num _textLayoutMicros = 0;
@override
Future<void> setUpAll() async {
super.setUpAll();
registerEngineBenchmarkValueListener('text_layout', (num value) {
_textLayoutMicros += value;
});
}
@override
Future<void> tearDownAll() async {
stopListeningToEngineBenchmarkValues('text_layout');
}
@override
void frameWillDraw() {
super.frameWillDraw();
_textLayoutMicros = 0;
}
@override
void frameDidDraw() {
// We need to do this before calling [super.frameDidDraw] because the latter
// updates the value of [showWidget] in preparation for the next frame.
// TODO(yjbanov): https://github.com/flutter/flutter/issues/53877
if (showWidget && _mode != _TestMode.useCanvasKit) {
profile!.addDataPoint(
'text_layout',
Duration(microseconds: _textLayoutMicros.toInt()),
reported: true,
);
}
super.frameDidDraw();
}
@override
Widget createWidget() {
_counter++;
return const MaterialApp(home: ColorsDemo());
}
}
// The code below was copied from `colors_demo.dart` in the `flutter_gallery`
// example.
const double kColorItemHeight = 48.0;
class Palette {
Palette({required this.name, required this.primary, this.accent, this.threshold = 900});
final String name;
final MaterialColor primary;
final MaterialAccentColor? accent;
final int
threshold; // titles for indices > threshold are white, otherwise black
}
final List<Palette> allPalettes = <Palette>[
Palette(
name: 'RED',
primary: Colors.red,
accent: Colors.redAccent,
threshold: 300),
Palette(
name: 'PINK',
primary: Colors.pink,
accent: Colors.pinkAccent,
threshold: 200),
Palette(
name: 'PURPLE',
primary: Colors.purple,
accent: Colors.purpleAccent,
threshold: 200),
Palette(
name: 'DEEP PURPLE',
primary: Colors.deepPurple,
accent: Colors.deepPurpleAccent,
threshold: 200),
Palette(
name: 'INDIGO',
primary: Colors.indigo,
accent: Colors.indigoAccent,
threshold: 200),
Palette(
name: 'BLUE',
primary: Colors.blue,
accent: Colors.blueAccent,
threshold: 400),
Palette(
name: 'LIGHT BLUE',
primary: Colors.lightBlue,
accent: Colors.lightBlueAccent,
threshold: 500),
Palette(
name: 'CYAN',
primary: Colors.cyan,
accent: Colors.cyanAccent,
threshold: 600),
Palette(
name: 'TEAL',
primary: Colors.teal,
accent: Colors.tealAccent,
threshold: 400),
Palette(
name: 'GREEN',
primary: Colors.green,
accent: Colors.greenAccent,
threshold: 500),
Palette(
name: 'LIGHT GREEN',
primary: Colors.lightGreen,
accent: Colors.lightGreenAccent,
threshold: 600),
Palette(
name: 'LIME',
primary: Colors.lime,
accent: Colors.limeAccent,
threshold: 800),
Palette(name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent),
Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent),
Palette(
name: 'ORANGE',
primary: Colors.orange,
accent: Colors.orangeAccent,
threshold: 700),
Palette(
name: 'DEEP ORANGE',
primary: Colors.deepOrange,
accent: Colors.deepOrangeAccent,
threshold: 400),
Palette(name: 'BROWN', primary: Colors.brown, threshold: 200),
Palette(name: 'GREY', primary: Colors.grey, threshold: 500),
Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500),
];
class ColorItem extends StatelessWidget {
const ColorItem({
super.key,
required this.index,
required this.color,
this.prefix = '',
});
final int index;
final Color color;
final String prefix;
String colorString() =>
"$_counter:#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
child: Container(
height: kColorItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
color: color,
child: SafeArea(
top: false,
bottom: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('$_counter:$prefix$index'),
Text(colorString()),
],
),
),
),
);
}
}
class PaletteTabView extends StatelessWidget {
const PaletteTabView({
super.key,
required this.colors,
});
final Palette colors;
static const List<int> primaryKeys = <int>[
50,
100,
200,
300,
400,
500,
600,
700,
800,
900,
];
static const List<int> accentKeys = <int>[100, 200, 400, 700];
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle whiteTextStyle =
textTheme.bodyMedium!.copyWith(color: Colors.white);
final TextStyle blackTextStyle =
textTheme.bodyMedium!.copyWith(color: Colors.black);
return Scrollbar(
child: ListView(
itemExtent: kColorItemHeight,
children: <Widget>[
...primaryKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: ColorItem(index: index, color: colors.primary[index]!),
);
}),
if (colors.accent != null)
...accentKeys.map<Widget>((int index) {
return DefaultTextStyle(
style:
index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: ColorItem(
index: index, color: colors.accent![index]!, prefix: 'A'),
);
}),
],
),
);
}
}
class ColorsDemo extends StatelessWidget {
const ColorsDemo({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: allPalettes.length,
child: Scaffold(
appBar: AppBar(
elevation: 0.0,
title: const Text('Colors'),
bottom: TabBar(
isScrollable: true,
tabs: allPalettes
.map<Widget>(
(Palette swatch) => Tab(text: '$_counter:${swatch.name}'))
.toList(),
),
),
body: TabBarView(
children: allPalettes.map<Widget>((Palette colors) {
return PaletteTabView(colors: colors);
}).toList(),
),
),
);
}
}