// 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 { 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(), ), ), ); } }