Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/ui/hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ void _reportTimings(List<int> 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')
Expand Down
97 changes: 72 additions & 25 deletions lib/ui/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ typedef VoidCallback = void Function();
/// Signature for [Window.onBeginFrame].
typedef FrameCallback = void Function(Duration duration);

// ignore: deprecated_member_use_from_same_package
/// 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<FrameTiming> timings);

Expand Down Expand Up @@ -69,7 +70,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.
Expand All @@ -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.onReportTimings].
/// To get the [FrameTiming] of your app, see [Window.frameTimings].
FrameTiming(List<int> timestamps)
: assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps;

Expand Down Expand Up @@ -914,31 +915,77 @@ 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 [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<FrameTiming> 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<FrameTiming> _frameTimingBroadcastController;

void _onFrameTimingListen() {
_internalSetOnReportTimings((List<FrameTiming> timings) {
timings.forEach(_frameTimingBroadcastController.add);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it more efficient to call timings.forEach(_frameTimingBroadcastController.add), or:

for (FrameTiming timing in timings) {
  _frameTimingBroadcastController.add(timing);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe forEach is ok here since we are passing in a method directly and do not allocate a closure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I write a for ( in ) loop, the analyzer will complain... It forces me to write forEach.

});
}

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop the "the" before "release", "profile" and "debug"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// 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<FrameTiming> get frameTimings {
_frameTimingBroadcastController ??= StreamController<FrameTiming>.broadcast(
onListen: _onFrameTimingListen,
onCancel: _onFrameTimingCancel,
);
return _frameTimingBroadcastController.stream;
}

_SetNeedsReportTimingsFunc _setNeedsReportTimings;
void _nativeSetNeedsReportTimings(bool value) native 'Window_setNeedsReportTimings';

Expand Down
109 changes: 80 additions & 29 deletions lib/web_ui/lib/src/ui/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<FrameTiming> timings);

/// Signature for [Window.onPointerDataPacket].
Expand Down Expand Up @@ -764,27 +776,74 @@ 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) {
_onReportTimings = callback;
_onReportTimingsZone = Zone.current;
}

// ignore: deprecated_member_use_from_same_package
/// Mock the calling of [onReportTimings] for unit tests.
void debugReportTimings(List<FrameTiming> 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<FrameTiming> _frameTimingBroadcastController;

void _onFrameTimingListen() {
_internalSetOnReportTimings((List<FrameTiming> 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<FrameTiming> get frameTimings {
_frameTimingBroadcastController ??= StreamController<FrameTiming>.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
Expand Down Expand Up @@ -1151,7 +1210,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.
Expand All @@ -1164,17 +1223,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<int> 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.
///
Expand All @@ -1191,17 +1248,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.
///
Expand All @@ -1210,11 +1263,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<int> _timestamps; // in microseconds
final List<int> _timestamps; // in microseconds

String _formatMS(Duration duration) => '${duration.inMicroseconds * 0.001}ms';

Expand Down
38 changes: 38 additions & 0 deletions testing/dart/window_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,42 @@ void main() {
FrameTiming timing = FrameTiming(<int>[1000, 8000, 9000, 19500]);
expect(timing.toString(), 'FrameTiming(buildDuration: 7.0ms, rasterDuration: 10.5ms, totalSpan: 18.5ms)');
});

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(<int>[1000, 8000, 9000, 19500]);
final Future<FrameTiming> frameTiming = window.frameTimings.first;
expect(window.debugNeedsReportTimings, true);
window.debugReportTimings(<FrameTiming>[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<FrameTiming> timingFuture = window.frameTimings.first;
//
// Subscription 2
final List<FrameTiming> timings = <FrameTiming>[];
final Completer<void> completer = Completer<void>();
int frameCount = 0;
window.frameTimings.listen((FrameTiming t) {
timings.add(t);
frameCount += 1;
if (frameCount == 2) {
completer.complete();
}
});

final FrameTiming secondMock = FrameTiming(<int>[1, 2, 3, 4]);
window.debugReportTimings(<FrameTiming>[secondMock, secondMock]);
final FrameTiming timing = await timingFuture;
expect(timing != mockTiming, isTrue);
expect(timing, equals(secondMock));
await completer.future;
expect(timings, hasLength(2));
});
}