-
Notifications
You must be signed in to change notification settings - Fork 6k
[web] Profile text layout and forward data to macrobenchmarks #17276
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For better accuracy I think you want to stop before sending the number.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, you're right. I think you can just not call |
||
| Profiler.instance.benchmark('text_layout', stopwatch.elapsedMicroseconds); | ||
| } | ||
|
|
||
| _lastUsedConstraints = constraints; | ||
|
|
||
| if (_geometricStyle.maxLines != null) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<BenchmarkDatapoint> data = <BenchmarkDatapoint>[]; | ||
| jsOnBenchmark((String name, num value) { | ||
| data.add(BenchmarkDatapoint(name, value)); | ||
| }); | ||
|
|
||
| Profiler.instance.benchmark('foo', 123); | ||
| expect(data, <BenchmarkDatapoint>[BenchmarkDatapoint('foo', 123)]); | ||
| data.clear(); | ||
|
|
||
| Profiler.instance.benchmark('bar', 0.0125); | ||
| expect(data, <BenchmarkDatapoint>[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<BenchmarkDatapoint> data = <BenchmarkDatapoint>[]; | ||
|
|
||
| // Wrong callback signature. | ||
| jsOnBenchmark((num value) { | ||
| data.add(BenchmarkDatapoint('bad', value)); | ||
| }); | ||
| expect( | ||
| () => Profiler.instance.benchmark('foo', 123), | ||
| throwsA(isA<TypeError>()), | ||
| ); | ||
| expect(data, isEmpty); | ||
|
|
||
| // Not even a callback. | ||
| jsOnBenchmark('string'); | ||
| expect( | ||
| () => Profiler.instance.benchmark('foo', 123), | ||
| throwsA(isA<TypeError>()), | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move into a shared function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about doing this initially, but decided it may not tree-shake well when I move the
throwinto a function. But now that tree-shaking is out of the picture anyway, I think it's reasonable to put this logic in a shared function.