Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 3ee6f25

Browse files
Implement frame timing callbacks in Skwasm. (#50737)
Fixes flutter/flutter#140429 Some notes here: * Refactored the frame timing systems so that we can deal with asynchronous rendering. * Consolidated rendering of multiple pictures in skwasm into a single call, so that the rasterization can be properly measured. * Pulled the frame timings tests into the `ui` test suite so that they run on all renderers (including skwasm).
1 parent 843ecc5 commit 3ee6f25

25 files changed

+400
-338
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10324,6 +10324,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart + ../
1032410324
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart + ../../../flutter/LICENSE
1032510325
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart + ../../../flutter/LICENSE
1032610326
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart + ../../../flutter/LICENSE
10327+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart + ../../../flutter/LICENSE
1032710328
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart + ../../../flutter/LICENSE
1032810329
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart + ../../../flutter/LICENSE
1032910330
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart + ../../../flutter/LICENSE
@@ -13161,6 +13162,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart
1316113162
FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart
1316213163
FILE: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart
1316313164
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart
13165+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart
1316413166
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart
1316513167
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
1316613168
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export 'engine/font_fallback_data.dart';
6565
export 'engine/font_fallbacks.dart';
6666
export 'engine/fonts.dart';
6767
export 'engine/frame_reference.dart';
68+
export 'engine/frame_timing_recorder.dart';
6869
export 'engine/html/backdrop_filter.dart';
6970
export 'engine/html/bitmap_canvas.dart';
7071
export 'engine/html/canvas.dart';

lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ abstract class DisplayCanvas {
131131
typedef RenderRequest = ({
132132
ui.Scene scene,
133133
Completer<void> completer,
134+
FrameTimingRecorder? recorder,
134135
});
135136

136137
/// A per-view queue of render requests. Only contains the current render

lib/web_ui/lib/src/engine/canvaskit/renderer.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -417,16 +417,17 @@ class CanvasKitRenderer implements Renderer {
417417
"Unable to render to a view which hasn't been registered");
418418
final ViewRasterizer rasterizer = _rasterizers[view.viewId]!;
419419
final RenderQueue renderQueue = rasterizer.queue;
420+
final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null;
420421
if (renderQueue.current != null) {
421422
// If a scene is already queued up, drop it and queue this one up instead
422423
// so that the scene view always displays the most recently requested scene.
423424
renderQueue.next?.completer.complete();
424425
final Completer<void> completer = Completer<void>();
425-
renderQueue.next = (scene: scene, completer: completer);
426+
renderQueue.next = (scene: scene, completer: completer, recorder: recorder);
426427
return completer.future;
427428
}
428429
final Completer<void> completer = Completer<void>();
429-
renderQueue.current = (scene: scene, completer: completer);
430+
renderQueue.current = (scene: scene, completer: completer, recorder: recorder);
430431
unawaited(_kickRenderLoop(rasterizer));
431432
return completer.future;
432433
}
@@ -435,7 +436,7 @@ class CanvasKitRenderer implements Renderer {
435436
final RenderQueue renderQueue = rasterizer.queue;
436437
final RenderRequest current = renderQueue.current!;
437438
try {
438-
await _renderScene(current.scene, rasterizer);
439+
await _renderScene(current.scene, rasterizer, current.recorder);
439440
current.completer.complete();
440441
} catch (error, stackTrace) {
441442
current.completer.completeError(error, stackTrace);
@@ -449,19 +450,20 @@ class CanvasKitRenderer implements Renderer {
449450
}
450451
}
451452

452-
Future<void> _renderScene(ui.Scene scene, ViewRasterizer rasterizer) async {
453+
Future<void> _renderScene(ui.Scene scene, ViewRasterizer rasterizer, FrameTimingRecorder? recorder) async {
453454
// "Build finish" and "raster start" happen back-to-back because we
454455
// render on the same thread, so there's no overhead from hopping to
455456
// another thread.
456457
//
457458
// CanvasKit works differently from the HTML renderer in that in HTML
458459
// we update the DOM in SceneBuilder.build, which is these function calls
459460
// here are CanvasKit-only.
460-
frameTimingsOnBuildFinish();
461-
frameTimingsOnRasterStart();
461+
recorder?.recordBuildFinish();
462+
recorder?.recordRasterStart();
462463

463464
await rasterizer.draw((scene as LayerScene).layerTree);
464-
frameTimingsOnRasterFinish();
465+
recorder?.recordRasterFinish();
466+
recorder?.submitTimings();
465467
}
466468

467469
// Map from view id to the associated Rasterizer for that view.

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1487,7 +1487,7 @@ class DomCanvasRenderingContextBitmapRenderer {}
14871487

14881488
extension DomCanvasRenderingContextBitmapRendererExtension
14891489
on DomCanvasRenderingContextBitmapRenderer {
1490-
external void transferFromImageBitmap(DomImageBitmap bitmap);
1490+
external void transferFromImageBitmap(DomImageBitmap? bitmap);
14911491
}
14921492

14931493
@JS('ImageData')
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:ui/src/engine.dart';
6+
import 'package:ui/ui.dart' as ui;
7+
8+
class FrameTimingRecorder {
9+
final int _vsyncStartMicros = _currentFrameVsyncStart;
10+
final int _buildStartMicros = _currentFrameBuildStart;
11+
12+
int? _buildFinishMicros;
13+
int? _rasterStartMicros;
14+
int? _rasterFinishMicros;
15+
16+
/// Collects frame timings from frames.
17+
///
18+
/// This list is periodically reported to the framework (see [_kFrameTimingsSubmitInterval]).
19+
static List<ui.FrameTiming> _frameTimings = <ui.FrameTiming>[];
20+
21+
/// These two metrics are collected early in the process, before the respective
22+
/// scene builders are created. These are instead treated as global state, which
23+
/// are used to initialize any recorders that are created by the scene builders.
24+
static int _currentFrameVsyncStart = 0;
25+
static int _currentFrameBuildStart = 0;
26+
27+
static void recordCurrentFrameVsync() {
28+
if (frameTimingsEnabled) {
29+
_currentFrameVsyncStart = _nowMicros();
30+
}
31+
}
32+
33+
static void recordCurrentFrameBuildStart() {
34+
if (frameTimingsEnabled) {
35+
_currentFrameBuildStart = _nowMicros();
36+
}
37+
}
38+
39+
/// The last time (in microseconds) we submitted frame timings.
40+
static int _frameTimingsLastSubmitTime = _nowMicros();
41+
/// The amount of time in microseconds we wait between submitting
42+
/// frame timings.
43+
static const int _kFrameTimingsSubmitInterval = 100000; // 100 milliseconds
44+
45+
/// Whether we are collecting [ui.FrameTiming]s.
46+
static bool get frameTimingsEnabled {
47+
return EnginePlatformDispatcher.instance.onReportTimings != null;
48+
}
49+
50+
/// Current timestamp in microseconds taken from the high-precision
51+
/// monotonically increasing timer.
52+
///
53+
/// See also:
54+
///
55+
/// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now,
56+
/// particularly notes about Firefox rounding to 1ms for security reasons,
57+
/// which can be bypassed in tests by setting certain browser options.
58+
static int _nowMicros() {
59+
return (domWindow.performance.now() * 1000).toInt();
60+
}
61+
62+
void recordBuildFinish([int? buildFinish]) {
63+
assert(_buildFinishMicros == null, "can't record build finish more than once");
64+
_buildFinishMicros = buildFinish ?? _nowMicros();
65+
}
66+
67+
void recordRasterStart([int? rasterStart]) {
68+
assert(_rasterStartMicros == null, "can't record raster start more than once");
69+
_rasterStartMicros = rasterStart ?? _nowMicros();
70+
}
71+
72+
void recordRasterFinish([int? rasterFinish]) {
73+
assert(_rasterFinishMicros == null, "can't record raster finish more than once");
74+
_rasterFinishMicros = rasterFinish ?? _nowMicros();
75+
}
76+
77+
void submitTimings() {
78+
assert(
79+
_buildFinishMicros != null &&
80+
_rasterStartMicros != null &&
81+
_rasterFinishMicros != null,
82+
'Attempted to submit an incomplete timings.'
83+
);
84+
final ui.FrameTiming timing = ui.FrameTiming(
85+
vsyncStart: _vsyncStartMicros,
86+
buildStart: _buildStartMicros,
87+
buildFinish: _buildFinishMicros!,
88+
rasterStart: _rasterStartMicros!,
89+
rasterFinish: _rasterFinishMicros!,
90+
rasterFinishWallTime: _rasterFinishMicros!,
91+
);
92+
_frameTimings.add(timing);
93+
final int now = _nowMicros();
94+
if (now - _frameTimingsLastSubmitTime > _kFrameTimingsSubmitInterval) {
95+
_frameTimingsLastSubmitTime = now;
96+
EnginePlatformDispatcher.instance.invokeOnReportTimings(_frameTimings);
97+
_frameTimings = <ui.FrameTiming>[];
98+
}
99+
}
100+
}

lib/web_ui/lib/src/engine/html/renderer.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,11 @@ class HtmlRenderer implements Renderer {
323323
@override
324324
Future<void> renderScene(ui.Scene scene, ui.FlutterView view) async {
325325
final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!;
326-
implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!);
327-
frameTimingsOnRasterFinish();
326+
scene as SurfaceScene;
327+
implicitView.dom.setScene(scene.webOnlyRootElement!);
328+
final FrameTimingRecorder? recorder = scene.timingRecorder;
329+
recorder?.recordRasterFinish();
330+
recorder?.submitTimings();
328331
}
329332

330333
@override

lib/web_ui/lib/src/engine/html/scene.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,20 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:ui/src/engine/display.dart';
5+
import 'package:ui/src/engine.dart';
66
import 'package:ui/ui.dart' as ui;
77

8-
import '../dom.dart';
9-
import '../vector_math.dart';
10-
import '../window.dart';
11-
import 'surface.dart';
12-
138
class SurfaceScene implements ui.Scene {
149
/// This class is created by the engine, and should not be instantiated
1510
/// or extended directly.
1611
///
1712
/// To create a Scene object, use a [SceneBuilder].
18-
SurfaceScene(this.webOnlyRootElement);
13+
SurfaceScene(this.webOnlyRootElement, {
14+
required this.timingRecorder,
15+
});
1916

2017
final DomElement? webOnlyRootElement;
18+
final FrameTimingRecorder? timingRecorder;
2119

2220
/// Creates a raster image representation of the current state of the scene.
2321
/// This is a slow operation that is performed on a background thread.

lib/web_ui/lib/src/engine/html/scene_builder.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'dart:typed_data';
77
import 'package:ui/ui.dart' as ui;
88
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
99

10-
import '../../engine.dart' show kProfileApplyFrame, kProfilePrerollFrame;
10+
import '../../engine.dart' show FrameTimingRecorder, kProfileApplyFrame, kProfilePrerollFrame;
1111
import '../display.dart';
1212
import '../dom.dart';
1313
import '../picture.dart';
@@ -511,8 +511,9 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
511511
// In the HTML renderer we time the beginning of the rasterization phase
512512
// (counter-intuitively) in SceneBuilder.build because DOM updates happen
513513
// here. This is different from CanvasKit.
514-
frameTimingsOnBuildFinish();
515-
frameTimingsOnRasterStart();
514+
final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null;
515+
recorder?.recordBuildFinish();
516+
recorder?.recordRasterStart();
516517
timeAction<void>(kProfilePrerollFrame, () {
517518
while (_surfaceStack.length > 1) {
518519
// Auto-pop layers that were pushed without a corresponding pop.
@@ -528,7 +529,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
528529
}
529530
commitScene(_persistedScene);
530531
_lastFrameScene = _persistedScene;
531-
return SurfaceScene(_persistedScene.rootElement);
532+
return SurfaceScene(_persistedScene.rootElement, timingRecorder: recorder);
532533
});
533534
}
534535

lib/web_ui/lib/src/engine/initialization.dart

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,15 @@ Future<void> initializeEngineServices({
158158
if (!waitingForAnimation) {
159159
waitingForAnimation = true;
160160
domWindow.requestAnimationFrame((JSNumber highResTime) {
161-
frameTimingsOnVsync();
161+
FrameTimingRecorder.recordCurrentFrameVsync();
162+
163+
// In Flutter terminology "building a frame" consists of "beginning
164+
// frame" and "drawing frame".
165+
//
166+
// We do not call `recordBuildFinish` from here because
167+
// part of the rasterization process, particularly in the HTML
168+
// renderer, takes place in the `SceneBuilder.build()`.
169+
FrameTimingRecorder.recordCurrentFrameBuildStart();
162170

163171
// Reset immediately, because `frameHandler` can schedule more frames.
164172
waitingForAnimation = false;
@@ -171,13 +179,6 @@ Future<void> initializeEngineServices({
171179
final int highResTimeMicroseconds =
172180
(1000 * highResTime.toDartDouble).toInt();
173181

174-
// In Flutter terminology "building a frame" consists of "beginning
175-
// frame" and "drawing frame".
176-
//
177-
// We do not call `frameTimingsOnBuildFinish` from here because
178-
// part of the rasterization process, particularly in the HTML
179-
// renderer, takes place in the `SceneBuilder.build()`.
180-
frameTimingsOnBuildStart();
181182
if (EnginePlatformDispatcher.instance.onBeginFrame != null) {
182183
EnginePlatformDispatcher.instance.invokeOnBeginFrame(
183184
Duration(microseconds: highResTimeMicroseconds));

0 commit comments

Comments
 (0)