[web] Add frame number support. (#171592)
Fixes https://github.com/flutter/flutter/issues/170972
### Description
- Adds `frameData` with `frameNumber` value to `FrameService`
- Adds non-mock `frameData` to `EngineFlutterWindow` and
`EnginePlatformDispatcher`
- Adds `frameNumber` value to `FrameTimingRecorder`'s recorded timings
## Pre-launch Checklist
- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart
index 2e2dbf1..613e3a6 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart
@@ -36,18 +36,16 @@
_instance = mock;
}
- /// A monotonically increasing frame number being rendered.
- ///
- /// This is intended for tests only.
- int get debugFrameNumber => _debugFrameNumber;
- int _debugFrameNumber = 0;
+ /// The [ui.FrameData] object for the current frame.
+ ui.FrameData get frameData => _frameData;
+ ui.FrameData _frameData = const ui.FrameData();
- /// Resets [debugFrameNumber] back to zero.
+ /// Resets [frameData] back to the initial value.
///
/// This is intended for tests only.
@visibleForTesting
- void debugResetFrameNumber() {
- _debugFrameNumber = 0;
+ void debugResetFrameData() {
+ _frameData = const ui.FrameData();
}
/// Whether a frame has already been scheduled.
@@ -102,7 +100,7 @@
try {
_isRenderingFrame = true;
- _debugFrameNumber += 1;
+ _frameData = ui.FrameData(frameNumber: _frameData.frameNumber + 1);
_renderFrame(highResTime.toDartDouble);
} finally {
_isRenderingFrame = false;
@@ -132,7 +130,7 @@
Timer.run(() {
_isRenderingFrame = true;
- _debugFrameNumber += 1;
+ _frameData = ui.FrameData(frameNumber: _frameData.frameNumber + 1);
// TODO(yjbanov): it's funky that if beginFrame crashes, the drawFrame
// fires anyway. We should clean this up, or better explain
// what the expectations are for various situations. The
@@ -158,6 +156,7 @@
}
void _renderFrame(double highResTime) {
+ FrameTimingRecorder.recordCurrentFrameNumber(_frameData.frameNumber);
FrameTimingRecorder.recordCurrentFrameVsync();
// In Flutter terminology "building a frame" consists of "beginning
@@ -181,6 +180,10 @@
);
}
+ if (EnginePlatformDispatcher.instance.onFrameDataChanged != null) {
+ EnginePlatformDispatcher.instance.invokeOnFrameDataChanged();
+ }
+
if (EnginePlatformDispatcher.instance.onDrawFrame != null) {
// On mobile Flutter flushes microtasks between onBeginFrame and
// onDrawFrame. The web doesn't because there's no way to hook into the
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart
index 0f329ab..0833dd3 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart
@@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:meta/meta.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
class FrameTimingRecorder {
+ final int _frameNumber = _currentFrameNumber;
final int _vsyncStartMicros = _currentFrameVsyncStart;
final int _buildStartMicros = _currentFrameBuildStart;
@@ -18,12 +20,30 @@
/// This list is periodically reported to the framework (see [_kFrameTimingsSubmitInterval]).
static List<ui.FrameTiming> _frameTimings = <ui.FrameTiming>[];
- /// These two metrics are collected early in the process, before the respective
+ /// List of the collected frame timings that are not yet reported.
+ ///
+ /// This is intended for tests only.
+ @visibleForTesting
+ static List<ui.FrameTiming> get debugFrameTimings => _frameTimings;
+
+ @visibleForTesting
+ static void debugResetFrameTimings() {
+ _frameTimings = <ui.FrameTiming>[];
+ }
+
+ /// These three metrics are collected early in the process, before the respective
/// scene builders are created. These are instead treated as global state, which
/// are used to initialize any recorders that are created by the scene builders.
+ static int _currentFrameNumber = 0;
static int _currentFrameVsyncStart = 0;
static int _currentFrameBuildStart = 0;
+ static void recordCurrentFrameNumber(int frameNumber) {
+ if (frameTimingsEnabled) {
+ _currentFrameNumber = frameNumber;
+ }
+ }
+
static void recordCurrentFrameVsync() {
if (frameTimingsEnabled) {
_currentFrameVsyncStart = _nowMicros();
@@ -36,6 +56,21 @@
}
}
+ @visibleForTesting
+ static void debugResetCurrentFrameNumber() {
+ _currentFrameNumber = 0;
+ }
+
+ @visibleForTesting
+ static void debugResetCurrentFrameVsync() {
+ _currentFrameVsyncStart = 0;
+ }
+
+ @visibleForTesting
+ static void debugResetCurrentFrameBuildStart() {
+ _currentFrameBuildStart = 0;
+ }
+
/// The last time (in microseconds) we submitted frame timings.
static int _frameTimingsLastSubmitTime = _nowMicros();
@@ -87,6 +122,7 @@
rasterStart: _rasterStartMicros!,
rasterFinish: _rasterFinishMicros!,
rasterFinishWallTime: _rasterFinishMicros!,
+ frameNumber: _frameNumber,
);
_frameTimings.add(timing);
final int now = _nowMicros();
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
index d7f87a3..73db507 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
@@ -1292,8 +1292,26 @@
});
}
+ /// The [ui.FrameData] object for the current frame.
@override
- ui.FrameData get frameData => const ui.FrameData.webOnly();
+ ui.FrameData get frameData => FrameService.instance.frameData;
+
+ /// A callback that is invoked when the window updates the [ui.FrameData].
+ @override
+ ui.VoidCallback? get onFrameDataChanged => _onFrameDataChanged;
+ ui.VoidCallback? _onFrameDataChanged;
+ Zone _onFrameDataChangedZone = Zone.root;
+ @override
+ set onFrameDataChanged(ui.VoidCallback? callback) {
+ _onFrameDataChanged = callback;
+ _onFrameDataChangedZone = Zone.current;
+ }
+
+ /// Engine code should use this method instead of the callback directly.
+ /// Otherwise zones won't work properly.
+ void invokeOnFrameDataChanged() {
+ invoke(onFrameDataChanged, _onFrameDataChangedZone);
+ }
@override
double scaleFontSize(double unscaledFontSize) => unscaledFontSize * textScaleFactor;
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart
index badd6dc..efb9ae4 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart
@@ -475,12 +475,14 @@
}
@override
- ui.FrameData get frameData => const ui.FrameData.webOnly();
+ ui.FrameData get frameData => platformDispatcher.frameData;
@override
- ui.VoidCallback? get onFrameDataChanged => null;
+ ui.VoidCallback? get onFrameDataChanged => platformDispatcher.onFrameDataChanged;
@override
- set onFrameDataChanged(ui.VoidCallback? callback) {}
+ set onFrameDataChanged(ui.VoidCallback? callback) {
+ platformDispatcher.onFrameDataChanged = callback;
+ }
@override
ui.AccessibilityFeatures get accessibilityFeatures => platformDispatcher.accessibilityFeatures;
diff --git a/engine/src/flutter/lib/web_ui/lib/window.dart b/engine/src/flutter/lib/web_ui/lib/window.dart
index 5564cb5..3412e6a 100644
--- a/engine/src/flutter/lib/web_ui/lib/window.dart
+++ b/engine/src/flutter/lib/web_ui/lib/window.dart
@@ -165,9 +165,15 @@
SingletonFlutterWindow get window => engine.window;
class FrameData {
- const FrameData.webOnly();
+ const FrameData({this.frameNumber = 0});
- int get frameNumber => -1;
+ /// The number of the current frame.
+ ///
+ /// This number monotonically increases, but doesn't necessarily
+ /// start at a particular value.
+ ///
+ /// If not provided, defaults to 0.
+ final int frameNumber;
}
class GestureSettings {
diff --git a/engine/src/flutter/lib/web_ui/test/engine/frame_service_test.dart b/engine/src/flutter/lib/web_ui/test/engine/frame_service_test.dart
index 261bd43..e99e0e2 100644
--- a/engine/src/flutter/lib/web_ui/test/engine/frame_service_test.dart
+++ b/engine/src/flutter/lib/web_ui/test/engine/frame_service_test.dart
@@ -34,22 +34,22 @@
test('counts frames', () async {
final instance = FrameService.instance;
- instance.debugResetFrameNumber();
+ instance.debugResetFrameData();
final frameCompleter = Completer<void>();
instance.onFinishedRenderingFrame = () {
frameCompleter.complete();
};
- expect(instance.debugFrameNumber, 0);
+ expect(instance.frameData.frameNumber, 0);
instance.scheduleFrame();
await frameCompleter.future;
- expect(instance.debugFrameNumber, 1);
+ expect(instance.frameData.frameNumber, 1);
});
- test('isFrameScheduled is true iff the frame is scheduled', () async {
+ test('isFrameScheduled is true if the frame is scheduled', () async {
final instance = FrameService.instance;
- instance.debugResetFrameNumber();
+ instance.debugResetFrameData();
var frameCompleter = Completer<void>();
instance.onFinishedRenderingFrame = () {
@@ -62,10 +62,10 @@
expect(instance.isFrameScheduled, isTrue);
await frameCompleter.future;
expect(instance.isFrameScheduled, isFalse);
- expect(instance.debugFrameNumber, 1);
+ expect(instance.frameData.frameNumber, 1);
// Test idempotency
- instance.debugResetFrameNumber();
+ instance.debugResetFrameData();
frameCompleter = Completer<void>();
instance.scheduleFrame();
instance.scheduleFrame();
@@ -75,7 +75,7 @@
expect(instance.isFrameScheduled, isTrue);
await frameCompleter.future;
expect(instance.isFrameScheduled, isFalse);
- expect(instance.debugFrameNumber, 1);
+ expect(instance.frameData.frameNumber, 1);
});
test('onBeginFrame and onDrawFrame are called with isRenderingFrame set to true', () async {
diff --git a/engine/src/flutter/lib/web_ui/test/engine/frame_timing_recorder_test.dart b/engine/src/flutter/lib/web_ui/test/engine/frame_timing_recorder_test.dart
new file mode 100644
index 0000000..10c1d42
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/test/engine/frame_timing_recorder_test.dart
@@ -0,0 +1,74 @@
+// 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.
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/src/engine.dart';
+import 'package:ui/ui.dart' as ui;
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+void testMain() {
+ group('FrameTimingRecorder', () {
+ setUp(() {
+ EnginePlatformDispatcher.instance.onReportTimings = null;
+ FrameTimingRecorder.debugResetCurrentFrameNumber();
+ FrameTimingRecorder.debugResetCurrentFrameVsync();
+ FrameTimingRecorder.debugResetCurrentFrameBuildStart();
+ FrameTimingRecorder.debugResetFrameTimings();
+ });
+
+ tearDown(() {
+ EnginePlatformDispatcher.instance.onReportTimings = null;
+ FrameTimingRecorder.debugResetCurrentFrameNumber();
+ FrameTimingRecorder.debugResetCurrentFrameVsync();
+ FrameTimingRecorder.debugResetCurrentFrameBuildStart();
+ FrameTimingRecorder.debugResetFrameTimings();
+ });
+
+ test('frameTimingsEnabled is false when onReportTimings is not set', () {
+ expect(EnginePlatformDispatcher.instance.onReportTimings, isNull);
+ expect(FrameTimingRecorder.frameTimingsEnabled, isFalse);
+ });
+
+ test('frameTimingsEnabled is true when onReportTimings is set', () {
+ EnginePlatformDispatcher.instance.onReportTimings = (_) {};
+ expect(EnginePlatformDispatcher.instance.onReportTimings, isNotNull);
+ expect(FrameTimingRecorder.frameTimingsEnabled, isTrue);
+ });
+
+ test('uses recorded frame number', () {
+ EnginePlatformDispatcher.instance.onReportTimings = (_) {};
+ expect(FrameTimingRecorder.frameTimingsEnabled, isTrue);
+
+ const int frameNumber1 = 333;
+ FrameTimingRecorder.recordCurrentFrameNumber(frameNumber1);
+
+ FrameTimingRecorder()
+ ..recordBuildFinish()
+ ..recordRasterStart()
+ ..recordRasterFinish()
+ ..submitTimings();
+
+ expect(FrameTimingRecorder.debugFrameTimings.length, 1);
+ final ui.FrameTiming timing1 = FrameTimingRecorder.debugFrameTimings.first;
+ expect(timing1.frameNumber, frameNumber1);
+
+ const int frameNumber2 = 334;
+ FrameTimingRecorder.recordCurrentFrameNumber(frameNumber2);
+
+ FrameTimingRecorder()
+ ..recordBuildFinish()
+ ..recordRasterStart()
+ ..recordRasterFinish()
+ ..submitTimings();
+
+ expect(FrameTimingRecorder.debugFrameTimings.length, 2);
+ final ui.FrameTiming timing2 = FrameTimingRecorder.debugFrameTimings.last;
+ expect(timing2.frameNumber, frameNumber2);
+ });
+ });
+}
diff --git a/engine/src/flutter/lib/web_ui/test/engine/window_test.dart b/engine/src/flutter/lib/web_ui/test/engine/window_test.dart
index bef2e09..33c8c4c 100644
--- a/engine/src/flutter/lib/web_ui/test/engine/window_test.dart
+++ b/engine/src/flutter/lib/web_ui/test/engine/window_test.dart
@@ -396,6 +396,23 @@
expect(responded, isTrue);
});
+ test('onFrameDataChanged preserves the zone', () {
+ final Zone innerZone = Zone.current.fork();
+
+ innerZone.runGuarded(() {
+ void callback() {
+ expect(Zone.current, innerZone);
+ }
+
+ myWindow.onFrameDataChanged = callback;
+
+ // Test that the getter returns the exact same callback, e.g. it doesn't wrap it.
+ expect(myWindow.onFrameDataChanged, same(callback));
+ });
+
+ EnginePlatformDispatcher.instance.invokeOnFrameDataChanged();
+ });
+
// Emulates the framework sending a request for screen orientation lock.
Future<bool> sendSetPreferredOrientations(List<dynamic> orientations) {
final Completer<bool> completer = Completer<bool>();