[web] Add benchmarks for text layout (#51663)
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rect.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rect.dart
index 34bca92..a6a2a9a 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rect.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rect.dart
@@ -9,7 +9,7 @@
/// Repeatedly paints a grid of rectangles.
///
/// Measures the performance of the `drawRect` operation.
-class BenchDrawRect extends RawRecorder {
+class BenchDrawRect extends SceneBuilderRecorder {
BenchDrawRect() : super(name: benchmarkName);
static const String benchmarkName = 'draw_rect';
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart
new file mode 100644
index 0000000..6c0fb71
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_layout.dart
@@ -0,0 +1,65 @@
+// 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:ui';
+
+import 'recorder.dart';
+
+int _counter = 0;
+
+Paragraph _generateParagraph() {
+ final ParagraphBuilder builder =
+ ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif'))
+ ..pushStyle(TextStyle(fontSize: 12.0))
+ ..addText(
+ '$_counter Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
+ 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ );
+ _counter++;
+ return builder.build();
+}
+
+/// Repeatedly lays out a paragraph using the DOM measurement approach.
+///
+/// Creates a different paragraph each time in order to avoid hitting the cache.
+class BenchTextDomLayout extends RawRecorder {
+ BenchTextDomLayout() : super(name: benchmarkName);
+
+ static const String benchmarkName = 'text_dom_layout';
+
+ @override
+ void body(Profile profile) {
+ final Paragraph paragraph = _generateParagraph();
+ profile.record('layout', () {
+ paragraph.layout(const ParagraphConstraints(width: double.infinity));
+ });
+ }
+}
+
+/// Repeatedly lays out a paragraph using the DOM measurement approach.
+///
+/// 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 BenchTextDomCachedLayout extends RawRecorder {
+ BenchTextDomCachedLayout() : super(name: benchmarkName);
+
+ static const String benchmarkName = 'text_dom_cached_layout';
+
+ final ParagraphBuilder builder =
+ ParagraphBuilder(ParagraphStyle(fontFamily: 'sans-serif'))
+ ..pushStyle(TextStyle(fontSize: 12.0))
+ ..addText(
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
+ 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ );
+
+ @override
+ void body(Profile profile) {
+ final Paragraph paragraph = builder.build();
+ profile.record('layout', () {
+ paragraph.layout(const ParagraphConstraints(width: double.infinity));
+ });
+ }
+}
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart
index db8eb49..dfa7d67 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_text_out_of_picture_bounds.dart
@@ -27,7 +27,7 @@
///
/// This reproduces the bug where we render more than visible causing
/// performance issues: https://github.com/flutter/flutter/issues/48516
-class BenchTextOutOfPictureBounds extends RawRecorder {
+class BenchTextOutOfPictureBounds extends SceneBuilderRecorder {
BenchTextOutOfPictureBounds() : super(name: benchmarkName) {
const Color red = Color.fromARGB(255, 255, 0, 0);
const Color green = Color.fromARGB(255, 0, 255, 0);
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
index 4ebcf6d..de353f6 100644
--- a/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart
@@ -44,6 +44,79 @@
return stopwatch.elapsed;
}
+/// Base class for benchmark recorders.
+///
+/// Each benchmark recorder has a [name] and a [run] method at a minimum.
+abstract class Recorder {
+ Recorder._(this.name);
+
+ /// The name of the benchmark.
+ ///
+ /// The results displayed in the Flutter Dashboard will use this name as a
+ /// prefix.
+ final String name;
+
+ /// The implementation of the benchmark that will produce a [Profile].
+ Future<Profile> run();
+}
+
+/// A recorder for benchmarking raw execution of Dart code.
+///
+/// This is useful for benchmarks that don't need frames or widgets.
+///
+/// Example:
+///
+/// ```
+/// class BenchForLoop extends RawRecorder {
+/// BenchForLoop() : super(name: benchmarkName);
+///
+/// static const String benchmarkName = 'for_loop';
+///
+/// @override
+/// void body(Profile profile) {
+/// profile.record('loop', () {
+/// double x = 0;
+/// for (int i = 0; i < 10000000; i++) {
+/// x *= 1.5;
+/// }
+/// });
+/// }
+/// }
+/// ```
+abstract class RawRecorder extends Recorder {
+ RawRecorder({@required String name}) : super._(name);
+
+ /// Called once before all runs of this benchmark recorder.
+ ///
+ /// This is useful for doing one-time setup work that's needed for the
+ /// benchmark.
+ void setUpAll() {}
+
+ /// Called once after all runs of this benchmark recorder.
+ ///
+ /// This is useful for doing one-time clean up work after the benchmark is
+ /// complete.
+ void tearDownAll() {}
+
+ /// The body of the benchmark.
+ ///
+ /// This is the part that records measurements of the benchmark.
+ void body(Profile profile);
+
+ @override
+ @nonVirtual
+ Future<Profile> run() async {
+ final Profile profile = Profile(name: name);
+ setUpAll();
+ do {
+ await Future<void>.delayed(Duration.zero);
+ body(profile);
+ } while (profile.shouldContinue());
+ tearDownAll();
+ return profile;
+ }
+}
+
/// A recorder for benchmarking interactions with the engine without the
/// framework by directly exercising [SceneBuilder].
///
@@ -52,13 +125,13 @@
/// Example:
///
/// ```
-/// class BenchDrawCircle extends RawRecorder {
+/// class BenchDrawCircle extends SceneBuilderRecorder {
/// BenchDrawCircle() : super(name: benchmarkName);
///
/// static const String benchmarkName = 'draw_circle';
///
/// @override
-/// void onDrawFrame(SceneBuilder sceneBuilder, FrameMetricsBuilder metricsBuilder) {
+/// void onDrawFrame(SceneBuilder sceneBuilder) {
/// final PictureRecorder pictureRecorder = PictureRecorder();
/// final Canvas canvas = Canvas(pictureRecorder);
/// final Paint paint = Paint()..color = const Color.fromARGB(255, 255, 0, 0);
@@ -69,8 +142,8 @@
/// }
/// }
/// ```
-abstract class RawRecorder extends Recorder {
- RawRecorder({@required String name}) : super._(name);
+abstract class SceneBuilderRecorder extends Recorder {
+ SceneBuilderRecorder({@required String name}) : super._(name);
/// Called from [Window.onBeginFrame].
@mustCallSuper
@@ -81,39 +154,31 @@
/// An implementation should exercise the [sceneBuilder] to build a frame.
/// However, it must not call [SceneBuilder.build] or [Window.render].
/// Instead the benchmark harness will call them and time them appropriately.
- ///
- /// The callback is given a [FrameMetricsBuilder] that can be populated
- /// with various frame-related metrics, such as paint time and layout time.
void onDrawFrame(SceneBuilder sceneBuilder);
@override
Future<Profile> run() {
final Completer<Profile> profileCompleter = Completer<Profile>();
+ final Profile profile = Profile(name: name);
+
window.onBeginFrame = (_) {
onBeginFrame();
};
window.onDrawFrame = () {
- Duration sceneBuildDuration;
- Duration windowRenderDuration;
- final Duration drawFrameDuration = timeAction(() {
+ profile.record('drawFrameDuration', () {
final SceneBuilder sceneBuilder = SceneBuilder();
onDrawFrame(sceneBuilder);
- sceneBuildDuration = timeAction(() {
+ profile.record('sceneBuildDuration', () {
final Scene scene = sceneBuilder.build();
- windowRenderDuration = timeAction(() {
+ profile.record('windowRenderDuration', () {
window.render(scene);
});
});
});
- _frames.add(FrameMetrics._(
- drawFrameDuration: drawFrameDuration,
- sceneBuildDuration: sceneBuildDuration,
- windowRenderDuration: windowRenderDuration,
- ));
- if (_shouldContinue()) {
+
+ if (profile.shouldContinue()) {
window.scheduleFrame();
} else {
- final Profile profile = _generateProfile();
profileCompleter.complete(profile);
}
};
@@ -183,7 +248,8 @@
/// }
/// }
/// ```
-abstract class WidgetRecorder extends Recorder implements _RecordingWidgetsBindingListener {
+abstract class WidgetRecorder extends Recorder
+ implements RecordingWidgetsBindingListener {
WidgetRecorder({@required String name}) : super._(name);
/// Creates a widget to be benchmarked.
@@ -194,6 +260,9 @@
/// low.
Widget createWidget();
+ @override
+ Profile profile;
+
final Completer<Profile> _profileCompleter = Completer<Profile>();
Stopwatch _drawFrameStopwatch;
@@ -205,15 +274,11 @@
@override
void _frameDidDraw() {
- _frames.add(FrameMetrics._(
- drawFrameDuration: _drawFrameStopwatch.elapsed,
- sceneBuildDuration: null,
- windowRenderDuration: null,
- ));
- if (_shouldContinue()) {
+ profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed);
+
+ if (profile.shouldContinue()) {
window.scheduleFrame();
} else {
- final Profile profile = _generateProfile();
_profileCompleter.complete(profile);
}
}
@@ -225,10 +290,15 @@
@override
Future<Profile> run() {
+ profile = Profile(name: name);
final _RecordingWidgetsBinding binding =
_RecordingWidgetsBinding.ensureInitialized();
final Widget widget = createWidget();
binding._beginRecording(this, widget);
+
+ _profileCompleter.future.whenComplete(() {
+ profile = null;
+ });
return _profileCompleter.future;
}
}
@@ -240,7 +310,8 @@
/// another frame that clears the screen. It repeats this process, measuring the
/// performance of frames that render the widget and ignoring the frames that
/// clear the screen.
-abstract class WidgetBuildRecorder extends Recorder implements _RecordingWidgetsBindingListener {
+abstract class WidgetBuildRecorder extends Recorder
+ implements RecordingWidgetsBindingListener {
WidgetBuildRecorder({@required String name}) : super._(name);
/// Creates a widget to be benchmarked.
@@ -250,6 +321,9 @@
/// consider using [WidgetRecorder].
Widget createWidget();
+ @override
+ Profile profile;
+
final Completer<Profile> _profileCompleter = Completer<Profile>();
Stopwatch _drawFrameStopwatch;
@@ -279,17 +353,13 @@
void _frameDidDraw() {
// Only record frames that show the widget.
if (_showWidget) {
- _frames.add(FrameMetrics._(
- drawFrameDuration: _drawFrameStopwatch.elapsed,
- sceneBuildDuration: null,
- windowRenderDuration: null,
- ));
+ profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed);
}
- if (_shouldContinue()) {
+
+ if (profile.shouldContinue()) {
_showWidget = !_showWidget;
_hostState._setStateTrampoline();
} else {
- final Profile profile = _generateProfile();
_profileCompleter.complete(profile);
}
}
@@ -301,9 +371,14 @@
@override
Future<Profile> run() {
+ profile = Profile(name: name);
final _RecordingWidgetsBinding binding =
_RecordingWidgetsBinding.ensureInitialized();
binding._beginRecording(this, _WidgetBuildRecorderHost(this));
+
+ _profileCompleter.future.whenComplete(() {
+ profile = null;
+ });
return _profileCompleter.future;
}
}
@@ -315,7 +390,8 @@
final WidgetBuildRecorder recorder;
@override
- State<StatefulWidget> createState() => recorder._hostState = _WidgetBuildRecorderHostState();
+ State<StatefulWidget> createState() =>
+ recorder._hostState = _WidgetBuildRecorderHostState();
}
class _WidgetBuildRecorderHostState extends State<_WidgetBuildRecorderHost> {
@@ -332,175 +408,181 @@
}
}
-/// Pumps frames and records frame metrics.
-abstract class Recorder {
- Recorder._(this.name);
+/// Series of time recordings indexed in time order.
+///
+/// It can calculate [average], [standardDeviation] and [noise]. If the amount
+/// of data collected is higher than [_kMeasuredSampleCount], then these
+/// calculations will only apply to the latest [_kMeasuredSampleCount] data
+/// points.
+class Timeseries {
+ Timeseries();
- /// The name of the benchmark being recorded.
+ /// List of all the values that have been recorded.
+ ///
+ /// This list has no limit.
+ final List<num> _allValues = <num>[];
+
+ /// List of values that are being used for measurement purposes.
+ ///
+ /// [average], [standardDeviation] and [noise] are all based on this list, not
+ /// the [_allValues] list.
+ final List<num> _measuredValues = <num>[];
+
+ /// The total amount of data collected, including ones that were dropped
+ /// because of the sample size limit.
+ int get count => _allValues.length;
+
+ double get average => _computeMean(_measuredValues);
+
+ double get standardDeviation =>
+ _computeStandardDeviationForPopulation(_measuredValues);
+
+ double get noise => standardDeviation / average;
+
+ void add(num value) {
+ _measuredValues.add(value);
+ _allValues.add(value);
+ // Don't let the [_measuredValues] list grow beyond [_kMeasuredSampleCount].
+ if (_measuredValues.length > _kMeasuredSampleCount) {
+ _measuredValues.removeAt(0);
+ }
+ }
+}
+
+/// Base class for a profile collected from running a benchmark.
+class Profile {
+ Profile({@required this.name}) : assert(name != null);
+
+ /// The name of the benchmark that produced this profile.
final String name;
- /// Frame metrics recorded during a single benchmark run.
- final List<FrameMetrics> _frames = <FrameMetrics>[];
+ /// This data will be used to display cards in the Flutter Dashboard.
+ final Map<String, Timeseries> scoreData = <String, Timeseries>{};
- /// Runs the benchmark and records a profile containing frame metrics.
- Future<Profile> run();
+ /// This data isn't displayed anywhere. It's stored for completeness purposes.
+ final Map<String, dynamic> extraData = <String, dynamic>{};
+
+ /// Invokes [callback] and records the duration of its execution under [key].
+ Duration record(String key, VoidCallback callback) {
+ final Duration duration = timeAction(callback);
+ addDataPoint(key, duration);
+ return duration;
+ }
+
+ void addDataPoint(String key, Duration duration) {
+ scoreData.putIfAbsent(key, () => Timeseries()).add(duration.inMicroseconds);
+ }
/// Decides whether the data collected so far is sufficient to stop, or
/// whether the benchmark should continue collecting more data.
///
/// The signals used are sample size, noise, and duration.
- bool _shouldContinue() {
- // Run through a minimum number of frames.
- if (_frames.length < _kMinSampleCount) {
+ ///
+ /// If any of the timeseries doesn't satisfy the noise requirements, this
+ /// method will return true (asking the benchmark to continue collecting
+ /// data).
+ bool shouldContinue() {
+ // If we haven't recorded anything yet, we don't wanna stop now.
+ if (scoreData.isEmpty) {
return true;
}
- final Profile profile = _generateProfile();
+ // Accumulates all the messages to be printed when the final decision is to
+ // stop collecting data.
+ final StringBuffer buffer = StringBuffer();
- // Is it still too noisy?
- if (profile.drawFrameDurationNoise > _kNoiseThreshold) {
- // If the benchmark has run long enough, stop it, even if it's noisy under
- // the assumption that this benchmark is always noisy and there's nothing
- // we can do about it.
- if (_frames.length > _kMaxSampleCount) {
- print(
- 'WARNING: Benchmark noise did not converge below ${_kNoiseThreshold * 100}%. '
- 'Stopping because it reached the maximum number of samples $_kMaxSampleCount. '
- 'Noise level is ${profile.drawFrameDurationNoise * 100}%.',
- );
- return false;
+ final Iterable<bool> shouldContinueList = scoreData.keys.map((String key) {
+ final Timeseries timeseries = scoreData[key];
+
+ // Collect enough data points before considering to stop.
+ if (timeseries.count < _kMinSampleCount) {
+ return true;
}
- // Keep running.
- return true;
+ // Is it still too noisy?
+ if (timeseries.noise > _kNoiseThreshold) {
+ // If the timeseries has enough data, stop it, even if it's noisy under
+ // the assumption that this benchmark is always noisy and there's nothing
+ // we can do about it.
+ if (timeseries.count > _kMaxSampleCount) {
+ buffer.writeln(
+ 'WARNING: Noise of benchmark "$name.$key" did not converge below '
+ '${_ratioToPercent(_kNoiseThreshold)}. Stopping because it reached the '
+ 'maximum number of samples $_kMaxSampleCount. Noise level is '
+ '${_ratioToPercent(timeseries.noise)}.',
+ );
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ buffer.writeln(
+ 'SUCCESS: Benchmark converged below ${_ratioToPercent(_kNoiseThreshold)}. '
+ 'Noise level is ${_ratioToPercent(timeseries.noise)}.',
+ );
+ return false;
+ });
+
+ // If any of the score data needs to continue to be collected, we should
+ // return true.
+ final bool finalDecision =
+ shouldContinueList.any((bool element) => element);
+ if (!finalDecision) {
+ print(buffer.toString());
+ }
+ return finalDecision;
+ }
+
+ /// Returns a JSON representation of the profile that will be sent to the
+ /// server.
+ Map<String, dynamic> toJson() {
+ final List<String> scoreKeys = <String>[];
+ final Map<String, dynamic> json = <String, dynamic>{
+ 'name': name,
+ 'scoreKeys': scoreKeys,
+ };
+
+ for (final String key in scoreData.keys) {
+ scoreKeys.add('$key.average');
+ final Timeseries timeseries = scoreData[key];
+ json['$key.average'] = timeseries.average;
+ json['$key.noise'] = timeseries.noise;
}
- print(
- 'SUCCESS: Benchmark converged below ${_kNoiseThreshold * 100}%. '
- 'Noise level is ${profile.drawFrameDurationNoise * 100}%.',
- );
- return false;
- }
+ json.addAll(extraData);
- Profile _generateProfile() {
- final List<FrameMetrics> measuredFrames =
- _frames.sublist(_frames.length - _kMeasuredSampleCount);
- final Iterable<double> noiseCheckDrawFrameTimes =
- measuredFrames.map<double>((FrameMetrics metric) =>
- metric.drawFrameDuration.inMicroseconds.toDouble());
- final double averageDrawFrameDurationMicros =
- _computeMean(noiseCheckDrawFrameTimes);
- final double standardDeviation =
- _computeStandardDeviationForPopulation(noiseCheckDrawFrameTimes);
- final double drawFrameDurationNoise =
- standardDeviation / averageDrawFrameDurationMicros;
-
- return Profile._(
- name: name,
- averageDrawFrameDuration:
- Duration(microseconds: averageDrawFrameDurationMicros.toInt()),
- drawFrameDurationNoise: drawFrameDurationNoise,
- frames: measuredFrames,
- );
- }
-}
-
-/// Contains metrics for a series of rendered frames.
-@immutable
-class Profile {
- Profile._({
- @required this.name,
- @required this.drawFrameDurationNoise,
- @required this.averageDrawFrameDuration,
- @required List<FrameMetrics> frames,
- }) : frames = List<FrameMetrics>.unmodifiable(frames);
-
- /// The name of the benchmark that produced this profile.
- final String name;
-
- /// Average amount of time [Window.onDrawFrame] took.
- final Duration averageDrawFrameDuration;
-
- /// The noise, as a fraction of [averageDrawFrameDuration], measure from the [frames].
- final double drawFrameDurationNoise;
-
- /// Frame metrics recorded during a single benchmark run.
- final List<FrameMetrics> frames;
-
- Map<String, dynamic> toJson() {
- return <String, dynamic>{
- 'name': name,
- 'scoreKeys': <String>['averageDrawFrameDuration'],
- 'averageDrawFrameDuration': averageDrawFrameDuration.inMicroseconds,
- 'drawFrameDurationNoise': drawFrameDurationNoise,
- 'frames': frames
- .map((FrameMetrics frameMetrics) => frameMetrics.toJson())
- .toList(),
- };
+ return json;
}
@override
String toString() {
- return _formatToStringLines(<String>[
- 'benchmark: $name',
- 'averageDrawFrameDuration: ${averageDrawFrameDuration.inMicroseconds}μs',
- 'drawFrameDurationNoise: ${drawFrameDurationNoise * 100}%',
- 'frames:',
- ...frames.expand((FrameMetrics frame) =>
- '$frame\n'.split('\n').map((String line) => '- $line\n')),
- ]);
+ final StringBuffer buffer = StringBuffer();
+ buffer.writeln('name: $name');
+ for (final String key in scoreData.keys) {
+ final Timeseries timeseries = scoreData[key];
+ buffer.writeln('$key:');
+ buffer.writeln(' | average: ${timeseries.average} μs');
+ buffer.writeln(' | noise: ${_ratioToPercent(timeseries.noise)}');
+ }
+ for (final String key in extraData.keys) {
+ final dynamic value = extraData[key];
+ if (value is List) {
+ buffer.writeln('$key:');
+ for (final dynamic item in value) {
+ buffer.writeln(' - $item');
+ }
+ } else {
+ buffer.writeln('$key: $value');
+ }
+ }
+ return buffer.toString();
}
}
-/// Contains metrics for a single frame.
-class FrameMetrics {
- FrameMetrics._({
- @required this.drawFrameDuration,
- @required this.sceneBuildDuration,
- @required this.windowRenderDuration,
- });
-
- /// Total amount of time taken by [Window.onDrawFrame].
- final Duration drawFrameDuration;
-
- /// The amount of time [SceneBuilder.build] took.
- final Duration sceneBuildDuration;
-
- /// The amount of time [Window.render] took.
- final Duration windowRenderDuration;
-
- Map<String, dynamic> toJson() {
- return <String, dynamic>{
- 'drawFrameDuration': drawFrameDuration.inMicroseconds,
- if (sceneBuildDuration != null)
- 'sceneBuildDuration': sceneBuildDuration.inMicroseconds,
- if (windowRenderDuration != null)
- 'windowRenderDuration': windowRenderDuration.inMicroseconds,
- };
- }
-
- @override
- String toString() {
- return _formatToStringLines(<String>[
- 'drawFrameDuration: ${drawFrameDuration.inMicroseconds}μs',
- if (sceneBuildDuration != null)
- 'sceneBuildDuration: ${sceneBuildDuration.inMicroseconds}μs',
- if (windowRenderDuration != null)
- 'windowRenderDuration: ${windowRenderDuration.inMicroseconds}μs',
- ]);
- }
-}
-
-String _formatToStringLines(List<String> lines) {
- return lines
- .map((String line) => line.trim())
- .where((String line) => line.isNotEmpty)
- .join('\n');
-}
-
/// Computes the arithmetic mean (or average) of given [values].
-double _computeMean(Iterable<double> values) {
- final double sum = values.reduce((double a, double b) => a + b);
+double _computeMean(Iterable<num> values) {
+ final num sum = values.reduce((num a, num b) => a + b);
return sum / values.length;
}
@@ -511,20 +593,24 @@
/// See also:
///
/// * https://en.wikipedia.org/wiki/Standard_deviation
-double _computeStandardDeviationForPopulation(Iterable<double> population) {
+double _computeStandardDeviationForPopulation(Iterable<num> population) {
final double mean = _computeMean(population);
final double sumOfSquaredDeltas = population.fold<double>(
0.0,
- (double previous, double value) => previous += math.pow(value - mean, 2),
+ (double previous, num value) => previous += math.pow(value - mean, 2),
);
return math.sqrt(sumOfSquaredDeltas / population.length);
}
+String _ratioToPercent(double value) {
+ return '${(value * 100).toStringAsFixed(2)}%';
+}
+
/// Implemented by recorders that use [_RecordingWidgetsBinding] to receive
/// frame life-cycle calls.
-abstract class _RecordingWidgetsBindingListener {
- /// Whether the binding should continue pumping frames.
- bool _shouldContinue();
+abstract class RecordingWidgetsBindingListener {
+ /// The profile where the benchmark is collecting metrics.
+ Profile profile;
/// Called just before calling [SchedulerBinding.handleDrawFrame].
void _frameWillDraw();
@@ -563,10 +649,11 @@
return WidgetsBinding.instance as _RecordingWidgetsBinding;
}
- _RecordingWidgetsBindingListener _listener;
+ RecordingWidgetsBindingListener _listener;
bool _hasErrored = false;
- void _beginRecording(_RecordingWidgetsBindingListener recorder, Widget widget) {
+ void _beginRecording(
+ RecordingWidgetsBindingListener recorder, Widget widget) {
final FlutterExceptionHandler originalOnError = FlutterError.onError;
// Fail hard and fast on errors. Benchmarks should not have any errors.
@@ -582,7 +669,7 @@
runApp(widget);
}
- /// To avoid calling [Recorder._shouldContinue] every time [scheduleFrame] is
+ /// To avoid calling [Profile.shouldContinue] every time [scheduleFrame] is
/// called, we cache this value at the beginning of the frame.
bool _benchmarkStopped = false;
@@ -592,7 +679,7 @@
if (_hasErrored) {
return;
}
- _benchmarkStopped = !_listener._shouldContinue();
+ _benchmarkStopped = !_listener.profile.shouldContinue();
super.handleBeginFrame(rawTimeStamp);
}
diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
index a5b6f21..8d88442 100644
--- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
@@ -6,6 +6,7 @@
import 'dart:convert' show json;
import 'dart:html' as html;
+import 'package:macrobenchmarks/src/web/bench_text_layout.dart';
import 'package:macrobenchmarks/src/web/bench_text_out_of_picture_bounds.dart';
import 'src/web/bench_build_material_checkbox.dart';
@@ -17,6 +18,8 @@
typedef RecorderFactory = Recorder Function();
+const bool isCanvasKit = bool.fromEnvironment('FLUTTER_WEB_USE_SKIA', defaultValue: false);
+
/// List of all benchmarks that run in the devicelab.
///
/// When adding a new benchmark, add it to this map. Make sure that the name
@@ -27,6 +30,12 @@
BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(),
BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(),
BenchBuildMaterialCheckbox.benchmarkName: () => BenchBuildMaterialCheckbox(),
+
+ // Benchmarks that we don't want to run using CanvasKit.
+ if (!isCanvasKit) ...<String, RecorderFactory>{
+ BenchTextDomLayout.benchmarkName: () => BenchTextDomLayout(),
+ BenchTextDomCachedLayout.benchmarkName: () => BenchTextDomCachedLayout(),
+ }
};
/// Whether we fell back to manual mode.
diff --git a/dev/devicelab/lib/tasks/web_benchmarks.dart b/dev/devicelab/lib/tasks/web_benchmarks.dart
index 0211fe0..8d41970 100644
--- a/dev/devicelab/lib/tasks/web_benchmarks.dart
+++ b/dev/devicelab/lib/tasks/web_benchmarks.dart
@@ -138,10 +138,6 @@
throw 'Score key is empty in benchmark "$benchmarkName". '
'Received [${scoreKeys.join(', ')}]';
}
- if (scoreKey.contains('.')) {
- throw 'Score key contain dots in benchmark "$benchmarkName". '
- 'Received [${scoreKeys.join(', ')}]';
- }
benchmarkScoreKeys.add('$namespace.$scoreKey');
}