Streamline frame timings recording (#25892)

diff --git a/ci/firebase_testlab.sh b/ci/firebase_testlab.sh
index 31ad03f..8080350 100755
--- a/ci/firebase_testlab.sh
+++ b/ci/firebase_testlab.sh
@@ -32,4 +32,4 @@
   --timeout 2m \
   --results-bucket=gs://flutter_firebase_testlab \
   --results-dir="engine_scenario_test/$GIT_REVISION/$BUILD_ID" \
-  --device model=flame,version=29 
+  --device model=flame,version=29
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 2d4afaf..b6d65ab 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -42,6 +42,9 @@
 FILE: ../../../flutter/flow/flow_run_all_unittests.cc
 FILE: ../../../flutter/flow/flow_test_utils.cc
 FILE: ../../../flutter/flow/flow_test_utils.h
+FILE: ../../../flutter/flow/frame_timings.cc
+FILE: ../../../flutter/flow/frame_timings.h
+FILE: ../../../flutter/flow/frame_timings_recorder_unittests.cc
 FILE: ../../../flutter/flow/gl_context_switch_unittests.cc
 FILE: ../../../flutter/flow/instrumentation.cc
 FILE: ../../../flutter/flow/instrumentation.h
diff --git a/flow/BUILD.gn b/flow/BUILD.gn
index e112594..1efd32e 100644
--- a/flow/BUILD.gn
+++ b/flow/BUILD.gn
@@ -14,6 +14,8 @@
     "diff_context.h",
     "embedded_views.cc",
     "embedded_views.h",
+    "frame_timings.cc",
+    "frame_timings.h",
     "instrumentation.cc",
     "instrumentation.h",
     "layers/backdrop_filter_layer.cc",
@@ -143,6 +145,7 @@
       "flow_run_all_unittests.cc",
       "flow_test_utils.cc",
       "flow_test_utils.h",
+      "frame_timings_recorder_unittests.cc",
       "gl_context_switch_unittests.cc",
       "layers/backdrop_filter_layer_unittests.cc",
       "layers/checkerboard_layertree_unittests.cc",
diff --git a/flow/frame_timings.cc b/flow/frame_timings.cc
new file mode 100644
index 0000000..d8e1589
--- /dev/null
+++ b/flow/frame_timings.cc
@@ -0,0 +1,135 @@
+// 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.
+
+#include "flutter/flow/frame_timings.h"
+
+#include <memory>
+
+#include "flutter/common/settings.h"
+#include "flutter/fml/logging.h"
+
+namespace flutter {
+
+FrameTimingsRecorder::FrameTimingsRecorder() = default;
+
+FrameTimingsRecorder::~FrameTimingsRecorder() = default;
+
+fml::TimePoint FrameTimingsRecorder::GetVsyncStartTime() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kVsync);
+  return vsync_start_;
+}
+
+fml::TimePoint FrameTimingsRecorder::GetVsyncTargetTime() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kVsync);
+  return vsync_target_;
+}
+
+fml::TimePoint FrameTimingsRecorder::GetBuildStartTime() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kBuildStart);
+  return build_start_;
+}
+
+fml::TimePoint FrameTimingsRecorder::GetBuildEndTime() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kBuildEnd);
+  return build_end_;
+}
+
+fml::TimePoint FrameTimingsRecorder::GetRasterStartTime() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kRasterStart);
+  return raster_start_;
+}
+
+fml::TimePoint FrameTimingsRecorder::GetRasterEndTime() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kRasterEnd);
+  return raster_end_;
+}
+
+fml::TimeDelta FrameTimingsRecorder::GetBuildDuration() const {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ >= State::kBuildEnd);
+  return build_end_ - build_start_;
+}
+
+void FrameTimingsRecorder::RecordVsync(fml::TimePoint vsync_start,
+                                       fml::TimePoint vsync_target) {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ == State::kUninitialized);
+  state_ = State::kVsync;
+  vsync_start_ = vsync_start;
+  vsync_target_ = vsync_target;
+}
+
+void FrameTimingsRecorder::RecordBuildStart(fml::TimePoint build_start) {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ == State::kVsync);
+  state_ = State::kBuildStart;
+  build_start_ = build_start;
+}
+
+void FrameTimingsRecorder::RecordBuildEnd(fml::TimePoint build_end) {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ == State::kBuildStart);
+  state_ = State::kBuildEnd;
+  build_end_ = build_end;
+}
+
+void FrameTimingsRecorder::RecordRasterStart(fml::TimePoint raster_start) {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ == State::kBuildEnd);
+  state_ = State::kRasterStart;
+  raster_start_ = raster_start;
+}
+
+FrameTiming FrameTimingsRecorder::RecordRasterEnd(fml::TimePoint raster_end) {
+  std::scoped_lock state_lock(state_mutex_);
+  FML_DCHECK(state_ == State::kRasterStart);
+  state_ = State::kRasterEnd;
+  raster_end_ = raster_end;
+  FrameTiming timing;
+  timing.Set(FrameTiming::kVsyncStart, vsync_start_);
+  timing.Set(FrameTiming::kBuildStart, build_start_);
+  timing.Set(FrameTiming::kBuildFinish, build_end_);
+  timing.Set(FrameTiming::kRasterStart, raster_start_);
+  timing.Set(FrameTiming::kRasterFinish, raster_end_);
+  return timing;
+}
+
+std::unique_ptr<FrameTimingsRecorder> FrameTimingsRecorder::CloneUntil(
+    State state) {
+  std::scoped_lock state_lock(state_mutex_);
+  std::unique_ptr<FrameTimingsRecorder> recorder =
+      std::make_unique<FrameTimingsRecorder>();
+  recorder->state_ = state;
+
+  if (state >= State::kVsync) {
+    recorder->vsync_start_ = vsync_start_;
+    recorder->vsync_target_ = vsync_target_;
+  }
+
+  if (state >= State::kBuildStart) {
+    recorder->build_start_ = build_start_;
+  }
+
+  if (state >= State::kRasterEnd) {
+    recorder->build_end_ = build_end_;
+  }
+
+  if (state >= State::kRasterStart) {
+    recorder->raster_start_ = raster_start_;
+  }
+
+  if (state >= State::kRasterEnd) {
+    recorder->raster_end_ = raster_end_;
+  }
+
+  return recorder;
+}
+
+}  // namespace flutter
diff --git a/flow/frame_timings.h b/flow/frame_timings.h
new file mode 100644
index 0000000..07e9f16
--- /dev/null
+++ b/flow/frame_timings.h
@@ -0,0 +1,98 @@
+// 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.
+
+#ifndef FLUTTER_FLOW_FRAME_TIMINGS_H_
+#define FLUTTER_FLOW_FRAME_TIMINGS_H_
+
+#include <mutex>
+
+#include "flutter/common/settings.h"
+#include "flutter/fml/macros.h"
+#include "flutter/fml/time/time_delta.h"
+#include "flutter/fml/time/time_point.h"
+
+namespace flutter {
+
+/// Records timestamps for various phases of a frame rendering process.
+///
+/// Recorder is created on vsync and destroyed after the rasterization of the
+/// frame. This class is thread safe and doesn't require additional
+/// synchronization.
+class FrameTimingsRecorder {
+ public:
+  /// Various states that the recorder can be in. When created the recorder is
+  /// in an unitialized state and transtions in sequential order of the states.
+  enum class State : uint32_t {
+    kUninitialized,
+    kVsync,
+    kBuildStart,
+    kBuildEnd,
+    kRasterStart,
+    kRasterEnd,
+  };
+
+  /// Default constructor, initializes the recorder with State::kUninitialized.
+  FrameTimingsRecorder();
+
+  ~FrameTimingsRecorder();
+
+  /// Timestamp of the vsync signal.
+  fml::TimePoint GetVsyncStartTime() const;
+
+  /// Timestamp of when the frame was targeted to be presented.
+  ///
+  /// This is typically the next vsync signal timestamp.
+  fml::TimePoint GetVsyncTargetTime() const;
+
+  /// Timestamp of when the frame building started.
+  fml::TimePoint GetBuildStartTime() const;
+
+  /// Timestamp of when the frame was finished building.
+  fml::TimePoint GetBuildEndTime() const;
+
+  /// Timestamp of when the frame rasterization started.
+  fml::TimePoint GetRasterStartTime() const;
+
+  /// Timestamp of when the frame rasterization finished.
+  fml::TimePoint GetRasterEndTime() const;
+
+  /// Duration of the frame build time.
+  fml::TimeDelta GetBuildDuration() const;
+
+  /// Records a vsync event.
+  void RecordVsync(fml::TimePoint vsync_start, fml::TimePoint vsync_target);
+
+  /// Records a build start event.
+  void RecordBuildStart(fml::TimePoint build_start);
+
+  /// Records a build end event.
+  void RecordBuildEnd(fml::TimePoint build_end);
+
+  /// Records a raster start event.
+  void RecordRasterStart(fml::TimePoint raster_start);
+
+  /// Clones the recorder until (and including) the specified state.
+  std::unique_ptr<FrameTimingsRecorder> CloneUntil(State state);
+
+  /// Records a raster end event, and builds a `FrameTiming` that summarizes all
+  /// the events. This summary is sent to the framework.
+  FrameTiming RecordRasterEnd(fml::TimePoint raster_end);
+
+ private:
+  mutable std::mutex state_mutex_;
+  State state_ = State::kUninitialized;
+
+  fml::TimePoint vsync_start_;
+  fml::TimePoint vsync_target_;
+  fml::TimePoint build_start_;
+  fml::TimePoint build_end_;
+  fml::TimePoint raster_start_;
+  fml::TimePoint raster_end_;
+
+  FML_DISALLOW_COPY_ASSIGN_AND_MOVE(FrameTimingsRecorder);
+};
+
+}  // namespace flutter
+
+#endif  // FLUTTER_FLOW_FRAME_TIMINGS_H_
diff --git a/flow/frame_timings_recorder_unittests.cc b/flow/frame_timings_recorder_unittests.cc
new file mode 100644
index 0000000..975af1c
--- /dev/null
+++ b/flow/frame_timings_recorder_unittests.cc
@@ -0,0 +1,91 @@
+// 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.
+
+#include "flutter/flow/frame_timings.h"
+
+#include "flutter/fml/time/time_delta.h"
+#include "flutter/fml/time/time_point.h"
+
+#include "gtest/gtest.h"
+
+namespace flutter {
+namespace testing {
+
+TEST(FrameTimingsRecorderTest, RecordVsync) {
+  auto recorder = std::make_unique<FrameTimingsRecorder>();
+  const auto st = fml::TimePoint::Now();
+  const auto en = st + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordVsync(st, en);
+
+  ASSERT_EQ(st, recorder->GetVsyncStartTime());
+  ASSERT_EQ(en, recorder->GetVsyncTargetTime());
+}
+
+TEST(FrameTimingsRecorderTest, RecordBuildTimes) {
+  auto recorder = std::make_unique<FrameTimingsRecorder>();
+
+  const auto st = fml::TimePoint::Now();
+  const auto en = st + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordVsync(st, en);
+
+  const auto build_start = fml::TimePoint::Now();
+  const auto build_end = build_start + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordBuildStart(build_start);
+  recorder->RecordBuildEnd(build_end);
+
+  ASSERT_EQ(build_start, recorder->GetBuildStartTime());
+  ASSERT_EQ(build_end, recorder->GetBuildEndTime());
+}
+
+TEST(FrameTimingsRecorderTest, RecordRasterTimes) {
+  auto recorder = std::make_unique<FrameTimingsRecorder>();
+
+  const auto st = fml::TimePoint::Now();
+  const auto en = st + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordVsync(st, en);
+
+  const auto build_start = fml::TimePoint::Now();
+  const auto build_end = build_start + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordBuildStart(build_start);
+  recorder->RecordBuildEnd(build_end);
+
+  const auto raster_start = fml::TimePoint::Now();
+  const auto raster_end = raster_start + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordRasterStart(raster_start);
+  recorder->RecordRasterEnd(raster_end);
+
+  ASSERT_EQ(raster_start, recorder->GetRasterStartTime());
+  ASSERT_EQ(raster_end, recorder->GetRasterEndTime());
+}
+
+// Windows and Fuchsia don't allow testing with killed by signal.
+#if !defined(OS_FUCHSIA) && !defined(OS_WIN) && \
+    (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG)
+
+TEST(FrameTimingsRecorderTest, ThrowWhenRecordBuildBeforeVsync) {
+  auto recorder = std::make_unique<FrameTimingsRecorder>();
+
+  const auto build_start = fml::TimePoint::Now();
+  EXPECT_EXIT(recorder->RecordBuildStart(build_start),
+              ::testing::KilledBySignal(SIGABRT),
+              "Check failed: state_ == State::kVsync.");
+}
+
+TEST(FrameTimingsRecorderTest, ThrowWhenRecordRasterBeforeBuildEnd) {
+  auto recorder = std::make_unique<FrameTimingsRecorder>();
+
+  const auto st = fml::TimePoint::Now();
+  const auto en = st + fml::TimeDelta::FromMillisecondsF(16);
+  recorder->RecordVsync(st, en);
+
+  const auto raster_start = fml::TimePoint::Now();
+  EXPECT_EXIT(recorder->RecordRasterStart(raster_start),
+              ::testing::KilledBySignal(SIGABRT),
+              "Check failed: state_ == State::kBuildEnd.");
+}
+
+#endif
+
+}  // namespace testing
+}  // namespace flutter
diff --git a/flow/layers/layer_tree.cc b/flow/layers/layer_tree.cc
index eb58bc1..2fa755f 100644
--- a/flow/layers/layer_tree.cc
+++ b/flow/layers/layer_tree.cc
@@ -4,7 +4,9 @@
 
 #include "flutter/flow/layers/layer_tree.h"
 
+#include "flutter/flow/frame_timings.h"
 #include "flutter/flow/layers/layer.h"
+#include "flutter/fml/time/time_point.h"
 #include "flutter/fml/trace_event.h"
 #include "third_party/skia/include/core/SkPictureRecorder.h"
 #include "third_party/skia/include/utils/SkNWayCanvas.h"
@@ -20,15 +22,6 @@
   FML_CHECK(device_pixel_ratio_ != 0.0f);
 }
 
-void LayerTree::RecordBuildTime(fml::TimePoint vsync_start,
-                                fml::TimePoint build_start,
-                                fml::TimePoint target_time) {
-  vsync_start_ = vsync_start;
-  build_start_ = build_start;
-  target_time_ = target_time;
-  build_finish_ = fml::TimePoint::Now();
-}
-
 bool LayerTree::Preroll(CompositorContext::ScopedFrame& frame,
                         bool ignore_raster_cache) {
   TRACE_EVENT0("flutter", "LayerTree::Preroll");
diff --git a/flow/layers/layer_tree.h b/flow/layers/layer_tree.h
index db8a33b..67a32ca 100644
--- a/flow/layers/layer_tree.h
+++ b/flow/layers/layer_tree.h
@@ -56,16 +56,6 @@
 
 #endif  // FLUTTER_ENABLE_DIFF_CONTEXT
 
-  void RecordBuildTime(fml::TimePoint vsync_start,
-                       fml::TimePoint build_start,
-                       fml::TimePoint target_time);
-  fml::TimePoint vsync_start() const { return vsync_start_; }
-  fml::TimeDelta vsync_overhead() const { return build_start_ - vsync_start_; }
-  fml::TimePoint build_start() const { return build_start_; }
-  fml::TimePoint build_finish() const { return build_finish_; }
-  fml::TimeDelta build_time() const { return build_finish_ - build_start_; }
-  fml::TimePoint target_time() const { return target_time_; }
-
   // The number of frame intervals missed after which the compositor must
   // trace the rasterized picture to a trace file. Specify 0 to disable all
   // tracing
@@ -87,10 +77,6 @@
 
  private:
   std::shared_ptr<Layer> root_layer_;
-  fml::TimePoint vsync_start_;
-  fml::TimePoint build_start_;
-  fml::TimePoint build_finish_;
-  fml::TimePoint target_time_;
   SkISize frame_size_ = SkISize::MakeEmpty();  // Physical pixels.
   const float device_pixel_ratio_;  // Logical / Physical pixels ratio.
   uint32_t rasterizer_tracing_threshold_;
diff --git a/lib/ui/painting/image_decoder_unittests.cc b/lib/ui/painting/image_decoder_unittests.cc
index 6e74df5..7b787b0 100644
--- a/lib/ui/painting/image_decoder_unittests.cc
+++ b/lib/ui/painting/image_decoder_unittests.cc
@@ -427,7 +427,8 @@
   latch.Wait();
 }
 
-// TODO(https://github.com/flutter/flutter/issues/81232) - disabled due to flakiness
+// TODO(https://github.com/flutter/flutter/issues/81232) - disabled due to
+// flakiness
 TEST_F(ImageDecoderFixtureTest, DISABLED_CanResizeWithoutDecode) {
   SkImageInfo info = {};
   size_t row_bytes;
diff --git a/shell/common/animator.cc b/shell/common/animator.cc
index c2fc83e..160ffbc 100644
--- a/shell/common/animator.cc
+++ b/shell/common/animator.cc
@@ -4,6 +4,8 @@
 
 #include "flutter/shell/common/animator.h"
 
+#include "flutter/flow/frame_timings.h"
+#include "flutter/fml/time/time_point.h"
 #include "flutter/fml/trace_event.h"
 #include "third_party/dart/runtime/include/dart_tools_api.h"
 
@@ -25,9 +27,6 @@
     : delegate_(delegate),
       task_runners_(std::move(task_runners)),
       waiter_(std::move(waiter)),
-      last_frame_begin_time_(),
-      last_vsync_start_time_(),
-      last_frame_target_time_(),
       dart_frame_deadline_(0),
 #if SHELL_ENABLE_METAL
       layer_tree_pipeline_(fml::MakeRefCounted<LayerTreePipeline>(2)),
@@ -96,10 +95,13 @@
   return (time - fxl_now).ToMicroseconds() + dart_now;
 }
 
-void Animator::BeginFrame(fml::TimePoint vsync_start_time,
-                          fml::TimePoint frame_target_time) {
+void Animator::BeginFrame(
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
   TRACE_EVENT_ASYNC_END0("flutter", "Frame Request Pending", frame_number_++);
 
+  frame_timings_recorder_ = std::move(frame_timings_recorder);
+  frame_timings_recorder_->RecordBuildStart(fml::TimePoint::Now());
+
   TRACE_EVENT0("flutter", "Animator::BeginFrame");
   while (!trace_flow_ids_.empty()) {
     uint64_t trace_flow_id = trace_flow_ids_.front();
@@ -130,13 +132,12 @@
   // We have acquired a valid continuation from the pipeline and are ready
   // to service potential frame.
   FML_DCHECK(producer_continuation_);
-
-  last_frame_begin_time_ = fml::TimePoint::Now();
-  last_vsync_start_time_ = vsync_start_time;
-  fml::tracing::TraceEventAsyncComplete("flutter", "VsyncSchedulingOverhead",
-                                        last_vsync_start_time_,
-                                        last_frame_begin_time_);
-  last_frame_target_time_ = frame_target_time;
+  fml::tracing::TraceEventAsyncComplete(
+      "flutter", "VsyncSchedulingOverhead",
+      frame_timings_recorder_->GetVsyncStartTime(),
+      frame_timings_recorder_->GetBuildStartTime());
+  const fml::TimePoint frame_target_time =
+      frame_timings_recorder_->GetVsyncTargetTime();
   dart_frame_deadline_ = FxlToDartOrEarlier(frame_target_time);
   {
     TRACE_EVENT2("flutter", "Framework Workload", "mode", "basic", "frame",
@@ -180,9 +181,15 @@
   }
   last_layer_tree_size_ = layer_tree->frame_size();
 
-  // Note the frame time for instrumentation.
-  layer_tree->RecordBuildTime(last_vsync_start_time_, last_frame_begin_time_,
-                              last_frame_target_time_);
+  if (!frame_timings_recorder_) {
+    // Framework can directly call render with a built scene.
+    frame_timings_recorder_ = std::make_unique<FrameTimingsRecorder>();
+    const fml::TimePoint placeholder_time = fml::TimePoint::Now();
+    frame_timings_recorder_->RecordVsync(placeholder_time, placeholder_time);
+    frame_timings_recorder_->RecordBuildStart(placeholder_time);
+  }
+
+  frame_timings_recorder_->RecordBuildEnd(fml::TimePoint::Now());
 
   // Commit the pending continuation.
   bool result = producer_continuation_.Complete(std::move(layer_tree));
@@ -190,16 +197,25 @@
     FML_DLOG(INFO) << "No pending continuation to commit";
   }
 
-  delegate_.OnAnimatorDraw(layer_tree_pipeline_, last_frame_target_time_);
+  delegate_.OnAnimatorDraw(layer_tree_pipeline_,
+                           std::move(frame_timings_recorder_));
 }
 
 bool Animator::CanReuseLastLayerTree() {
   return !regenerate_layer_tree_;
 }
 
-void Animator::DrawLastLayerTree() {
+void Animator::DrawLastLayerTree(
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
   pending_frame_semaphore_.Signal();
-  delegate_.OnAnimatorDrawLastLayerTree();
+  // In this case BeginFrame doesn't get called, we need to
+  // adjust frame timings to update build start and end times,
+  // given that the frame doesn't get built in this case, we
+  // will use Now() for both start and end times as an indication.
+  const auto now = fml::TimePoint::Now();
+  frame_timings_recorder->RecordBuildStart(now);
+  frame_timings_recorder->RecordBuildEnd(now);
+  delegate_.OnAnimatorDrawLastLayerTree(std::move(frame_timings_recorder));
 }
 
 void Animator::RequestFrame(bool regenerate_layer_tree) {
@@ -236,13 +252,13 @@
 
 void Animator::AwaitVSync() {
   waiter_->AsyncWaitForVsync(
-      [self = weak_factory_.GetWeakPtr()](fml::TimePoint vsync_start_time,
-                                          fml::TimePoint frame_target_time) {
+      [self = weak_factory_.GetWeakPtr()](
+          std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
         if (self) {
           if (self->CanReuseLastLayerTree()) {
-            self->DrawLastLayerTree();
+            self->DrawLastLayerTree(std::move(frame_timings_recorder));
           } else {
-            self->BeginFrame(vsync_start_time, frame_target_time);
+            self->BeginFrame(std::move(frame_timings_recorder));
           }
         }
       });
diff --git a/shell/common/animator.h b/shell/common/animator.h
index 6927e85..c8efaff 100644
--- a/shell/common/animator.h
+++ b/shell/common/animator.h
@@ -8,6 +8,7 @@
 #include <deque>
 
 #include "flutter/common/task_runners.h"
+#include "flutter/flow/frame_timings.h"
 #include "flutter/fml/memory/ref_ptr.h"
 #include "flutter/fml/memory/weak_ptr.h"
 #include "flutter/fml/synchronization/semaphore.h"
@@ -36,9 +37,10 @@
 
     virtual void OnAnimatorDraw(
         fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
-        fml::TimePoint frame_target_time) = 0;
+        std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) = 0;
 
-    virtual void OnAnimatorDrawLastLayerTree() = 0;
+    virtual void OnAnimatorDrawLastLayerTree(
+        std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) = 0;
   };
 
   Animator(Delegate& delegate,
@@ -83,11 +85,12 @@
  private:
   using LayerTreePipeline = Pipeline<flutter::LayerTree>;
 
-  void BeginFrame(fml::TimePoint frame_start_time,
-                  fml::TimePoint frame_target_time);
+  void BeginFrame(std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder);
 
   bool CanReuseLastLayerTree();
-  void DrawLastLayerTree();
+
+  void DrawLastLayerTree(
+      std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder);
 
   void AwaitVSync();
 
@@ -100,9 +103,7 @@
   TaskRunners task_runners_;
   std::shared_ptr<VsyncWaiter> waiter_;
 
-  fml::TimePoint last_frame_begin_time_;
-  fml::TimePoint last_vsync_start_time_;
-  fml::TimePoint last_frame_target_time_;
+  std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder_;
   int64_t dart_frame_deadline_;
   fml::RefPtr<LayerTreePipeline> layer_tree_pipeline_;
   fml::Semaphore pending_frame_semaphore_;
diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc
index acd3397..7281f5f 100644
--- a/shell/common/rasterizer.cc
+++ b/shell/common/rasterizer.cc
@@ -5,12 +5,15 @@
 #include "flutter/shell/common/rasterizer.h"
 
 #include <algorithm>
+#include <memory>
 #include <utility>
 
+#include "flow/frame_timings.h"
 #include "flutter/common/graphics/persistent_cache.h"
 #include "flutter/fml/time/time_delta.h"
 #include "flutter/fml/time/time_point.h"
 #include "flutter/shell/common/serialization_callbacks.h"
+#include "fml/make_copyable.h"
 #include "third_party/skia/include/core/SkEncodedImageFormat.h"
 #include "third_party/skia/include/core/SkImageEncoder.h"
 #include "third_party/skia/include/core/SkPictureRecorder.h"
@@ -145,15 +148,18 @@
   return last_layer_tree_.get();
 }
 
-void Rasterizer::DrawLastLayerTree() {
+void Rasterizer::DrawLastLayerTree(
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
   if (!last_layer_tree_ || !surface_) {
     return;
   }
-  DrawToSurface(*last_layer_tree_);
+  DrawToSurface(frame_timings_recorder->GetBuildDuration(), *last_layer_tree_);
 }
 
-void Rasterizer::Draw(fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
-                      LayerTreeDiscardCallback discardCallback) {
+void Rasterizer::Draw(
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder,
+    fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
+    LayerTreeDiscardCallback discardCallback) {
   TRACE_EVENT0("flutter", "GPURasterizer::Draw");
   if (raster_thread_merger_ &&
       !raster_thread_merger_->IsOnRasterizingThread()) {
@@ -164,13 +170,18 @@
                  .GetRasterTaskRunner()
                  ->RunsTasksOnCurrentThread());
 
+  std::unique_ptr<FrameTimingsRecorder> resubmit_recorder =
+      frame_timings_recorder->CloneUntil(
+          FrameTimingsRecorder::State::kBuildEnd);
+
   RasterStatus raster_status = RasterStatus::kFailed;
   Pipeline<flutter::LayerTree>::Consumer consumer =
       [&](std::unique_ptr<LayerTree> layer_tree) {
         if (discardCallback(*layer_tree.get())) {
           raster_status = RasterStatus::kDiscarded;
         } else {
-          raster_status = DoDraw(std::move(layer_tree));
+          raster_status =
+              DoDraw(std::move(frame_timings_recorder), std::move(layer_tree));
         }
       };
 
@@ -202,11 +213,13 @@
   switch (consume_result) {
     case PipelineConsumeResult::MoreAvailable: {
       delegate_.GetTaskRunners().GetRasterTaskRunner()->PostTask(
-          [weak_this = weak_factory_.GetWeakPtr(), pipeline]() {
-            if (weak_this) {
-              weak_this->Draw(pipeline);
-            }
-          });
+          fml::MakeCopyable(
+              [weak_this = weak_factory_.GetWeakPtr(), pipeline,
+               resubmit_recorder = std::move(resubmit_recorder)]() mutable {
+                if (weak_this) {
+                  weak_this->Draw(std::move(resubmit_recorder), pipeline);
+                }
+              }));
       break;
     }
     default:
@@ -332,6 +345,7 @@
 }
 
 RasterStatus Rasterizer::DoDraw(
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder,
     std::unique_ptr<flutter::LayerTree> layer_tree) {
   FML_DCHECK(delegate_.GetTaskRunners()
                  .GetRasterTaskRunner()
@@ -341,19 +355,13 @@
     return RasterStatus::kFailed;
   }
 
-  FrameTiming timing;
-#if !defined(OS_FUCHSIA)
-  const fml::TimePoint frame_target_time = layer_tree->target_time();
-#endif
-  timing.Set(FrameTiming::kVsyncStart, layer_tree->vsync_start());
-  timing.Set(FrameTiming::kBuildStart, layer_tree->build_start());
-  timing.Set(FrameTiming::kBuildFinish, layer_tree->build_finish());
-  timing.Set(FrameTiming::kRasterStart, fml::TimePoint::Now());
+  frame_timings_recorder->RecordRasterStart(fml::TimePoint::Now());
 
   PersistentCache* persistent_cache = PersistentCache::GetCacheForProcess();
   persistent_cache->ResetStoredNewShaders();
 
-  RasterStatus raster_status = DrawToSurface(*layer_tree);
+  RasterStatus raster_status =
+      DrawToSurface(frame_timings_recorder->GetBuildDuration(), *layer_tree);
   if (raster_status == RasterStatus::kSuccess) {
     last_layer_tree_ = std::move(layer_tree);
   } else if (raster_status == RasterStatus::kResubmit ||
@@ -373,12 +381,14 @@
   // Rasterizer::DoDraw finishes. Future work is needed to adapt the timestamp
   // for Fuchsia to capture SceneUpdateContext::ExecutePaintTasks.
   const auto raster_finish_time = fml::TimePoint::Now();
-  timing.Set(FrameTiming::kRasterFinish, raster_finish_time);
-  delegate_.OnFrameRasterized(timing);
+  delegate_.OnFrameRasterized(
+      frame_timings_recorder->RecordRasterEnd(raster_finish_time));
 
 // SceneDisplayLag events are disabled on Fuchsia.
 // see: https://github.com/flutter/flutter/issues/56598
 #if !defined(OS_FUCHSIA)
+  fml::TimePoint frame_target_time =
+      frame_timings_recorder->GetVsyncTargetTime();
   if (raster_finish_time > frame_target_time) {
     fml::TimePoint latest_frame_target_time =
         delegate_.GetLatestFrameTargetTime();
@@ -432,14 +442,13 @@
   return raster_status;
 }
 
-RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {
+RasterStatus Rasterizer::DrawToSurface(
+    const fml::TimeDelta frame_build_duration,
+    flutter::LayerTree& layer_tree) {
   TRACE_EVENT0("flutter", "Rasterizer::DrawToSurface");
   FML_DCHECK(surface_);
 
-  // There is no way for the compositor to know how long the layer tree
-  // construction took. Fortunately, the layer tree does. Grab that time
-  // for instrumentation.
-  compositor_context_->ui_time().SetLapTime(layer_tree.build_time());
+  compositor_context_->ui_time().SetLapTime(frame_build_duration);
 
   SkCanvas* embedder_root_canvas = nullptr;
   if (external_view_embedder_) {
diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h
index 9258224..1f1559c 100644
--- a/shell/common/rasterizer.h
+++ b/shell/common/rasterizer.h
@@ -8,10 +8,11 @@
 #include <memory>
 #include <optional>
 
-#include "flow/embedded_views.h"
 #include "flutter/common/settings.h"
 #include "flutter/common/task_runners.h"
 #include "flutter/flow/compositor_context.h"
+#include "flutter/flow/embedded_views.h"
+#include "flutter/flow/frame_timings.h"
 #include "flutter/flow/layers/layer_tree.h"
 #include "flutter/flow/surface.h"
 #include "flutter/fml/closure.h"
@@ -195,7 +196,8 @@
   ///             textures instead of waiting for the framework to do the work
   ///             to generate the layer tree describing the same contents.
   ///
-  void DrawLastLayerTree();
+  void DrawLastLayerTree(
+      std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder);
 
   //----------------------------------------------------------------------------
   /// @brief      Gets the registry of external textures currently in use by the
@@ -241,7 +243,8 @@
   /// @param[in]  discardCallback if specified and returns true, the layer tree
   ///                             is discarded instead of being rendered
   ///
-  void Draw(fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
+  void Draw(std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder,
+            fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
             LayerTreeDiscardCallback discardCallback = NoDiscard);
 
   //----------------------------------------------------------------------------
@@ -478,9 +481,12 @@
       SkISize size,
       std::function<void(SkCanvas*)> draw_callback);
 
-  RasterStatus DoDraw(std::unique_ptr<flutter::LayerTree> layer_tree);
+  RasterStatus DoDraw(
+      std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder,
+      std::unique_ptr<flutter::LayerTree> layer_tree);
 
-  RasterStatus DrawToSurface(flutter::LayerTree& layer_tree);
+  RasterStatus DrawToSurface(const fml::TimeDelta frame_build_duration,
+                             flutter::LayerTree& layer_tree);
 
   void FireNextFrameCallbackIfPresent();
 
diff --git a/shell/common/rasterizer_unittests.cc b/shell/common/rasterizer_unittests.cc
index 96321df..9effc8a 100644
--- a/shell/common/rasterizer_unittests.cc
+++ b/shell/common/rasterizer_unittests.cc
@@ -6,8 +6,13 @@
 
 #include "flutter/shell/common/rasterizer.h"
 
+#include <memory>
+
+#include "flutter/flow/frame_timings.h"
+#include "flutter/fml/time/time_point.h"
 #include "flutter/shell/common/thread_host.h"
 #include "flutter/testing/testing.h"
+
 #include "gmock/gmock.h"
 
 using testing::_;
@@ -73,6 +78,16 @@
   EXPECT_TRUE(rasterizer != nullptr);
 }
 
+static std::unique_ptr<FrameTimingsRecorder> CreateFinishedBuildRecorder() {
+  std::unique_ptr<FrameTimingsRecorder> recorder =
+      std::make_unique<FrameTimingsRecorder>();
+  const auto now = fml::TimePoint::Now();
+  recorder->RecordVsync(now, now);
+  recorder->RecordBuildStart(now);
+  recorder->RecordBuildEnd(now);
+  return recorder;
+}
+
 TEST(RasterizerTest, drawEmptyPipeline) {
   std::string test_name =
       ::testing::UnitTest::GetInstance()->current_test_info()->name();
@@ -91,7 +106,7 @@
   fml::AutoResetWaitableEvent latch;
   thread_host.raster_thread->GetTaskRunner()->PostTask([&] {
     auto pipeline = fml::AdoptRef(new Pipeline<LayerTree>(/*depth=*/10));
-    rasterizer->Draw(pipeline, nullptr);
+    rasterizer->Draw(CreateFinishedBuildRecorder(), pipeline, nullptr);
     latch.Signal();
   });
   latch.Wait();
@@ -148,7 +163,7 @@
     bool result = pipeline->Produce().Complete(std::move(layer_tree));
     EXPECT_TRUE(result);
     auto no_discard = [](LayerTree&) { return false; };
-    rasterizer->Draw(pipeline, no_discard);
+    rasterizer->Draw(CreateFinishedBuildRecorder(), pipeline, no_discard);
     latch.Signal();
   });
   latch.Wait();
@@ -202,7 +217,7 @@
     bool result = pipeline->Produce().Complete(std::move(layer_tree));
     EXPECT_TRUE(result);
     auto no_discard = [](LayerTree&) { return false; };
-    rasterizer->Draw(pipeline, no_discard);
+    rasterizer->Draw(CreateFinishedBuildRecorder(), pipeline, no_discard);
     latch.Signal();
   });
   latch.Wait();
@@ -261,7 +276,7 @@
   bool result = pipeline->Produce().Complete(std::move(layer_tree));
   EXPECT_TRUE(result);
   auto no_discard = [](LayerTree&) { return false; };
-  rasterizer->Draw(pipeline, no_discard);
+  rasterizer->Draw(CreateFinishedBuildRecorder(), pipeline, no_discard);
 }
 
 TEST(RasterizerTest, externalViewEmbedderDoesntEndFrameWhenNoSurfaceIsSet) {
@@ -294,7 +309,7 @@
   thread_host.raster_thread->GetTaskRunner()->PostTask([&] {
     auto pipeline = fml::AdoptRef(new Pipeline<LayerTree>(/*depth=*/10));
     auto no_discard = [](LayerTree&) { return false; };
-    rasterizer->Draw(pipeline, no_discard);
+    rasterizer->Draw(CreateFinishedBuildRecorder(), pipeline, no_discard);
     latch.Signal();
   });
   latch.Wait();
diff --git a/shell/common/shell.cc b/shell/common/shell.cc
index 7387ec7..35180e2 100644
--- a/shell/common/shell.cc
+++ b/shell/common/shell.cc
@@ -1138,13 +1138,16 @@
 }
 
 // |Animator::Delegate|
-void Shell::OnAnimatorDraw(fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
-                           fml::TimePoint frame_target_time) {
+void Shell::OnAnimatorDraw(
+    fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
   FML_DCHECK(is_setup_);
 
   // record the target time for use by rasterizer.
   {
     std::scoped_lock time_recorder_lock(time_recorder_mutex_);
+    const fml::TimePoint frame_target_time =
+        frame_timings_recorder->GetVsyncTargetTime();
     if (!latest_frame_target_time_) {
       latest_frame_target_time_ = frame_target_time;
     } else if (latest_frame_target_time_ < frame_target_time) {
@@ -1158,32 +1161,38 @@
            tree.frame_size() != expected_frame_size_;
   };
 
-  task_runners_.GetRasterTaskRunner()->PostTask(
+  task_runners_.GetRasterTaskRunner()->PostTask(fml::MakeCopyable(
       [&waiting_for_first_frame = waiting_for_first_frame_,
        &waiting_for_first_frame_condition = waiting_for_first_frame_condition_,
        rasterizer = rasterizer_->GetWeakPtr(), pipeline = std::move(pipeline),
-       discard_callback = std::move(discard_callback)]() {
+       discard_callback = std::move(discard_callback),
+       frame_timings_recorder = std::move(frame_timings_recorder)]() mutable {
         if (rasterizer) {
-          rasterizer->Draw(pipeline, std::move(discard_callback));
+          rasterizer->Draw(std::move(frame_timings_recorder), pipeline,
+                           std::move(discard_callback));
 
           if (waiting_for_first_frame.load()) {
             waiting_for_first_frame.store(false);
             waiting_for_first_frame_condition.notify_all();
           }
         }
-      });
+      }));
 }
 
 // |Animator::Delegate|
-void Shell::OnAnimatorDrawLastLayerTree() {
+void Shell::OnAnimatorDrawLastLayerTree(
+    std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) {
   FML_DCHECK(is_setup_);
 
-  task_runners_.GetRasterTaskRunner()->PostTask(
-      [rasterizer = rasterizer_->GetWeakPtr()]() {
+  auto task = fml::MakeCopyable(
+      [rasterizer = rasterizer_->GetWeakPtr(),
+       frame_timings_recorder = std::move(frame_timings_recorder)]() mutable {
         if (rasterizer) {
-          rasterizer->DrawLastLayerTree();
+          rasterizer->DrawLastLayerTree(std::move(frame_timings_recorder));
         }
       });
+
+  task_runners_.GetRasterTaskRunner()->PostTask(task);
 }
 
 // |Engine::Delegate|
diff --git a/shell/common/shell.h b/shell/common/shell.h
index 4bd957e..c3f2366 100644
--- a/shell/common/shell.h
+++ b/shell/common/shell.h
@@ -539,11 +539,13 @@
   void OnAnimatorNotifyIdle(int64_t deadline) override;
 
   // |Animator::Delegate|
-  void OnAnimatorDraw(fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
-                      fml::TimePoint frame_target_time) override;
+  void OnAnimatorDraw(
+      fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline,
+      std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) override;
 
   // |Animator::Delegate|
-  void OnAnimatorDrawLastLayerTree() override;
+  void OnAnimatorDrawLastLayerTree(
+      std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder) override;
 
   // |Engine::Delegate|
   void OnEngineUpdateSemantics(
diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc
index 88c140f..bfdfcbb 100644
--- a/shell/common/shell_test.cc
+++ b/shell/common/shell_test.cc
@@ -6,6 +6,7 @@
 
 #include "flutter/shell/common/shell_test.h"
 
+#include "flutter/flow/frame_timings.h"
 #include "flutter/flow/layers/layer_tree.h"
 #include "flutter/flow/layers/transform_layer.h"
 #include "flutter/fml/build_config.h"
@@ -137,7 +138,10 @@
           const auto frame_begin_time = fml::TimePoint::Now();
           const auto frame_end_time =
               frame_begin_time + fml::TimeDelta::FromSecondsF(1.0 / 60.0);
-          engine->animator_->BeginFrame(frame_begin_time, frame_end_time);
+          std::unique_ptr<FrameTimingsRecorder> recorder =
+              std::make_unique<FrameTimingsRecorder>();
+          recorder->RecordVsync(frame_begin_time, frame_end_time);
+          engine->animator_->BeginFrame(std::move(recorder));
         }
         latch.Signal();
       });
@@ -176,7 +180,10 @@
         const auto frame_begin_time = fml::TimePoint::Now();
         const auto frame_end_time =
             frame_begin_time + fml::TimeDelta::FromSecondsF(1.0 / 60.0);
-        engine->animator_->BeginFrame(frame_begin_time, frame_end_time);
+        std::unique_ptr<FrameTimingsRecorder> recorder =
+            std::make_unique<FrameTimingsRecorder>();
+        recorder->RecordVsync(frame_begin_time, frame_end_time);
+        engine->animator_->BeginFrame(std::move(recorder));
         latch.Signal();
       });
   latch.Wait();
diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc
index 123a29a..eb749fa 100644
--- a/shell/common/shell_unittests.cc
+++ b/shell/common/shell_unittests.cc
@@ -1143,7 +1143,7 @@
 #if defined(OS_FUCHSIA)
        DISABLED_SkipAndSubmitFrame
 #else
-       SkipAndSubmitFrame
+       DISABLED_SkipAndSubmitFrame
 #endif
 ) {
   auto settings = CreateSettingsForFixture();
diff --git a/shell/common/vsync_waiter.cc b/shell/common/vsync_waiter.cc
index 40ced72..ede589f 100644
--- a/shell/common/vsync_waiter.cc
+++ b/shell/common/vsync_waiter.cc
@@ -4,6 +4,7 @@
 
 #include "flutter/shell/common/vsync_waiter.h"
 
+#include "flow/frame_timings.h"
 #include "flutter/fml/task_runner.h"
 #include "flutter/fml/trace_event.h"
 #include "fml/message_loop_task_queues.h"
@@ -132,7 +133,11 @@
          pause_secondary_tasks]() {
           FML_TRACE_EVENT("flutter", kVsyncTraceName, "StartTime",
                           frame_start_time, "TargetTime", frame_target_time);
-          callback(frame_start_time, frame_target_time);
+          std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder =
+              std::make_unique<FrameTimingsRecorder>();
+          frame_timings_recorder->RecordVsync(frame_start_time,
+                                              frame_target_time);
+          callback(std::move(frame_timings_recorder));
           TRACE_FLOW_END("flutter", kVsyncFlowName, flow_identifier);
           if (pause_secondary_tasks) {
             ResumeDartMicroTasks();
diff --git a/shell/common/vsync_waiter.h b/shell/common/vsync_waiter.h
index d593c70..6af0b8d 100644
--- a/shell/common/vsync_waiter.h
+++ b/shell/common/vsync_waiter.h
@@ -11,6 +11,7 @@
 #include <unordered_map>
 
 #include "flutter/common/task_runners.h"
+#include "flutter/flow/frame_timings.h"
 #include "flutter/fml/time/time_point.h"
 
 namespace flutter {
@@ -19,8 +20,7 @@
 /// getting callbacks when a vsync event happens.
 class VsyncWaiter : public std::enable_shared_from_this<VsyncWaiter> {
  public:
-  using Callback = std::function<void(fml::TimePoint frame_start_time,
-                                      fml::TimePoint frame_target_time)>;
+  using Callback = std::function<void(std::unique_ptr<FrameTimingsRecorder>)>;
 
   virtual ~VsyncWaiter();
 
@@ -40,7 +40,7 @@
 
   const TaskRunners task_runners_;
 
-  VsyncWaiter(TaskRunners task_runners);
+  explicit VsyncWaiter(TaskRunners task_runners);
 
   // Implementations are meant to override this method and arm their vsync
   // latches when in response to this invocation. On vsync, they are meant to
diff --git a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm
index 37d9b23..b00b1cf 100644
--- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm
+++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm
@@ -18,13 +18,16 @@
 namespace flutter {
 
 VsyncWaiterIOS::VsyncWaiterIOS(flutter::TaskRunners task_runners)
-    : VsyncWaiter(std::move(task_runners)),
-      client_([[VSyncClient alloc] initWithTaskRunner:task_runners_.GetUITaskRunner()
-                                             callback:std::bind(&VsyncWaiterIOS::FireCallback,
-                                                                this,
-                                                                std::placeholders::_1,
-                                                                std::placeholders::_2,
-                                                                true)]) {}
+    : VsyncWaiter(std::move(task_runners)) {
+  auto callback = [this](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
+    const fml::TimePoint start_time = recorder->GetVsyncStartTime();
+    const fml::TimePoint target_time = recorder->GetVsyncTargetTime();
+    FireCallback(start_time, target_time, true);
+  };
+  client_ =
+      fml::scoped_nsobject{[[VSyncClient alloc] initWithTaskRunner:task_runners_.GetUITaskRunner()
+                                                          callback:callback]};
+}
 
 VsyncWaiterIOS::~VsyncWaiterIOS() {
   // This way, we will get no more callbacks from the display link that holds a weak (non-nilling)
@@ -75,9 +78,12 @@
   fml::TimePoint frame_start_time = fml::TimePoint::Now() - fml::TimeDelta::FromSecondsF(delay);
   fml::TimePoint frame_target_time = frame_start_time + fml::TimeDelta::FromSecondsF(link.duration);
 
+  std::unique_ptr<flutter::FrameTimingsRecorder> recorder =
+      std::make_unique<flutter::FrameTimingsRecorder>();
+  recorder->RecordVsync(frame_start_time, frame_target_time);
   display_link_.get().paused = YES;
 
-  callback_(frame_start_time, frame_target_time);
+  callback_(std::move(recorder));
 }
 
 - (void)invalidate {
diff --git a/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc b/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc
index 851922c..85a820b 100644
--- a/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc
+++ b/shell/platform/fuchsia/flutter/vsync_waiter_unittests.cc
@@ -9,6 +9,7 @@
 
 #include <array>
 
+#include "flutter/flow/frame_timings.h"
 #include "flutter/fml/synchronization/waitable_event.h"
 #include "flutter/fml/time/time_delta.h"
 #include "flutter/fml/time/time_point.h"
@@ -73,8 +74,9 @@
 
   fml::AutoResetWaitableEvent latch;
   vsync_waiter->AsyncWaitForVsync(
-      [&latch](fml::TimePoint frame_start_time,
-               fml::TimePoint frame_target_time) { latch.Signal(); });
+      [&latch](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
+        latch.Signal();
+      });
   SignalVsyncEvent();
 
   bool did_timeout =