From cefdb133d24385b64d65c0d5df079bd80dc3f848 Mon Sep 17 00:00:00 2001 From: Yuqian Li Date: Thu, 15 Aug 2019 15:31:44 -0700 Subject: [PATCH 1/4] Add a BroadcastStream to FrameTiming --- lib/ui/window.dart | 46 +++++++++++++++++++++++++++++++++++ testing/dart/window_test.dart | 38 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 2858b422a362d..87f79cd7d555b 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -928,9 +928,11 @@ class Window { /// Flutter spends less than 0.1ms every 1 second to report the timings /// (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for /// 60fps), or 0.01% CPU usage per second. + @Deprecated('Use frameTimingStream instead.') TimingsCallback get onReportTimings => _onReportTimings; TimingsCallback _onReportTimings; Zone _onReportTimingsZone; + @Deprecated('Use frameTimingStream instead.') set onReportTimings(TimingsCallback callback) { if ((callback == null) != (_onReportTimings == null)) { _setNeedsReportTimings(callback != null); @@ -939,6 +941,50 @@ class Window { _onReportTimingsZone = Zone.current; } + /// Mock the calling of [onReportTimings] for unit tests. + void debugReportTimings(List timings) { + onReportTimings(timings); + } + + /// Check whether the engine has to report timings. + /// + /// This is for unit tests and debug purposes only. + bool get debugNeedsReportTimings => onReportTimings != null; + + StreamController _frameTimingBroadcastController; + + void _onFrameTimingListen() { + onReportTimings = (List timings) { + timings.forEach(_frameTimingBroadcastController.add); + }; + } + + // If there's no one listening, set [onReportTimings] back to null so the + // engine won't send [FrameTiming] from engine to the framework. + void _onFrameTimingCancel() { + onReportTimings = null; + } + + /// A broadcast stream of the frames' time-related performance metrics. + /// + /// It's recommended to listen to this stream instead of overriding + /// [Window.onReportTimings] directly because the latter may accidentally + /// break other listeners or callbacks. + /// + /// See also: + /// + /// * [FrameTiming], the data event of this stream + /// * [onReportTimings], the low level callback that this stream and the + /// Flutter engine use. It's recommended to use this stream instead of + /// overriding [onReportTimings] directly. + Stream get frameTimingStream { + _frameTimingBroadcastController ??= StreamController.broadcast( + onListen: _onFrameTimingListen, + onCancel: _onFrameTimingCancel, + ); + return _frameTimingBroadcastController.stream; + } + _SetNeedsReportTimingsFunc _setNeedsReportTimings; void _nativeSetNeedsReportTimings(bool value) native 'Window_setNeedsReportTimings'; diff --git a/testing/dart/window_test.dart b/testing/dart/window_test.dart index 6a4ed12bbcefc..c197077a2af36 100644 --- a/testing/dart/window_test.dart +++ b/testing/dart/window_test.dart @@ -24,4 +24,42 @@ void main() { FrameTiming timing = FrameTiming([1000, 8000, 9000, 19500]); expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)'); }); + + test('window.frameTimingStream works', () async { + // Test a single subscription. Check that debugNeedsReportTimings is + // properly reset after the subscription is cancelled. + expect(window.debugNeedsReportTimings, false); + final FrameTiming mockTiming = FrameTiming([1000, 8000, 9000, 19500]); + final Future frameTiming = window.frameTimingStream.first; + expect(window.debugNeedsReportTimings, true); + window.debugReportTimings([mockTiming]); + expect(await frameTiming, equals(mockTiming)); + expect(window.debugNeedsReportTimings, false); + + // Test multiple (two) subscriptions after that debugNeedsReportTimings has + // been reset to false by the single subscription test above. + // + // Subscription 1 + final Future timingFuture = window.frameTimingStream.first; + // + // Subscription 2 + final List timings = []; + final Completer completer = Completer(); + int frameCount = 0; + window.frameTimingStream.listen((FrameTiming t) { + timings.add(t); + frameCount += 1; + if (frameCount == 2) { + completer.complete(); + } + }); + + final FrameTiming secondMock = FrameTiming([1, 2, 3, 4]); + window.debugReportTimings([secondMock, secondMock]); + final FrameTiming timing = await timingFuture; + expect(timing != mockTiming, isTrue); + expect(timing, equals(secondMock)); + await completer.future; + expect(timings, hasLength(2)); + }); } From 855b2747b7ee6947f627c7700c0b410f2620052a Mon Sep 17 00:00:00 2001 From: Yuqian Li Date: Thu, 15 Aug 2019 16:13:40 -0700 Subject: [PATCH 2/4] Fix deprecated calls --- lib/ui/hooks.dart | 2 +- lib/ui/window.dart | 75 +++++++++++++++++++++++----------------------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 6f0fa3a2d8adf..33e98b8dab907 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -187,7 +187,7 @@ void _reportTimings(List timings) { for (int i = 0; i < timings.length; i += FramePhase.values.length) { frameTimings.add(FrameTiming(timings.sublist(i, i + FramePhase.values.length))); } - _invoke1(window.onReportTimings, window._onReportTimingsZone, frameTimings); + _invoke1(window._onReportTimings, window._onReportTimingsZone, frameTimings); } @pragma('vm:entry-point') diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 87f79cd7d555b..305a36380a225 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -10,17 +10,17 @@ typedef VoidCallback = void Function(); /// Signature for [Window.onBeginFrame]. typedef FrameCallback = void Function(Duration duration); -/// Signature for [Window.onReportTimings]. +/// Signature for [Window._onReportTimings]. /// -/// {@template dart.ui.TimingsCallback.list} -/// The callback takes a list of [FrameTiming] because it may not be -/// immediately triggered after each frame. Instead, Flutter tries to batch -/// frames together and send all their timings at once to decrease the -/// overhead (as this is available in the release mode). The list is sorted in -/// ascending order of time (earliest frame first). The timing of any frame -/// will be sent within about 1 second (100ms if in the profile/debug mode) -/// even if there are no later frames to batch. The timing of the first frame -/// will be sent immediately without batching. +/// The callback takes a list of [FrameTiming] because it may not be immediately +/// triggered after each frame. The list is sorted in ascending order of time +/// (earliest frame first). +/// {@template dart.ui.timings_batching} +/// Flutter tries to batch frames together and send all their timings at once to +/// decrease the overhead (as this is available in the release mode). The timing +/// of any frame will be sent within about 1 second (100ms if in the +/// profile/debug mode) even if there are no later frames to batch. The timing +/// of the first frame will be sent immediately without batching. /// {@endtemplate} typedef TimingsCallback = void Function(List timings); @@ -69,7 +69,7 @@ enum FramePhase { /// Time-related performance metrics of a frame. /// -/// See [Window.onReportTimings] for how to get this. +/// See [Window.frameTimingStream] for how to get this. /// /// The metrics in debug mode (`flutter run` without any flags) may be very /// different from those in profile and release modes due to the debug overhead. @@ -82,7 +82,7 @@ class FrameTiming { /// [FramePhase.values]. /// /// This constructor is usually only called by the Flutter engine, or a test. - /// To get the [FrameTiming] of your app, see [Window.onReportTimings]. + /// To get the [FrameTiming] of your app, see [Window.frameTimingStream]. FrameTiming(List timestamps) : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; @@ -914,26 +914,17 @@ class Window { /// A callback that is invoked to report the [FrameTiming] of recently /// rasterized frames. /// - /// This can be used to see if the application has missed frames (through - /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high - /// latencies (through [FrameTiming.totalSpan]). - /// - /// Unlike [Timeline], the timing information here is available in the release - /// mode (additional to the profile and the debug mode). Hence this can be - /// used to monitor the application's performance in the wild. - /// - /// {@macro dart.ui.TimingsCallback.list} - /// - /// If this is null, no additional work will be done. If this is not null, - /// Flutter spends less than 0.1ms every 1 second to report the timings - /// (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for - /// 60fps), or 0.01% CPU usage per second. + /// This is deprecated, use [frameTimingStream] instead. @Deprecated('Use frameTimingStream instead.') TimingsCallback get onReportTimings => _onReportTimings; TimingsCallback _onReportTimings; Zone _onReportTimingsZone; @Deprecated('Use frameTimingStream instead.') set onReportTimings(TimingsCallback callback) { + _internalSetOnReportTimings(callback); + } + + void _internalSetOnReportTimings(TimingsCallback callback) { if ((callback == null) != (_onReportTimings == null)) { _setNeedsReportTimings(callback != null); } @@ -941,42 +932,50 @@ class Window { _onReportTimingsZone = Zone.current; } - /// Mock the calling of [onReportTimings] for unit tests. + /// Mock the calling of [_onReportTimings] for unit tests. void debugReportTimings(List timings) { - onReportTimings(timings); + _onReportTimings(timings); } /// Check whether the engine has to report timings. /// /// This is for unit tests and debug purposes only. - bool get debugNeedsReportTimings => onReportTimings != null; + bool get debugNeedsReportTimings => _onReportTimings != null; StreamController _frameTimingBroadcastController; void _onFrameTimingListen() { - onReportTimings = (List timings) { + _internalSetOnReportTimings((List timings) { timings.forEach(_frameTimingBroadcastController.add); - }; + }); } // If there's no one listening, set [onReportTimings] back to null so the // engine won't send [FrameTiming] from engine to the framework. void _onFrameTimingCancel() { - onReportTimings = null; + _internalSetOnReportTimings(null); } /// A broadcast stream of the frames' time-related performance metrics. /// - /// It's recommended to listen to this stream instead of overriding - /// [Window.onReportTimings] directly because the latter may accidentally - /// break other listeners or callbacks. + /// This can be used to see if the application has missed frames (through + /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high + /// latencies (through [FrameTiming.totalSpan]). + /// + /// Unlike [Timeline], the timing information here is available in the release + /// mode (additional to the profile and the debug mode). Hence this can be + /// used to monitor the application's performance in the wild. + /// + /// {@macro dart.ui.timings_batching} + /// + /// If no one is listening to this stream, no additional work will be done. + /// Otherwise, Flutter spends less than 0.1ms every 1 second to report the + /// timings (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame + /// budget for 60fps), or 0.01% CPU usage per second. /// /// See also: /// /// * [FrameTiming], the data event of this stream - /// * [onReportTimings], the low level callback that this stream and the - /// Flutter engine use. It's recommended to use this stream instead of - /// overriding [onReportTimings] directly. Stream get frameTimingStream { _frameTimingBroadcastController ??= StreamController.broadcast( onListen: _onFrameTimingListen, From ae505ab67a4dce9a8f5348ab6ae5fd654ac3bab9 Mon Sep 17 00:00:00 2001 From: Yuqian Li Date: Fri, 16 Aug 2019 11:52:04 -0700 Subject: [PATCH 3/4] More fixes --- lib/ui/window.dart | 24 ++++--- lib/web_ui/lib/src/ui/window.dart | 112 ++++++++++++++++++++++-------- testing/dart/window_test.dart | 8 +-- 3 files changed, 100 insertions(+), 44 deletions(-) diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 305a36380a225..aa38559c7b40b 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -10,7 +10,8 @@ typedef VoidCallback = void Function(); /// Signature for [Window.onBeginFrame]. typedef FrameCallback = void Function(Duration duration); -/// Signature for [Window._onReportTimings]. +// ignore: deprecated_member_use_from_same_package +/// Signature for [Window.onReportTimings]. /// /// The callback takes a list of [FrameTiming] because it may not be immediately /// triggered after each frame. The list is sorted in ascending order of time @@ -69,7 +70,7 @@ enum FramePhase { /// Time-related performance metrics of a frame. /// -/// See [Window.frameTimingStream] for how to get this. +/// See [Window.frameTimings] for how to get this. /// /// The metrics in debug mode (`flutter run` without any flags) may be very /// different from those in profile and release modes due to the debug overhead. @@ -82,7 +83,7 @@ class FrameTiming { /// [FramePhase.values]. /// /// This constructor is usually only called by the Flutter engine, or a test. - /// To get the [FrameTiming] of your app, see [Window.frameTimingStream]. + /// To get the [FrameTiming] of your app, see [Window.frameTimings]. FrameTiming(List timestamps) : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; @@ -914,12 +915,12 @@ class Window { /// A callback that is invoked to report the [FrameTiming] of recently /// rasterized frames. /// - /// This is deprecated, use [frameTimingStream] instead. - @Deprecated('Use frameTimingStream instead.') + /// This is deprecated, use [frameTimings] instead. + @Deprecated('Use frameTimings instead.') TimingsCallback get onReportTimings => _onReportTimings; TimingsCallback _onReportTimings; Zone _onReportTimingsZone; - @Deprecated('Use frameTimingStream instead.') + @Deprecated('Use frameTimings instead.') set onReportTimings(TimingsCallback callback) { _internalSetOnReportTimings(callback); } @@ -932,7 +933,8 @@ class Window { _onReportTimingsZone = Zone.current; } - /// Mock the calling of [_onReportTimings] for unit tests. + // ignore: deprecated_member_use_from_same_package + /// Mock the calling of [onReportTimings] for unit tests. void debugReportTimings(List timings) { _onReportTimings(timings); } @@ -963,20 +965,20 @@ class Window { /// latencies (through [FrameTiming.totalSpan]). /// /// Unlike [Timeline], the timing information here is available in the release - /// mode (additional to the profile and the debug mode). Hence this can be - /// used to monitor the application's performance in the wild. + /// mode (additional to profile and debug mode). Hence this can be used to + /// monitor the application's performance in the wild. /// /// {@macro dart.ui.timings_batching} /// /// If no one is listening to this stream, no additional work will be done. /// Otherwise, Flutter spends less than 0.1ms every 1 second to report the - /// timings (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame + /// timings (measured on iPhone 6s). The 0.1ms is about 0.6% of 16ms (frame /// budget for 60fps), or 0.01% CPU usage per second. /// /// See also: /// /// * [FrameTiming], the data event of this stream - Stream get frameTimingStream { + Stream get frameTimings { _frameTimingBroadcastController ??= StreamController.broadcast( onListen: _onFrameTimingListen, onCancel: _onFrameTimingCancel, diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index 64df7504c0d9a..cf85c41ca3798 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -16,7 +16,19 @@ typedef VoidCallback = void Function(); /// common time base. typedef FrameCallback = void Function(Duration duration); +// ignore: deprecated_member_use_from_same_package /// Signature for [Window.onReportTimings]. +/// +/// The callback takes a list of [FrameTiming] because it may not be immediately +/// triggered after each frame. The list is sorted in ascending order of time +/// (earliest frame first). +/// {@template dart.ui.timings_batching} +/// Flutter tries to batch frames together and send all their timings at once to +/// decrease the overhead (as this is available in the release mode). The timing +/// of any frame will be sent within about 1 second (100ms if in the +/// profile/debug mode) even if there are no later frames to batch. The timing +/// of the first frame will be sent immediately without batching. +/// {@endtemplate} typedef TimingsCallback = void Function(List timings); /// Signature for [Window.onPointerDataPacket]. @@ -764,27 +776,77 @@ abstract class Window { /// A callback that is invoked to report the [FrameTiming] of recently /// rasterized frames. /// - /// This can be used to see if the application has missed frames (through - /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high - /// latencies (through [FrameTiming.totalSpan]). - /// - /// Unlike [Timeline], the timing information here is available in the release - /// mode (additional to the profile and the debug mode). Hence this can be - /// used to monitor the application's performance in the wild. - /// - /// The callback may not be immediately triggered after each frame. Instead, - /// it tries to batch frames together and send all their timings at once to - /// decrease the overhead (as this is available in the release mode). The - /// timing of any frame will be sent within about 1 second even if there are - /// no later frames to batch. + /// This is deprecated, use [frameTimings] instead. + @Deprecated('Use frameTimings instead.') TimingsCallback get onReportTimings => _onReportTimings; TimingsCallback _onReportTimings; Zone _onReportTimingsZone; + @Deprecated('Use frameTimings instead.') set onReportTimings(TimingsCallback callback) { + _internalSetOnReportTimings(callback); + } + + void _internalSetOnReportTimings(TimingsCallback callback) { + if ((callback == null) != (_onReportTimings == null)) { + _setNeedsReportTimings(callback != null); + } _onReportTimings = callback; _onReportTimingsZone = Zone.current; } + // ignore: deprecated_member_use_from_same_package + /// Mock the calling of [onReportTimings] for unit tests. + void debugReportTimings(List timings) { + _onReportTimings(timings); + } + + /// Check whether the engine has to report timings. + /// + /// This is for unit tests and debug purposes only. + bool get debugNeedsReportTimings => _onReportTimings != null; + + StreamController _frameTimingBroadcastController; + + void _onFrameTimingListen() { + _internalSetOnReportTimings((List timings) { + timings.forEach(_frameTimingBroadcastController.add); + }); + } + + // If there's no one listening, set [onReportTimings] back to null so the + // engine won't send [FrameTiming] from engine to the framework. + void _onFrameTimingCancel() { + _internalSetOnReportTimings(null); + } + + /// A broadcast stream of the frames' time-related performance metrics. + /// + /// This can be used to see if the application has missed frames (through + /// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high + /// latencies (through [FrameTiming.totalSpan]). + /// + /// Unlike [Timeline], the timing information here is available in the release + /// mode (additional to profile and debug mode). Hence this can be used to + /// monitor the application's performance in the wild. + /// + /// {@macro dart.ui.timings_batching} + /// + /// If no one is listening to this stream, no additional work will be done. + /// Otherwise, Flutter spends less than 0.1ms every 1 second to report the + /// timings (measured on iPhone 6s). The 0.1ms is about 0.6% of 16ms (frame + /// budget for 60fps), or 0.01% CPU usage per second. + /// + /// See also: + /// + /// * [FrameTiming], the data event of this stream + Stream get frameTimings { + _frameTimingBroadcastController ??= StreamController.broadcast( + onListen: _onFrameTimingListen, + onCancel: _onFrameTimingCancel, + ); + return _frameTimingBroadcastController.stream; + } + /// A callback that is invoked for each frame after [onBeginFrame] has /// completed and after the microtask queue has been drained. This can be /// used to implement a second phase of frame rendering that happens @@ -1151,7 +1213,7 @@ enum FramePhase { /// Time-related performance metrics of a frame. /// -/// See [Window.onReportTimings] for how to get this. +/// See [Window.frameTimings] for how to get this. /// /// The metrics in debug mode (`flutter run` without any flags) may be very /// different from those in profile and release modes due to the debug overhead. @@ -1164,17 +1226,15 @@ class FrameTiming { /// [FramePhase.values]. /// /// This constructor is usually only called by the Flutter engine, or a test. - /// To get the [FrameTiming] of your app, see [Window.onReportTimings]. + /// To get the [FrameTiming] of your app, see [Window.frameTimings]. FrameTiming(List timestamps) - : assert(timestamps.length == FramePhase.values.length), - _timestamps = timestamps; + : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; /// This is a raw timestamp in microseconds from some epoch. The epoch in all /// [FrameTiming] is the same, but it may not match [DateTime]'s epoch. int timestampInMicroseconds(FramePhase phase) => _timestamps[phase.index]; - Duration _rawDuration(FramePhase phase) => - Duration(microseconds: _timestamps[phase.index]); + Duration _rawDuration(FramePhase phase) => Duration(microseconds: _timestamps[phase.index]); /// The duration to build the frame on the UI thread. /// @@ -1191,17 +1251,13 @@ class FrameTiming { /// {@template dart.ui.FrameTiming.fps_milliseconds} /// That's about 16ms for 60fps, and 8ms for 120fps. /// {@endtemplate} - Duration get buildDuration => - _rawDuration(FramePhase.buildFinish) - - _rawDuration(FramePhase.buildStart); + Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); /// The duration to rasterize the frame on the GPU thread. /// /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} /// {@macro dart.ui.FrameTiming.fps_milliseconds} - Duration get rasterDuration => - _rawDuration(FramePhase.rasterFinish) - - _rawDuration(FramePhase.rasterStart); + Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); /// The timespan between build start and raster finish. /// @@ -1210,11 +1266,9 @@ class FrameTiming { /// {@macro dart.ui.FrameTiming.fps_milliseconds} /// /// See also [buildDuration] and [rasterDuration]. - Duration get totalSpan => - _rawDuration(FramePhase.rasterFinish) - - _rawDuration(FramePhase.buildStart); + Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.buildStart); - final List _timestamps; // in microseconds + final List _timestamps; // in microseconds String _formatMS(Duration duration) => '${duration.inMicroseconds * 0.001}ms'; diff --git a/testing/dart/window_test.dart b/testing/dart/window_test.dart index c197077a2af36..33e68013183c8 100644 --- a/testing/dart/window_test.dart +++ b/testing/dart/window_test.dart @@ -25,12 +25,12 @@ void main() { expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)'); }); - test('window.frameTimingStream works', () async { + test('window.frameTimings works', () async { // Test a single subscription. Check that debugNeedsReportTimings is // properly reset after the subscription is cancelled. expect(window.debugNeedsReportTimings, false); final FrameTiming mockTiming = FrameTiming([1000, 8000, 9000, 19500]); - final Future frameTiming = window.frameTimingStream.first; + final Future frameTiming = window.frameTimings.first; expect(window.debugNeedsReportTimings, true); window.debugReportTimings([mockTiming]); expect(await frameTiming, equals(mockTiming)); @@ -40,13 +40,13 @@ void main() { // been reset to false by the single subscription test above. // // Subscription 1 - final Future timingFuture = window.frameTimingStream.first; + final Future timingFuture = window.frameTimings.first; // // Subscription 2 final List timings = []; final Completer completer = Completer(); int frameCount = 0; - window.frameTimingStream.listen((FrameTiming t) { + window.frameTimings.listen((FrameTiming t) { timings.add(t); frameCount += 1; if (frameCount == 2) { From 7440bfb6f5b8ce71481dca97471f99a79c7d25f4 Mon Sep 17 00:00:00 2001 From: Yuqian Li Date: Sat, 17 Aug 2019 13:57:33 -0700 Subject: [PATCH 4/4] Fix web_ui --- lib/web_ui/lib/src/ui/window.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index cf85c41ca3798..05b5ab31e0aaa 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -787,9 +787,6 @@ abstract class Window { } void _internalSetOnReportTimings(TimingsCallback callback) { - if ((callback == null) != (_onReportTimings == null)) { - _setNeedsReportTimings(callback != null); - } _onReportTimings = callback; _onReportTimingsZone = Zone.current; }