blob: 6788ec8b71232d96724accc7cb0550a8ca613f87 [file] [log] [blame]
// 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(),
),
),
);
}
}