diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index ebc6f87e28903..f9cfcab303213 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -448,6 +448,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/render_vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 04e688ff090e7..aea106762cbe4 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -69,6 +69,7 @@ part 'engine/platform_views.dart'; part 'engine/plugins.dart'; part 'engine/pointer_binding.dart'; part 'engine/pointer_converter.dart'; +part 'engine/profiler.dart'; part 'engine/render_vertices.dart'; part 'engine/rrect_renderer.dart'; part 'engine/semantics/accessibility.dart'; @@ -164,6 +165,10 @@ void webOnlyInitializeEngine() { WebExperiments.ensureInitialized(); + if (Profiler.isBenchmarkMode) { + Profiler.ensureInitialized(); + } + bool waitingForAnimation = false; ui.webOnlyScheduleFrameCallback = () { // We're asked to schedule a frame and call `frameHandler` when the frame diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart new file mode 100644 index 0000000000000..383f480917042 --- /dev/null +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -0,0 +1,72 @@ +// Copyright 2013 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. + +// @dart = 2.6 +part of engine; + +typedef OnBenchmark = void Function(String name, num value); + +/// The purpose of this class is to facilitate communication of +/// profiling/benchmark data to the outside world (e.g. a macrobenchmark that's +/// running a flutter app). +/// +/// To use the [Profiler]: +/// +/// 1. Set the environment variable `FLUTTER_WEB_ENABLE_PROFILING` to true. +/// +/// 2. Using JS interop, assign a listener function to +/// `window._flutter_internal_on_benchmark` in the browser. +/// +/// The listener function will be called every time a new benchmark number is +/// calculated. The signature is `Function(String name, num value)`. +class Profiler { + Profiler._() { + _checkBenchmarkMode(); + } + + static bool isBenchmarkMode = const bool.fromEnvironment( + 'FLUTTER_WEB_ENABLE_PROFILING', + defaultValue: true, + ); + + static Profiler ensureInitialized() { + _checkBenchmarkMode(); + return Profiler._instance ??= Profiler._(); + } + + static Profiler get instance { + _checkBenchmarkMode(); + if (_instance == null) { + throw Exception( + 'Profiler has not been properly initialized. ' + 'Make sure Profiler.ensureInitialized() is being called before you ' + 'access Profiler.instance', + ); + } + return _instance; + } + + static Profiler _instance; + + static void _checkBenchmarkMode() { + if (!isBenchmarkMode) { + throw Exception( + 'Cannot use Profiler unless benchmark mode is enabled. ' + 'You can enable it by setting the `FLUTTER_WEB_ENABLE_PROFILING` ' + 'environment variable to true.', + ); + } + } + + /// Used to send benchmark data to whoever is listening to them. + void benchmark(String name, num value) { + _checkBenchmarkMode(); + + final OnBenchmark onBenchmark = + js_util.getProperty(html.window, '_flutter_internal_on_benchmark'); + if (onBenchmark != null) { + onBenchmark(name, value); + } + } +} diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 83ec07b875f25..3dbc6fbecafe1 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -255,7 +255,16 @@ class EngineParagraph implements ui.Paragraph { return; } + Stopwatch stopwatch; + if (Profiler.isBenchmarkMode) { + stopwatch = Stopwatch()..start(); + } _measurementResult = _measurementService.measure(this, constraints); + if (Profiler.isBenchmarkMode) { + stopwatch.stop(); + Profiler.instance.benchmark('text_layout', stopwatch.elapsedMicroseconds); + } + _lastUsedConstraints = constraints; if (_geometricStyle.maxLines != null) { diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 3a63ee87085d7..ce9d1499caca2 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -13,6 +13,7 @@ import 'mock_engine_canvas.dart'; void main() { setUpAll(() { WebExperiments.ensureInitialized(); + Profiler.ensureInitialized(); }); group('EngineCanvas', () { diff --git a/lib/web_ui/test/engine/profiler_test.dart b/lib/web_ui/test/engine/profiler_test.dart new file mode 100644 index 0000000000000..58f12d17cf758 --- /dev/null +++ b/lib/web_ui/test/engine/profiler_test.dart @@ -0,0 +1,99 @@ +// Copyright 2013 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. + +// @dart = 2.6 +import 'dart:html' as html; +import 'dart:js_util' as js_util; + +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +void main() { + setUp(() { + Profiler.isBenchmarkMode = true; + Profiler.ensureInitialized(); + }); + + tearDown(() { + jsOnBenchmark(null); + Profiler.isBenchmarkMode = false; + }); + + test('works when there is no listener', () { + expect(() => Profiler.instance.benchmark('foo', 123), returnsNormally); + }); + + test('can listen to benchmarks', () { + final List data = []; + jsOnBenchmark((String name, num value) { + data.add(BenchmarkDatapoint(name, value)); + }); + + Profiler.instance.benchmark('foo', 123); + expect(data, [BenchmarkDatapoint('foo', 123)]); + data.clear(); + + Profiler.instance.benchmark('bar', 0.0125); + expect(data, [BenchmarkDatapoint('bar', 0.0125)]); + data.clear(); + + // Remove listener and make sure nothing breaks and the data isn't being + // sent to the old callback anymore. + jsOnBenchmark(null); + expect(() => Profiler.instance.benchmark('baz', 99.999), returnsNormally); + expect(data, isEmpty); + }); + + test('throws on wrong listener type', () { + final List data = []; + + // Wrong callback signature. + jsOnBenchmark((num value) { + data.add(BenchmarkDatapoint('bad', value)); + }); + expect( + () => Profiler.instance.benchmark('foo', 123), + throwsA(isA()), + ); + expect(data, isEmpty); + + // Not even a callback. + jsOnBenchmark('string'); + expect( + () => Profiler.instance.benchmark('foo', 123), + throwsA(isA()), + ); + }); +} + +class BenchmarkDatapoint { + BenchmarkDatapoint(this.name, this.value); + + final String name; + final num value; + + @override + int get hashCode => hashValues(name, value); + + @override + operator ==(dynamic other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return name == other.name && value == other.value; + } + + @override + String toString() { + return '$runtimeType("$name", $value)'; + } +} + +void jsOnBenchmark(dynamic listener) { + js_util.setProperty(html.window, '_flutter_internal_on_benchmark', listener); +} diff --git a/lib/web_ui/test/paragraph_builder_test.dart b/lib/web_ui/test/paragraph_builder_test.dart index 54a3bcf53b046..6aa9d0015bef6 100644 --- a/lib/web_ui/test/paragraph_builder_test.dart +++ b/lib/web_ui/test/paragraph_builder_test.dart @@ -11,6 +11,7 @@ import 'package:test/test.dart'; void main() { setUpAll(() { WebExperiments.ensureInitialized(); + Profiler.ensureInitialized(); }); test('Should be able to build and layout a paragraph', () {