Reverts "Implement frame timing callbacks in Skwasm. (#50737)" (#50895)

Reverts flutter/engine#50737

Initiated by: goderbauer

Reason for reverting: Fails in device lab, see https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8755350727803344657/+/u/run_web_benchmarks_skwasm/stdout

Original PR Author: eyebrowsoffire

Reviewed By: {mdebbar, yjbanov}

This change reverts the following previous change:
Original Description:
Fixes https://github.com/flutter/flutter/issues/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).
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 2d40298..d287ed8 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_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_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 0fbda33..9697966 100644
--- a/lib/web_ui/lib/src/engine.dart
+++ b/lib/web_ui/lib/src/engine.dart
@@ -65,7 +65,6 @@
 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 bdf6d74..58a23e8 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 @@
 typedef RenderRequest = ({
   ui.Scene scene,
   Completer<void> 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 7d673a6..f4fdaef 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 @@
         "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<void> completer = Completer<void>();
-      renderQueue.next = (scene: scene, completer: completer, recorder: recorder);
+      renderQueue.next = (scene: scene, completer: completer);
       return completer.future;
     }
     final Completer<void> completer = Completer<void>();
-    renderQueue.current = (scene: scene, completer: completer, recorder: recorder);
+    renderQueue.current = (scene: scene, completer: completer);
     unawaited(_kickRenderLoop(rasterizer));
     return completer.future;
   }
@@ -436,7 +435,7 @@
     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 @@
     }
   }
 
-  Future<void> _renderScene(ui.Scene scene, ViewRasterizer rasterizer, FrameTimingRecorder? recorder) async {
+  Future<void> _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 @@
     // 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 386666d..7505877 100644
--- a/lib/web_ui/lib/src/engine/dom.dart
+++ b/lib/web_ui/lib/src/engine/dom.dart
@@ -1487,7 +1487,7 @@
 
 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 ec2944b..0000000
--- 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<ui.FrameTiming> _frameTimings = <ui.FrameTiming>[];
-
-  /// 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 = <ui.FrameTiming>[];
-    }
-  }
-}
diff --git a/lib/web_ui/lib/src/engine/html/renderer.dart b/lib/web_ui/lib/src/engine/html/renderer.dart
index b41fac3..c9febef 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 @@
   @override
   Future<void> 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 b4deb9a..f15d043 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 701bb11..e721aa5 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 '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 @@
     // 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<void>(kProfilePrerollFrame, () {
       while (_surfaceStack.length > 1) {
         // Auto-pop layers that were pushed without a corresponding pop.
@@ -529,7 +528,7 @@
       }
       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 745f8b2..0dab016 100644
--- a/lib/web_ui/lib/src/engine/initialization.dart
+++ b/lib/web_ui/lib/src/engine/initialization.dart
@@ -158,15 +158,7 @@
     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 @@
         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 d5ef8b3..ffabd12 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 @@
   }
 }
 
+/// 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<ui.FrameTiming> _frameTimings = <ui.FrameTiming>[];
+
+/// 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 = <ui.FrameTiming>[];
+  }
+}
+
+/// 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 b137b70..23010c6 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 @@
 
 const String kCanvasContainerTag = 'flt-canvas-container';
 
-typedef RenderResult = ({
-  List<DomImageBitmap> 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<RenderResult> renderPictures(List<ScenePicture> picture);
+  FutureOr<DomImageBitmap> renderPicture(ScenePicture picture);
 }
 
 class _SceneRender {
-  _SceneRender(
-    this.scene,
-    this._completer, {
-    this.recorder,
-  }) {
+  _SceneRender(this.scene, this._completer) {
     scene.beginRender();
   }
 
   final EngineScene scene;
   final Completer<void> _completer;
-  final FrameTimingRecorder? recorder;
 
   void done() {
     scene.endRender();
@@ -58,24 +47,24 @@
   _SceneRender? _currentRender;
   _SceneRender? _nextRender;
 
-  Future<void> renderScene(EngineScene scene, FrameTimingRecorder? recorder) {
+  Future<void> 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<void> completer = Completer<void>();
-      _nextRender = _SceneRender(scene, completer, recorder: recorder);
+      _nextRender = _SceneRender(scene, completer);
       return completer.future;
     }
     final Completer<void> completer = Completer<void>();
-    _currentRender = _SceneRender(scene, completer, recorder: recorder);
+    _currentRender = _SceneRender(scene, completer);
     _kickRenderLoop();
     return completer.future;
   }
 
   Future<void> _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 @@
     }
   }
 
-  Future<void> _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async {
+  Future<void> _renderScene(EngineScene scene) async {
     final List<LayerSlice> slices = scene.rootLayer.slices;
-    final List<ScenePicture> picturesToRender = <ScenePicture>[];
-    for (final LayerSlice slice in slices) {
-      if (slice is PictureSlice) {
-        picturesToRender.add(slice.picture);
-      }
-    }
-    final Map<ScenePicture, DomImageBitmap> renderMap;
-    if (picturesToRender.isNotEmpty) {
-      final RenderResult renderResult = await pictureRenderer.renderPictures(picturesToRender);
-      renderMap = <ScenePicture, DomImageBitmap>{
-        for (int i = 0; i < picturesToRender.length; i++)
-          picturesToRender[i]: renderResult.imageBitmaps[i],
-      };
-      recorder?.recordRasterStart(renderResult.rasterStartMicros);
-      recorder?.recordRasterFinish(renderResult.rasterEndMicros);
-    } else {
-      renderMap = <ScenePicture, DomImageBitmap>{};
-      recorder?.recordRasterStart();
-      recorder?.recordRasterFinish();
-    }
-    recorder?.submitTimings();
-
+    final Iterable<Future<DomImageBitmap?>> renderFutures = slices.map(
+      (LayerSlice slice) async => switch (slice) {
+          PlatformViewSlice() => null,
+          PictureSlice() => pictureRenderer.renderPicture(slice.picture),
+        }
+    );
+    final List<DomImageBitmap?> renderedBitmaps = await Future.wait(renderFutures);
     final List<SliceContainer?> reusableContainers = List<SliceContainer?>.from(containers);
     final List<SliceContainer> newContainers = <SliceContainer>[];
-    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 @@
             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 2b800ba..ee32ffd 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 @@
       final ui.Canvas canvas = ui.Canvas(recorder);
       canvas.drawImage(this, ui.Offset.zero, ui.Paint());
       final DomImageBitmap bitmap =
-        (await (renderer as SkwasmRenderer).surface.renderPictures(
-          <SkwasmPicture>[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 @@
 
       // 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 22b7462..3831188 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 @@
   isLeaf: true)
 external void surfaceDestroy(SurfaceHandle surface);
 
-@Native<Int32 Function(SurfaceHandle, Pointer<PictureHandle>, Int)>(
-  symbol: 'surface_renderPictures',
+@Native<Int32 Function(SurfaceHandle, PictureHandle)>(
+  symbol: 'surface_renderPicture',
   isLeaf: true)
-external CallbackId surfaceRenderPictures(SurfaceHandle surface, Pointer<PictureHandle> picture, int count);
+external CallbackId surfaceRenderPicture(SurfaceHandle surface, PictureHandle picture);
 
 @Native<Int32 Function(
   SurfaceHandle,
diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart
index a0bfd47..6e5c4ab 100644
--- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart
+++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart
@@ -401,13 +401,10 @@
   // https://github.com/flutter/flutter/issues/137073.
   @override
   Future<void> 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 @@
   SkwasmSurface surface;
 
   @override
-  FutureOr<RenderResult> renderPictures(List<ScenePicture> pictures) =>
-    surface.renderPictures(pictures.cast<SkwasmPicture>());
+  FutureOr<DomImageBitmap> 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 eddcc19..34af06d 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/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<JSAny> 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 @@
     surfaceSetCallbackHandler(handle, SkwasmCallbackHandler.instance.callbackPointer);
   }
 
-  Future<RenderResult> renderPictures(List<SkwasmPicture> pictures) =>
-    withStackScope((StackScope scope) async {
-      final Pointer<PictureHandle> pictureHandles =
-        scope.allocPointerArray(pictures.length).cast<PictureHandle>();
-      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<DomImageBitmap>(),
-        rasterStartMicros: (rasterResult.rasterStartMilliseconds.toDartDouble * 1000).toInt(),
-        rasterEndMicros: (rasterResult.rasterEndMilliseconds.toDartDouble * 1000).toInt(),
-      );
-      return result;
-    });
+  Future<DomImageBitmap> renderPicture(SkwasmPicture picture) async {
+    final int callbackId = surfaceRenderPicture(handle, picture.handle);
+    final DomImageBitmap bitmap = (await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as DomImageBitmap;
+    return bitmap;
+  }
 
   Future<ByteData> 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 76cbac2..5e62614 100644
--- a/lib/web_ui/skwasm/library_skwasm_support.js
+++ b/lib/web_ui/skwasm/library_skwasm_support.js
@@ -27,18 +27,11 @@
           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 @@
         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 @@
       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 @@
   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 c9132b8..ce36a19 100644
--- a/lib/web_ui/skwasm/skwasm_support.h
+++ b/lib/web_ui/skwasm/skwasm_support.h
@@ -23,21 +23,17 @@
 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<SkPicture>* 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 a1c24b4..28b64bd 100644
--- a/lib/web_ui/skwasm/surface.cpp
+++ b/lib/web_ui/skwasm/surface.cpp
@@ -39,19 +39,11 @@
 }
 
 // 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<sk_sp<SkPicture>[]> picturePointers =
-      std::make_unique<sk_sp<SkPicture>[]>(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 @@
 }
 
 // Worker thread only
-void Surface::renderPicturesOnWorker(sk_sp<SkPicture>* 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<SkPicture> 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<SkPicture>(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 @@
   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<SkPicture>* pictures,
-                                                  int pictureCount,
-                                                  uint32_t callbackId,
-                                                  double rasterStart) {
-  // This will release the pictures when they leave scope.
-  std::unique_ptr<sk_sp<SkPicture>> picturesPointer =
-      std::unique_ptr<sk_sp<SkPicture>>(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 7e1d5fb..3577a94 100644
--- a/lib/web_ui/skwasm/surface.h
+++ b/lib/web_ui/skwasm/surface.h
@@ -62,7 +62,7 @@
 
   // 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 @@
       SkwasmObject textureSource);
 
   // Worker thread
-  void renderPicturesOnWorker(sk_sp<SkPicture>* 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 0000000..0cacd42
--- /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 0000000..314e1a8
--- /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<void> runFrameTimingsTest() async {
+  final EnginePlatformDispatcher dispatcher = ui.PlatformDispatcher.instance as EnginePlatformDispatcher;
+
+  List<ui.FrameTiming>? timings;
+  dispatcher.onReportTimings = (List<ui.FrameTiming> data) {
+    timings = data;
+  };
+  Completer<void> frameDone = Completer<void>();
+  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<void>.delayed(const Duration(milliseconds: 150));
+
+  // Frame 2.
+  frameDone = Completer<void>();
+  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 93d54b0..48e84b7 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 @@
       createDomCanvasElement(width: 500, height: 500);
 
   @override
-  Future<RenderResult> renderPictures(List<ScenePicture> pictures) async {
-    renderedPictures.addAll(pictures);
-    final List<DomImageBitmap> bitmaps = await Future.wait(pictures.map((ScenePicture picture) {
-      final ui.Rect cullRect = picture.cullRect;
-      final Future<DomImageBitmap> 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<DomImageBitmap> 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<ScenePicture> renderedPictures = <ScenePicture>[];
@@ -71,7 +65,7 @@
     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<DomElement> children = sceneElement.children.toList();
@@ -106,7 +100,7 @@
     final EngineRootLayer rootLayer = EngineRootLayer();
     rootLayer.slices.add(PlatformViewSlice(<PlatformView>[platformView], null));
     final EngineScene scene = EngineScene(rootLayer);
-    await sceneView.renderScene(scene, null);
+    await sceneView.renderScene(scene);
 
     final DomElement sceneElement = sceneView.sceneElement;
     final List<DomElement> children = sceneElement.children.toList();
@@ -140,7 +134,7 @@
       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 0000000..14ec8f2
--- /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 62f83b7..0000000
--- 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<ui.FrameTiming>? timings;
-    dispatcher.onReportTimings = (List<ui.FrameTiming> data) {
-      timings = data;
-    };
-    Completer<void> frameDone = Completer<void>();
-    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<void>.delayed(const Duration(milliseconds: 150));
-
-    // Frame 2.
-    frameDone = Completer<void>();
-    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));
-    }
-  });
-}