diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 2d40298c8be5d..d287ed8733f88 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -10324,7 +10324,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart + ../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart + ../../../flutter/LICENSE @@ -13162,7 +13161,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 0fbda333aac87..9697966fe3bf0 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -65,7 +65,6 @@ export 'engine/font_fallback_data.dart'; export 'engine/font_fallbacks.dart'; export 'engine/fonts.dart'; export 'engine/frame_reference.dart'; -export 'engine/frame_timing_recorder.dart'; export 'engine/html/backdrop_filter.dart'; export 'engine/html/bitmap_canvas.dart'; export 'engine/html/canvas.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index bdf6d744f7152..58a23e871cc92 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -131,7 +131,6 @@ abstract class DisplayCanvas { typedef RenderRequest = ({ ui.Scene scene, Completer completer, - FrameTimingRecorder? recorder, }); /// A per-view queue of render requests. Only contains the current render diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 7d673a63c6b3e..f4fdaef67c0f9 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -417,17 +417,16 @@ class CanvasKitRenderer implements Renderer { "Unable to render to a view which hasn't been registered"); final ViewRasterizer rasterizer = _rasterizers[view.viewId]!; final RenderQueue renderQueue = rasterizer.queue; - final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; if (renderQueue.current != null) { // If a scene is already queued up, drop it and queue this one up instead // so that the scene view always displays the most recently requested scene. renderQueue.next?.completer.complete(); final Completer completer = Completer(); - renderQueue.next = (scene: scene, completer: completer, recorder: recorder); + renderQueue.next = (scene: scene, completer: completer); return completer.future; } final Completer completer = Completer(); - renderQueue.current = (scene: scene, completer: completer, recorder: recorder); + renderQueue.current = (scene: scene, completer: completer); unawaited(_kickRenderLoop(rasterizer)); return completer.future; } @@ -436,7 +435,7 @@ class CanvasKitRenderer implements Renderer { final RenderQueue renderQueue = rasterizer.queue; final RenderRequest current = renderQueue.current!; try { - await _renderScene(current.scene, rasterizer, current.recorder); + await _renderScene(current.scene, rasterizer); current.completer.complete(); } catch (error, stackTrace) { current.completer.completeError(error, stackTrace); @@ -450,7 +449,7 @@ class CanvasKitRenderer implements Renderer { } } - Future _renderScene(ui.Scene scene, ViewRasterizer rasterizer, FrameTimingRecorder? recorder) async { + Future _renderScene(ui.Scene scene, ViewRasterizer rasterizer) async { // "Build finish" and "raster start" happen back-to-back because we // render on the same thread, so there's no overhead from hopping to // another thread. @@ -458,12 +457,11 @@ class CanvasKitRenderer implements Renderer { // CanvasKit works differently from the HTML renderer in that in HTML // we update the DOM in SceneBuilder.build, which is these function calls // here are CanvasKit-only. - recorder?.recordBuildFinish(); - recorder?.recordRasterStart(); + frameTimingsOnBuildFinish(); + frameTimingsOnRasterStart(); await rasterizer.draw((scene as LayerScene).layerTree); - recorder?.recordRasterFinish(); - recorder?.submitTimings(); + frameTimingsOnRasterFinish(); } // Map from view id to the associated Rasterizer for that view. diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 386666d6797ae..750587774c313 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -1487,7 +1487,7 @@ class DomCanvasRenderingContextBitmapRenderer {} extension DomCanvasRenderingContextBitmapRendererExtension on DomCanvasRenderingContextBitmapRenderer { - external void transferFromImageBitmap(DomImageBitmap? bitmap); + external void transferFromImageBitmap(DomImageBitmap bitmap); } @JS('ImageData') diff --git a/lib/web_ui/lib/src/engine/frame_timing_recorder.dart b/lib/web_ui/lib/src/engine/frame_timing_recorder.dart deleted file mode 100644 index ec2944bb41327..0000000000000 --- a/lib/web_ui/lib/src/engine/frame_timing_recorder.dart +++ /dev/null @@ -1,100 +0,0 @@ -// 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:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -class FrameTimingRecorder { - final int _vsyncStartMicros = _currentFrameVsyncStart; - final int _buildStartMicros = _currentFrameBuildStart; - - int? _buildFinishMicros; - int? _rasterStartMicros; - int? _rasterFinishMicros; - - /// Collects frame timings from frames. - /// - /// This list is periodically reported to the framework (see [_kFrameTimingsSubmitInterval]). - static List _frameTimings = []; - - /// These two 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 _currentFrameVsyncStart = 0; - static int _currentFrameBuildStart = 0; - - static void recordCurrentFrameVsync() { - if (frameTimingsEnabled) { - _currentFrameVsyncStart = _nowMicros(); - } - } - - static void recordCurrentFrameBuildStart() { - if (frameTimingsEnabled) { - _currentFrameBuildStart = _nowMicros(); - } - } - - /// The last time (in microseconds) we submitted frame timings. - static int _frameTimingsLastSubmitTime = _nowMicros(); - /// The amount of time in microseconds we wait between submitting - /// frame timings. - static const int _kFrameTimingsSubmitInterval = 100000; // 100 milliseconds - - /// Whether we are collecting [ui.FrameTiming]s. - static bool get frameTimingsEnabled { - return EnginePlatformDispatcher.instance.onReportTimings != null; - } - - /// Current timestamp in microseconds taken from the high-precision - /// monotonically increasing timer. - /// - /// See also: - /// - /// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now, - /// particularly notes about Firefox rounding to 1ms for security reasons, - /// which can be bypassed in tests by setting certain browser options. - static int _nowMicros() { - return (domWindow.performance.now() * 1000).toInt(); - } - - void recordBuildFinish([int? buildFinish]) { - assert(_buildFinishMicros == null, "can't record build finish more than once"); - _buildFinishMicros = buildFinish ?? _nowMicros(); - } - - void recordRasterStart([int? rasterStart]) { - assert(_rasterStartMicros == null, "can't record raster start more than once"); - _rasterStartMicros = rasterStart ?? _nowMicros(); - } - - void recordRasterFinish([int? rasterFinish]) { - assert(_rasterFinishMicros == null, "can't record raster finish more than once"); - _rasterFinishMicros = rasterFinish ?? _nowMicros(); - } - - void submitTimings() { - assert( - _buildFinishMicros != null && - _rasterStartMicros != null && - _rasterFinishMicros != null, - 'Attempted to submit an incomplete timings.' - ); - final ui.FrameTiming timing = ui.FrameTiming( - vsyncStart: _vsyncStartMicros, - buildStart: _buildStartMicros, - buildFinish: _buildFinishMicros!, - rasterStart: _rasterStartMicros!, - rasterFinish: _rasterFinishMicros!, - rasterFinishWallTime: _rasterFinishMicros!, - ); - _frameTimings.add(timing); - final int now = _nowMicros(); - if (now - _frameTimingsLastSubmitTime > _kFrameTimingsSubmitInterval) { - _frameTimingsLastSubmitTime = now; - EnginePlatformDispatcher.instance.invokeOnReportTimings(_frameTimings); - _frameTimings = []; - } - } -} diff --git a/lib/web_ui/lib/src/engine/html/renderer.dart b/lib/web_ui/lib/src/engine/html/renderer.dart index b41fac3739234..c9febef391e37 100644 --- a/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/lib/web_ui/lib/src/engine/html/renderer.dart @@ -323,11 +323,8 @@ class HtmlRenderer implements Renderer { @override Future renderScene(ui.Scene scene, ui.FlutterView view) async { final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; - scene as SurfaceScene; - implicitView.dom.setScene(scene.webOnlyRootElement!); - final FrameTimingRecorder? recorder = scene.timingRecorder; - recorder?.recordRasterFinish(); - recorder?.submitTimings(); + implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!); + frameTimingsOnRasterFinish(); } @override diff --git a/lib/web_ui/lib/src/engine/html/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart index b4deb9a6dac69..f15d043447e4b 100644 --- a/lib/web_ui/lib/src/engine/html/scene.dart +++ b/lib/web_ui/lib/src/engine/html/scene.dart @@ -2,20 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:ui/src/engine.dart'; +import 'package:ui/src/engine/display.dart'; import 'package:ui/ui.dart' as ui; +import '../dom.dart'; +import '../vector_math.dart'; +import '../window.dart'; +import 'surface.dart'; + class SurfaceScene implements ui.Scene { /// This class is created by the engine, and should not be instantiated /// or extended directly. /// /// To create a Scene object, use a [SceneBuilder]. - SurfaceScene(this.webOnlyRootElement, { - required this.timingRecorder, - }); + SurfaceScene(this.webOnlyRootElement); final DomElement? webOnlyRootElement; - final FrameTimingRecorder? timingRecorder; /// Creates a raster image representation of the current state of the scene. /// This is a slow operation that is performed on a background thread. diff --git a/lib/web_ui/lib/src/engine/html/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart index 701bb11ef92ad..e721aa5a5576b 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -7,7 +7,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; -import '../../engine.dart' show FrameTimingRecorder, kProfileApplyFrame, kProfilePrerollFrame; +import '../../engine.dart' show kProfileApplyFrame, kProfilePrerollFrame; import '../display.dart'; import '../dom.dart'; import '../picture.dart'; @@ -511,9 +511,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { // In the HTML renderer we time the beginning of the rasterization phase // (counter-intuitively) in SceneBuilder.build because DOM updates happen // here. This is different from CanvasKit. - final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; - recorder?.recordBuildFinish(); - recorder?.recordRasterStart(); + frameTimingsOnBuildFinish(); + frameTimingsOnRasterStart(); timeAction(kProfilePrerollFrame, () { while (_surfaceStack.length > 1) { // Auto-pop layers that were pushed without a corresponding pop. @@ -529,7 +528,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { } commitScene(_persistedScene); _lastFrameScene = _persistedScene; - return SurfaceScene(_persistedScene.rootElement, timingRecorder: recorder); + return SurfaceScene(_persistedScene.rootElement); }); } diff --git a/lib/web_ui/lib/src/engine/initialization.dart b/lib/web_ui/lib/src/engine/initialization.dart index 745f8b2e84cf7..0dab016be43ba 100644 --- a/lib/web_ui/lib/src/engine/initialization.dart +++ b/lib/web_ui/lib/src/engine/initialization.dart @@ -158,15 +158,7 @@ Future initializeEngineServices({ if (!waitingForAnimation) { waitingForAnimation = true; domWindow.requestAnimationFrame((JSNumber highResTime) { - FrameTimingRecorder.recordCurrentFrameVsync(); - - // In Flutter terminology "building a frame" consists of "beginning - // frame" and "drawing frame". - // - // We do not call `recordBuildFinish` from here because - // part of the rasterization process, particularly in the HTML - // renderer, takes place in the `SceneBuilder.build()`. - FrameTimingRecorder.recordCurrentFrameBuildStart(); + frameTimingsOnVsync(); // Reset immediately, because `frameHandler` can schedule more frames. waitingForAnimation = false; @@ -179,6 +171,13 @@ Future initializeEngineServices({ final int highResTimeMicroseconds = (1000 * highResTime.toDartDouble).toInt(); + // In Flutter terminology "building a frame" consists of "beginning + // frame" and "drawing frame". + // + // We do not call `frameTimingsOnBuildFinish` from here because + // part of the rasterization process, particularly in the HTML + // renderer, takes place in the `SceneBuilder.build()`. + frameTimingsOnBuildStart(); if (EnginePlatformDispatcher.instance.onBeginFrame != null) { EnginePlatformDispatcher.instance.invokeOnBeginFrame( Duration(microseconds: highResTimeMicroseconds)); diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart index d5ef8b3fa831b..ffabd12d2156b 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -5,8 +5,11 @@ import 'dart:async'; import 'dart:js_interop'; +import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; +import 'dom.dart'; +import 'platform_dispatcher.dart'; import 'util.dart'; // TODO(mdebbar): Deprecate this and remove it. @@ -124,6 +127,118 @@ class Profiler { } } +/// Whether we are collecting [ui.FrameTiming]s. +bool get _frameTimingsEnabled { + return EnginePlatformDispatcher.instance.onReportTimings != null; +} + +/// Collects frame timings from frames. +/// +/// This list is periodically reported to the framework (see +/// [_kFrameTimingsSubmitInterval]). +List _frameTimings = []; + +/// The amount of time in microseconds we wait between submitting +/// frame timings. +const int _kFrameTimingsSubmitInterval = 100000; // 100 milliseconds + +/// The last time (in microseconds) we submitted frame timings. +int _frameTimingsLastSubmitTime = _nowMicros(); + +// These variables store individual [ui.FrameTiming] properties. +int _vsyncStartMicros = -1; +int _buildStartMicros = -1; +int _buildFinishMicros = -1; +int _rasterStartMicros = -1; +int _rasterFinishMicros = -1; + +/// Records the vsync timestamp for this frame. +void frameTimingsOnVsync() { + if (!_frameTimingsEnabled) { + return; + } + _vsyncStartMicros = _nowMicros(); +} + +/// Records the time when the framework started building the frame. +void frameTimingsOnBuildStart() { + if (!_frameTimingsEnabled) { + return; + } + _buildStartMicros = _nowMicros(); +} + +/// Records the time when the framework finished building the frame. +void frameTimingsOnBuildFinish() { + if (!_frameTimingsEnabled) { + return; + } + _buildFinishMicros = _nowMicros(); +} + +/// Records the time when the framework started rasterizing the frame. +/// +/// On the web, this value is almost always the same as [_buildFinishMicros] +/// because it's single-threaded so there's no delay between building +/// and rasterization. +/// +/// This also means different things between HTML and CanvasKit renderers. +/// +/// In HTML "rasterization" only captures DOM updates, but not the work that +/// the browser performs after the DOM updates are committed. The browser +/// does not report that information. +/// +/// CanvasKit captures everything because we control the rasterization +/// process, so we know exactly when rasterization starts and ends. +void frameTimingsOnRasterStart() { + if (!_frameTimingsEnabled) { + return; + } + _rasterStartMicros = _nowMicros(); +} + +/// Records the time when the framework started rasterizing the frame. +/// +/// See [_frameTimingsOnRasterStart] for more details on what rasterization +/// timings mean on the web. +void frameTimingsOnRasterFinish() { + if (!_frameTimingsEnabled) { + return; + } + final int now = _nowMicros(); + _rasterFinishMicros = now; + _frameTimings.add(ui.FrameTiming( + vsyncStart: _vsyncStartMicros, + buildStart: _buildStartMicros, + buildFinish: _buildFinishMicros, + rasterStart: _rasterStartMicros, + rasterFinish: _rasterFinishMicros, + rasterFinishWallTime: _rasterFinishMicros, + )); + _vsyncStartMicros = -1; + _buildStartMicros = -1; + _buildFinishMicros = -1; + _rasterStartMicros = -1; + _rasterFinishMicros = -1; + if (now - _frameTimingsLastSubmitTime > _kFrameTimingsSubmitInterval) { + _frameTimingsLastSubmitTime = now; + EnginePlatformDispatcher.instance.invokeOnReportTimings(_frameTimings); + _frameTimings = []; + } +} + +/// Current timestamp in microseconds taken from the high-precision +/// monotonically increasing timer. +/// +/// See also: +/// +/// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now, +/// particularly notes about Firefox rounding to 1ms for security reasons, +/// which can be bypassed in tests by setting certain browser options. +int _nowMicros() { + return (domWindow.performance.now() * 1000).toInt(); +} + /// Counts various events that take place while the app is running. /// /// This class will slow down the app, and therefore should be disabled while diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index b137b70f4f909..23010c6db0bff 100644 --- a/lib/web_ui/lib/src/engine/scene_view.dart +++ b/lib/web_ui/lib/src/engine/scene_view.dart @@ -9,31 +9,20 @@ import 'package:ui/ui.dart' as ui; const String kCanvasContainerTag = 'flt-canvas-container'; -typedef RenderResult = ({ - List imageBitmaps, - int rasterStartMicros, - int rasterEndMicros, -}); - // This is an interface that renders a `ScenePicture` as a `DomImageBitmap`. // It is optionally asynchronous. It is required for the `EngineSceneView` to // composite pictures into the canvases in the DOM tree it builds. abstract class PictureRenderer { - FutureOr renderPictures(List picture); + FutureOr renderPicture(ScenePicture picture); } class _SceneRender { - _SceneRender( - this.scene, - this._completer, { - this.recorder, - }) { + _SceneRender(this.scene, this._completer) { scene.beginRender(); } final EngineScene scene; final Completer _completer; - final FrameTimingRecorder? recorder; void done() { scene.endRender(); @@ -58,24 +47,24 @@ class EngineSceneView { _SceneRender? _currentRender; _SceneRender? _nextRender; - Future renderScene(EngineScene scene, FrameTimingRecorder? recorder) { + Future renderScene(EngineScene scene) { if (_currentRender != null) { // If a scene is already queued up, drop it and queue this one up instead // so that the scene view always displays the most recently requested scene. _nextRender?.done(); final Completer completer = Completer(); - _nextRender = _SceneRender(scene, completer, recorder: recorder); + _nextRender = _SceneRender(scene, completer); return completer.future; } final Completer completer = Completer(); - _currentRender = _SceneRender(scene, completer, recorder: recorder); + _currentRender = _SceneRender(scene, completer); _kickRenderLoop(); return completer.future; } Future _kickRenderLoop() async { final _SceneRender current = _currentRender!; - await _renderScene(current.scene, current.recorder); + await _renderScene(current.scene); current.done(); _currentRender = _nextRender; _nextRender = null; @@ -86,33 +75,19 @@ class EngineSceneView { } } - Future _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async { + Future _renderScene(EngineScene scene) async { final List slices = scene.rootLayer.slices; - final List picturesToRender = []; - for (final LayerSlice slice in slices) { - if (slice is PictureSlice) { - picturesToRender.add(slice.picture); - } - } - final Map renderMap; - if (picturesToRender.isNotEmpty) { - final RenderResult renderResult = await pictureRenderer.renderPictures(picturesToRender); - renderMap = { - for (int i = 0; i < picturesToRender.length; i++) - picturesToRender[i]: renderResult.imageBitmaps[i], - }; - recorder?.recordRasterStart(renderResult.rasterStartMicros); - recorder?.recordRasterFinish(renderResult.rasterEndMicros); - } else { - renderMap = {}; - recorder?.recordRasterStart(); - recorder?.recordRasterFinish(); - } - recorder?.submitTimings(); - + final Iterable> renderFutures = slices.map( + (LayerSlice slice) async => switch (slice) { + PlatformViewSlice() => null, + PictureSlice() => pictureRenderer.renderPicture(slice.picture), + } + ); + final List renderedBitmaps = await Future.wait(renderFutures); final List reusableContainers = List.from(containers); final List newContainers = []; - for (final LayerSlice slice in slices) { + for (int i = 0; i < slices.length; i++) { + final LayerSlice slice = slices[i]; switch (slice) { case PictureSlice(): PictureSliceContainer? container; @@ -131,7 +106,7 @@ class EngineSceneView { container = PictureSliceContainer(slice.picture.cullRect); } container.updateContents(); - container.renderBitmap(renderMap[slice.picture]!); + container.renderBitmap(renderedBitmaps[i]!); newContainers.add(container); case PlatformViewSlice(): diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart index 2b800ba276964..ee32ffd987350 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart @@ -62,9 +62,9 @@ class SkwasmImage extends SkwasmObjectWrapper implements ui.Image { final ui.Canvas canvas = ui.Canvas(recorder); canvas.drawImage(this, ui.Offset.zero, ui.Paint()); final DomImageBitmap bitmap = - (await (renderer as SkwasmRenderer).surface.renderPictures( - [recorder.endRecording() as SkwasmPicture], - )).imageBitmaps.single; + await (renderer as SkwasmRenderer).surface.renderPicture( + recorder.endRecording() as SkwasmPicture, + ); final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(bitmap.width.toDartInt, bitmap.height.toDartInt); final DomCanvasRenderingContextBitmapRenderer context = @@ -75,7 +75,8 @@ class SkwasmImage extends SkwasmObjectWrapper implements ui.Image { // Zero out the contents of the canvas so that resources can be reclaimed // by the browser. - context.transferFromImageBitmap(null); + offscreenCanvas.width = 0; + offscreenCanvas.height = 0; return ByteData.view(arrayBuffer.toDart); } else { return (renderer as SkwasmRenderer).surface.rasterizeImage(this, format); diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart index 22b7462eec9bc..3831188c4df63 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart @@ -35,10 +35,10 @@ external void surfaceSetCallbackHandler( isLeaf: true) external void surfaceDestroy(SurfaceHandle surface); -@Native, Int)>( - symbol: 'surface_renderPictures', +@Native( + symbol: 'surface_renderPicture', isLeaf: true) -external CallbackId surfaceRenderPictures(SurfaceHandle surface, Pointer picture, int count); +external CallbackId surfaceRenderPicture(SurfaceHandle surface, PictureHandle picture); @Native renderScene(ui.Scene scene, ui.FlutterView view) { - final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled ? FrameTimingRecorder() : null; - recorder?.recordBuildFinish(); - view as EngineFlutterView; assert(view is EngineFlutterWindow, 'Skwasm does not support multi-view mode yet'); final EngineSceneView sceneView = _getSceneViewForView(view); - return sceneView.renderScene(scene as EngineScene, recorder); + return sceneView.renderScene(scene as EngineScene); } EngineSceneView _getSceneViewForView(EngineFlutterView view) { @@ -480,6 +477,6 @@ class SkwasmPictureRenderer implements PictureRenderer { SkwasmSurface surface; @override - FutureOr renderPictures(List pictures) => - surface.renderPictures(pictures.cast()); + FutureOr renderPicture(ScenePicture picture) => + surface.renderPicture(picture as SkwasmPicture); } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart index eddcc19ff402b..34af06d0d1fd7 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart @@ -12,17 +12,6 @@ import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; -@JS() -@staticInterop -@anonymous -class RasterResult {} - -extension RasterResultExtension on RasterResult { - external JSNumber get rasterStartMilliseconds; - external JSNumber get rasterEndMilliseconds; - external JSArray get imageBitmaps; -} - @pragma('wasm:export') WasmVoid callbackHandler(WasmI32 callbackId, WasmI32 context, WasmExternRef? jsContext) { // Actually hide this call behind whether skwasm is enabled. Otherwise, the SkwasmCallbackHandler @@ -89,22 +78,11 @@ class SkwasmSurface { surfaceSetCallbackHandler(handle, SkwasmCallbackHandler.instance.callbackPointer); } - Future renderPictures(List pictures) => - withStackScope((StackScope scope) async { - final Pointer pictureHandles = - scope.allocPointerArray(pictures.length).cast(); - for (int i = 0; i < pictures.length; i++) { - pictureHandles[i] = pictures[i].handle; - } - final int callbackId = surfaceRenderPictures(handle, pictureHandles, pictures.length); - final RasterResult rasterResult = (await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as RasterResult; - final RenderResult result = ( - imageBitmaps: rasterResult.imageBitmaps.toDart.cast(), - rasterStartMicros: (rasterResult.rasterStartMilliseconds.toDartDouble * 1000).toInt(), - rasterEndMicros: (rasterResult.rasterEndMilliseconds.toDartDouble * 1000).toInt(), - ); - return result; - }); + Future renderPicture(SkwasmPicture picture) async { + final int callbackId = surfaceRenderPicture(handle, picture.handle); + final DomImageBitmap bitmap = (await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as DomImageBitmap; + return bitmap; + } Future rasterizeImage(SkwasmImage image, ui.ImageByteFormat format) async { final int callbackId = surfaceRasterizeImage( diff --git a/lib/web_ui/skwasm/library_skwasm_support.js b/lib/web_ui/skwasm/library_skwasm_support.js index 76cbac2db2a87..5e62614e009c4 100644 --- a/lib/web_ui/skwasm/library_skwasm_support.js +++ b/lib/web_ui/skwasm/library_skwasm_support.js @@ -27,18 +27,11 @@ mergeInto(LibraryManager.library, { return; } switch (skwasmMessage) { - case 'renderPictures': - _surface_renderPicturesOnWorker(data.surface, data.pictures, data.pictureCount, data.callbackId, performance.now()); + case 'renderPicture': + _surface_renderPictureOnWorker(data.surface, data.picture, data.callbackId); return; case 'onRenderComplete': - _surface_onRenderComplete( - data.surface, - data.callbackId, { - "imageBitmaps": data.imageBitmaps, - "rasterStartMilliseconds": data.rasterStart, - "rasterEndMilliseconds": data.rasterEnd, - }, - ); + _surface_onRenderComplete(data.surface, data.callbackId, data.imageBitmap); return; case 'setAssociatedObject': associatedObjectsMap.set(data.pointer, data.object); @@ -61,12 +54,11 @@ mergeInto(LibraryManager.library, { PThread.pthreads[threadId].addEventListener("message", eventListener); } }; - _skwasm_dispatchRenderPictures = function(threadId, surfaceHandle, pictures, pictureCount, callbackId) { + _skwasm_dispatchRenderPicture = function(threadId, surfaceHandle, pictureHandle, callbackId) { PThread.pthreads[threadId].postMessage({ - skwasmMessage: 'renderPictures', + skwasmMessage: 'renderPicture', surface: surfaceHandle, - pictures, - pictureCount, + picture: pictureHandle, callbackId, }); }; @@ -93,23 +85,15 @@ mergeInto(LibraryManager.library, { canvas.width = width; canvas.height = height; }; - _skwasm_captureImageBitmap = function(contextHandle, width, height, imagePromises) { - if (!imagePromises) imagePromises = Array(); + _skwasm_captureImageBitmap = async function(surfaceHandle, contextHandle, callbackId, width, height) { const canvas = handleToCanvasMap.get(contextHandle); - imagePromises.push(createImageBitmap(canvas, 0, 0, width, height)); - return imagePromises; - }; - _skwasm_resolveAndPostImages = async function(surfaceHandle, imagePromises, rasterStart, callbackId) { - const imageBitmaps = imagePromises ? await Promise.all(imagePromises) : []; - const rasterEnd = performance.now(); + const imageBitmap = await createImageBitmap(canvas, 0, 0, width, height); postMessage({ skwasmMessage: 'onRenderComplete', surface: surfaceHandle, callbackId, - imageBitmaps, - rasterStart, - rasterEnd, - }, [...imageBitmaps]); + imageBitmap, + }, [imageBitmap]); }; _skwasm_createGlTextureFromTextureSource = function(textureSource, width, height) { const glCtx = GL.currentContext.GLctx; @@ -141,16 +125,14 @@ mergeInto(LibraryManager.library, { skwasm_disposeAssociatedObjectOnThread__deps: ['$skwasm_support_setup'], skwasm_registerMessageListener: function() {}, skwasm_registerMessageListener__deps: ['$skwasm_support_setup'], - skwasm_dispatchRenderPictures: function() {}, - skwasm_dispatchRenderPictures__deps: ['$skwasm_support_setup'], + skwasm_dispatchRenderPicture: function() {}, + skwasm_dispatchRenderPicture__deps: ['$skwasm_support_setup'], skwasm_createOffscreenCanvas: function () {}, skwasm_createOffscreenCanvas__deps: ['$skwasm_support_setup'], skwasm_resizeCanvas: function () {}, skwasm_resizeCanvas__deps: ['$skwasm_support_setup'], skwasm_captureImageBitmap: function () {}, skwasm_captureImageBitmap__deps: ['$skwasm_support_setup'], - skwasm_resolveAndPostImages: function () {}, - skwasm_resolveAndPostImages__deps: ['$skwasm_support_setup'], skwasm_createGlTextureFromTextureSource: function () {}, skwasm_createGlTextureFromTextureSource__deps: ['$skwasm_support_setup'], }); diff --git a/lib/web_ui/skwasm/skwasm_support.h b/lib/web_ui/skwasm/skwasm_support.h index c9132b89dd166..ce36a192a69b6 100644 --- a/lib/web_ui/skwasm/skwasm_support.h +++ b/lib/web_ui/skwasm/skwasm_support.h @@ -23,21 +23,17 @@ extern SkwasmObject skwasm_getAssociatedObject(void* pointer); extern void skwasm_disposeAssociatedObjectOnThread(unsigned long threadId, void* pointer); extern void skwasm_registerMessageListener(pthread_t threadId); -extern void skwasm_dispatchRenderPictures(unsigned long threadId, - Skwasm::Surface* surface, - sk_sp* pictures, - int count, - uint32_t callbackId); +extern void skwasm_dispatchRenderPicture(unsigned long threadId, + Skwasm::Surface* surface, + SkPicture* picture, + uint32_t callbackId); extern uint32_t skwasm_createOffscreenCanvas(int width, int height); extern void skwasm_resizeCanvas(uint32_t contextHandle, int width, int height); -extern SkwasmObject skwasm_captureImageBitmap(uint32_t contextHandle, - int width, - int height, - SkwasmObject imagePromises); -extern void skwasm_resolveAndPostImages(Skwasm::Surface* surface, - SkwasmObject imagePromises, - double rasterStart, - uint32_t callbackId); +extern void skwasm_captureImageBitmap(Skwasm::Surface* surfaceHandle, + uint32_t contextHandle, + uint32_t bitmapId, + int width, + int height); extern unsigned int skwasm_createGlTextureFromTextureSource( SkwasmObject textureSource, int width, diff --git a/lib/web_ui/skwasm/surface.cpp b/lib/web_ui/skwasm/surface.cpp index a1c24b4bad83e..28b64bd25a328 100644 --- a/lib/web_ui/skwasm/surface.cpp +++ b/lib/web_ui/skwasm/surface.cpp @@ -39,19 +39,11 @@ void Surface::dispose() { } // Main thread only -uint32_t Surface::renderPictures(SkPicture** pictures, int count) { +uint32_t Surface::renderPicture(SkPicture* picture) { assert(emscripten_is_main_browser_thread()); uint32_t callbackId = ++_currentCallbackId; - std::unique_ptr[]> picturePointers = - std::make_unique[]>(count); - for (int i = 0; i < count; i++) { - picturePointers[i] = sk_ref_sp(pictures[i]); - } - - // Releasing picturePointers here and will recreate the unique_ptr on the - // other thread See surface_renderPicturesOnWorker - skwasm_dispatchRenderPictures(_thread, this, picturePointers.release(), count, - callbackId); + picture->ref(); + skwasm_dispatchRenderPicture(_thread, this, picture, callbackId); return callbackId; } @@ -144,31 +136,20 @@ void Surface::_recreateSurface() { } // Worker thread only -void Surface::renderPicturesOnWorker(sk_sp* pictures, - int pictureCount, - uint32_t callbackId, - double rasterStart) { - // This is populated by the `captureImageBitmap` call the first time it is - // passed in. - SkwasmObject imagePromiseArray = __builtin_wasm_ref_null_extern(); - for (int i = 0; i < pictureCount; i++) { - sk_sp picture = pictures[i]; - SkRect pictureRect = picture->cullRect(); - SkIRect roundedOutRect; - pictureRect.roundOut(&roundedOutRect); - _resizeCanvasToFit(roundedOutRect.width(), roundedOutRect.height()); - SkMatrix matrix = - SkMatrix::Translate(-roundedOutRect.fLeft, -roundedOutRect.fTop); - makeCurrent(_glContext); - auto canvas = _surface->getCanvas(); - canvas->drawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc); - canvas->drawPicture(picture, &matrix, nullptr); - _grContext->flush(_surface.get()); - imagePromiseArray = - skwasm_captureImageBitmap(_glContext, roundedOutRect.width(), - roundedOutRect.height(), imagePromiseArray); - } - skwasm_resolveAndPostImages(this, imagePromiseArray, rasterStart, callbackId); +void Surface::renderPictureOnWorker(SkPicture* picture, uint32_t callbackId) { + SkRect pictureRect = picture->cullRect(); + SkIRect roundedOutRect; + pictureRect.roundOut(&roundedOutRect); + _resizeCanvasToFit(roundedOutRect.width(), roundedOutRect.height()); + SkMatrix matrix = + SkMatrix::Translate(-roundedOutRect.fLeft, -roundedOutRect.fTop); + makeCurrent(_glContext); + auto canvas = _surface->getCanvas(); + canvas->drawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc); + canvas->drawPicture(sk_ref_sp(picture), &matrix, nullptr); + _grContext->flush(_surface.get()); + skwasm_captureImageBitmap(this, _glContext, callbackId, + roundedOutRect.width(), roundedOutRect.height()); } void Surface::_rasterizeImage(SkImage* image, @@ -244,22 +225,16 @@ SKWASM_EXPORT void surface_destroy(Surface* surface) { surface->dispose(); } -SKWASM_EXPORT uint32_t surface_renderPictures(Surface* surface, - SkPicture** pictures, - int count) { - return surface->renderPictures(pictures, count); +SKWASM_EXPORT uint32_t surface_renderPicture(Surface* surface, + SkPicture* picture) { + return surface->renderPicture(picture); } -SKWASM_EXPORT void surface_renderPicturesOnWorker(Surface* surface, - sk_sp* pictures, - int pictureCount, - uint32_t callbackId, - double rasterStart) { - // This will release the pictures when they leave scope. - std::unique_ptr> picturesPointer = - std::unique_ptr>(pictures); - surface->renderPicturesOnWorker(pictures, pictureCount, callbackId, - rasterStart); +SKWASM_EXPORT void surface_renderPictureOnWorker(Surface* surface, + SkPicture* picture, + uint32_t callbackId) { + surface->renderPictureOnWorker(picture, callbackId); + picture->unref(); } SKWASM_EXPORT uint32_t surface_rasterizeImage(Surface* surface, diff --git a/lib/web_ui/skwasm/surface.h b/lib/web_ui/skwasm/surface.h index 7e1d5fbb526e1..3577a947c782a 100644 --- a/lib/web_ui/skwasm/surface.h +++ b/lib/web_ui/skwasm/surface.h @@ -62,7 +62,7 @@ class Surface { // Main thread only void dispose(); - uint32_t renderPictures(SkPicture** picture, int count); + uint32_t renderPicture(SkPicture* picture); uint32_t rasterizeImage(SkImage* image, ImageByteFormat format); void setCallbackHandler(CallbackHandler* callbackHandler); void onRenderComplete(uint32_t callbackId, SkwasmObject imageBitmap); @@ -72,10 +72,7 @@ class Surface { SkwasmObject textureSource); // Worker thread - void renderPicturesOnWorker(sk_sp* picture, - int pictureCount, - uint32_t callbackId, - double rasterStart); + void renderPictureOnWorker(SkPicture* picture, uint32_t callbackId); private: void _runWorker(); diff --git a/lib/web_ui/test/canvaskit/frame_timings_test.dart b/lib/web_ui/test/canvaskit/frame_timings_test.dart new file mode 100644 index 0000000000000..0cacd4246616b --- /dev/null +++ b/lib/web_ui/test/canvaskit/frame_timings_test.dart @@ -0,0 +1,23 @@ +// 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 '../common/frame_timings_common.dart'; +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('frame timings', () { + setUpCanvasKitTest(withImplicitView: true); + + test('collects frame timings', () async { + await runFrameTimingsTest(); + }); + }); +} diff --git a/lib/web_ui/test/common/frame_timings_common.dart b/lib/web_ui/test/common/frame_timings_common.dart new file mode 100644 index 0000000000000..314e1a808861e --- /dev/null +++ b/lib/web_ui/test/common/frame_timings_common.dart @@ -0,0 +1,53 @@ +// 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 'dart:async'; + +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart' show EnginePlatformDispatcher; +import 'package:ui/ui.dart' as ui; + +/// Tests frame timings in a renderer-agnostic way. +/// +/// See CanvasKit-specific and HTML-specific test files `frame_timings_test.dart`. +Future runFrameTimingsTest() async { + final EnginePlatformDispatcher dispatcher = ui.PlatformDispatcher.instance as EnginePlatformDispatcher; + + List? timings; + dispatcher.onReportTimings = (List data) { + timings = data; + }; + Completer frameDone = Completer(); + dispatcher.onDrawFrame = () { + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + sceneBuilder + ..pushOffset(0, 0) + ..pop(); + dispatcher.render(sceneBuilder.build()).then((_) { + frameDone.complete(); + }); + }; + + // Frame 1. + dispatcher.scheduleFrame(); + await frameDone.future; + expect(timings, isNull, reason: "100 ms hasn't passed yet"); + await Future.delayed(const Duration(milliseconds: 150)); + + // Frame 2. + frameDone = Completer(); + dispatcher.scheduleFrame(); + await frameDone.future; + expect(timings, hasLength(2), reason: '100 ms passed. 2 frames pumped.'); + for (final ui.FrameTiming timing in timings!) { + expect(timing.vsyncOverhead, greaterThanOrEqualTo(Duration.zero)); + expect(timing.buildDuration, greaterThanOrEqualTo(Duration.zero)); + expect(timing.rasterDuration, greaterThanOrEqualTo(Duration.zero)); + expect(timing.totalSpan, greaterThanOrEqualTo(Duration.zero)); + expect(timing.layerCacheCount, equals(0)); + expect(timing.layerCacheBytes, equals(0)); + expect(timing.pictureCacheCount, equals(0)); + expect(timing.pictureCacheBytes, equals(0)); + } +} diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 93d54b09b226f..48e84b717f5eb 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -24,23 +24,17 @@ class StubPictureRenderer implements PictureRenderer { createDomCanvasElement(width: 500, height: 500); @override - Future renderPictures(List pictures) async { - renderedPictures.addAll(pictures); - final List bitmaps = await Future.wait(pictures.map((ScenePicture picture) { - final ui.Rect cullRect = picture.cullRect; - final Future bitmap = createImageBitmap(scratchCanvasElement as JSObject, ( - x: 0, - y: 0, - width: cullRect.width.toInt(), - height: cullRect.height.toInt(), - )); - return bitmap; - })); - return ( - imageBitmaps: bitmaps, - rasterStartMicros: 0, - rasterEndMicros: 0, - ); + Future renderPicture(ScenePicture picture) async { + renderedPictures.add(picture); + final ui.Rect cullRect = picture.cullRect; + final DomImageBitmap bitmap = + await createImageBitmap(scratchCanvasElement as JSObject, ( + x: 0, + y: 0, + width: cullRect.width.toInt(), + height: cullRect.height.toInt(), + )); + return bitmap; } List renderedPictures = []; @@ -71,7 +65,7 @@ void testMain() { final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PictureSlice(picture)); final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene, null); + await sceneView.renderScene(scene); final DomElement sceneElement = sceneView.sceneElement; final List children = sceneElement.children.toList(); @@ -106,7 +100,7 @@ void testMain() { final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PlatformViewSlice([platformView], null)); final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene, null); + await sceneView.renderScene(scene); final DomElement sceneElement = sceneView.sceneElement; final List children = sceneElement.children.toList(); @@ -140,7 +134,7 @@ void testMain() { final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PictureSlice(picture)); final EngineScene scene = EngineScene(rootLayer); - renderFutures.add(sceneView.renderScene(scene, null)); + renderFutures.add(sceneView.renderScene(scene)); } await Future.wait(renderFutures); diff --git a/lib/web_ui/test/engine/surface/frame_timings_test.dart b/lib/web_ui/test/engine/surface/frame_timings_test.dart new file mode 100644 index 0000000000000..14ec8f2e353da --- /dev/null +++ b/lib/web_ui/test/engine/surface/frame_timings_test.dart @@ -0,0 +1,23 @@ +// 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 '../../common/frame_timings_common.dart'; +import '../../common/test_initialization.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + setUp(() async { + await bootstrapAndRunApp(withImplicitView: true); + }); + + test('collects frame timings', () async { + await runFrameTimingsTest(); + }); +} diff --git a/lib/web_ui/test/ui/frame_timings_test.dart b/lib/web_ui/test/ui/frame_timings_test.dart deleted file mode 100644 index 62f83b71d4d09..0000000000000 --- a/lib/web_ui/test/ui/frame_timings_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -// 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 'dart:async'; - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -import '../common/test_initialization.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - setUp(() async { - await bootstrapAndRunApp(withImplicitView: true); - }); - - test('collects frame timings', () async { - final EnginePlatformDispatcher dispatcher = ui.PlatformDispatcher.instance as EnginePlatformDispatcher; - List? timings; - dispatcher.onReportTimings = (List data) { - timings = data; - }; - Completer frameDone = Completer(); - dispatcher.onDrawFrame = () { - final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder - ..pushOffset(0, 0) - ..pop(); - dispatcher.render(sceneBuilder.build()).then((_) { - frameDone.complete(); - }); - }; - - // Frame 1. - dispatcher.scheduleFrame(); - await frameDone.future; - expect(timings, isNull, reason: "100 ms hasn't passed yet"); - await Future.delayed(const Duration(milliseconds: 150)); - - // Frame 2. - frameDone = Completer(); - dispatcher.scheduleFrame(); - await frameDone.future; - expect(timings, hasLength(2), reason: '100 ms passed. 2 frames pumped.'); - for (final ui.FrameTiming timing in timings!) { - expect(timing.vsyncOverhead, greaterThanOrEqualTo(Duration.zero)); - expect(timing.buildDuration, greaterThanOrEqualTo(Duration.zero)); - expect(timing.rasterDuration, greaterThanOrEqualTo(Duration.zero)); - expect(timing.totalSpan, greaterThanOrEqualTo(Duration.zero)); - expect(timing.layerCacheCount, equals(0)); - expect(timing.layerCacheBytes, equals(0)); - expect(timing.pictureCacheCount, equals(0)); - expect(timing.pictureCacheBytes, equals(0)); - } - }); -}