Refactor `Stopwatch` into `StopwatchVisualizer` and `SkStopwatchVisualizer` (#45200)

Partial work towards https://github.com/flutter/flutter/issues/126009.

Still working as before:
<img width="1210" alt="Screenshot 2023-08-28 at 6 32 45 PM"
src="https://github.com/flutter/engine/assets/168174/38728015-d0d4-4933-bd31-d2326c76aeee">

(There are some `PerformanceOverlayLayerDefault.*` tests that don't run
locally, so I guess I'll let CI run those)
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 11251d2..3ae421e 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -846,6 +846,8 @@
 ORIGIN: ../../../flutter/flow/skia_gpu_object.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/flow/stopwatch.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/flow/stopwatch.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/flow/stopwatch_sk.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/flow/stopwatch_sk.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/flow/surface.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/flow/surface.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/flow/surface_frame.cc + ../../../flutter/LICENSE
@@ -3592,6 +3594,8 @@
 FILE: ../../../flutter/flow/skia_gpu_object.h
 FILE: ../../../flutter/flow/stopwatch.cc
 FILE: ../../../flutter/flow/stopwatch.h
+FILE: ../../../flutter/flow/stopwatch_sk.cc
+FILE: ../../../flutter/flow/stopwatch_sk.h
 FILE: ../../../flutter/flow/surface.cc
 FILE: ../../../flutter/flow/surface.h
 FILE: ../../../flutter/flow/surface_frame.cc
diff --git a/flow/BUILD.gn b/flow/BUILD.gn
index f52d325..5d69ae2 100644
--- a/flow/BUILD.gn
+++ b/flow/BUILD.gn
@@ -77,6 +77,8 @@
     "skia_gpu_object.h",
     "stopwatch.cc",
     "stopwatch.h",
+    "stopwatch_sk.cc",
+    "stopwatch_sk.h",
     "surface.cc",
     "surface.h",
     "surface_frame.cc",
diff --git a/flow/layers/performance_overlay_layer.cc b/flow/layers/performance_overlay_layer.cc
index 2b693b2..4275a99 100644
--- a/flow/layers/performance_overlay_layer.cc
+++ b/flow/layers/performance_overlay_layer.cc
@@ -8,6 +8,7 @@
 #include <iostream>
 #include <string>
 
+#include "flow/stopwatch_sk.h"
 #include "third_party/skia/include/core/SkFont.h"
 #include "third_party/skia/include/core/SkTextBlob.h"
 
@@ -29,7 +30,11 @@
 
   if (show_graph) {
     SkRect visualization_rect = SkRect::MakeXYWH(x, y, width, height);
-    stopwatch.Visualize(canvas, visualization_rect);
+
+    // TODO(matanlurey): Select a visualizer based on the current backend.
+    // https://github.com/flutter/flutter/issues/126009
+    SkStopwatchVisualizer visualizer = SkStopwatchVisualizer(stopwatch);
+    visualizer.Visualize(canvas, visualization_rect);
   }
 
   if (show_labels) {
diff --git a/flow/stopwatch.cc b/flow/stopwatch.cc
index 4d28f37..b8e8d30 100644
--- a/flow/stopwatch.cc
+++ b/flow/stopwatch.cc
@@ -4,14 +4,9 @@
 
 #include "flutter/flow/stopwatch.h"
 
-#include "include/core/SkCanvas.h"
-#include "third_party/skia/include/core/SkPath.h"
-#include "third_party/skia/include/core/SkSurface.h"
-
 namespace flutter {
 
 static const size_t kMaxSamples = 120;
-static const size_t kMaxFrameMarkers = 8;
 
 Stopwatch::Stopwatch(const RefreshRateUpdater& updater)
     : refresh_rate_updater_(updater),
@@ -19,8 +14,6 @@
       current_sample_(0) {
   const fml::TimeDelta delta = fml::TimeDelta::Zero();
   laps_.resize(kMaxSamples, delta);
-  cache_dirty_ = true;
-  prev_drawn_sample_index_ = 0;
 }
 
 Stopwatch::~Stopwatch() = default;
@@ -51,12 +44,20 @@
   return laps_[(current_sample_ - 1) % kMaxSamples];
 }
 
-double Stopwatch::UnitFrameInterval(double raster_time_ms) const {
-  return raster_time_ms / GetFrameBudget().count();
+const fml::TimeDelta& Stopwatch::GetLap(size_t index) const {
+  return laps_[index];
 }
 
-double Stopwatch::UnitHeight(double raster_time_ms,
-                             double max_unit_interval) const {
+size_t Stopwatch::GetCurrentSample() const {
+  return current_sample_;
+}
+
+double StopwatchVisualizer::UnitFrameInterval(double raster_time_ms) const {
+  return raster_time_ms / stopwatch_.GetFrameBudget().count();
+}
+
+double StopwatchVisualizer::UnitHeight(double raster_time_ms,
+                                       double max_unit_interval) const {
   double unit_height = UnitFrameInterval(raster_time_ms) / max_unit_interval;
   if (unit_height > 1.0) {
     unit_height = 1.0;
@@ -82,169 +83,6 @@
   return sum / kMaxSamples;
 }
 
-// Initialize the SkSurface for drawing into. Draws the base background and any
-// timing data from before the initial Visualize() call.
-void Stopwatch::InitVisualizeSurface(SkISize size) const {
-  // Mark as dirty if the size has changed.
-  if (visualize_cache_surface_) {
-    if (size.width() != visualize_cache_surface_->width() ||
-        size.height() != visualize_cache_surface_->height()) {
-      cache_dirty_ = true;
-    };
-  }
-
-  if (!cache_dirty_) {
-    return;
-  }
-  cache_dirty_ = false;
-
-  // TODO(garyq): Use a GPU surface instead of a CPU surface.
-  visualize_cache_surface_ =
-      SkSurfaces::Raster(SkImageInfo::MakeN32Premul(size));
-
-  SkCanvas* cache_canvas = visualize_cache_surface_->getCanvas();
-
-  // Establish the graph position.
-  const SkScalar x = 0;
-  const SkScalar y = 0;
-  const SkScalar width = size.width();
-  const SkScalar height = size.height();
-
-  SkPaint paint;
-  paint.setColor(0x99FFFFFF);
-  cache_canvas->drawRect(SkRect::MakeXYWH(x, y, width, height), paint);
-
-  // Scale the graph to show frame times up to those that are 3 times the frame
-  // time.
-  const double one_frame_ms = GetFrameBudget().count();
-  const double max_interval = one_frame_ms * 3.0;
-  const double max_unit_interval = UnitFrameInterval(max_interval);
-
-  // Draw the old data to initially populate the graph.
-  // Prepare a path for the data. We start at the height of the last point, so
-  // it looks like we wrap around
-  SkPath path;
-  path.setIsVolatile(true);
-  path.moveTo(x, height);
-  path.lineTo(x, y + height * (1.0 - UnitHeight(laps_[0].ToMillisecondsF(),
-                                                max_unit_interval)));
-  double unit_x;
-  double unit_next_x = 0.0;
-  for (size_t i = 0; i < kMaxSamples; i += 1) {
-    unit_x = unit_next_x;
-    unit_next_x = (static_cast<double>(i + 1) / kMaxSamples);
-    const double sample_y =
-        y + height * (1.0 - UnitHeight(laps_[i].ToMillisecondsF(),
-                                       max_unit_interval));
-    path.lineTo(x + width * unit_x, sample_y);
-    path.lineTo(x + width * unit_next_x, sample_y);
-  }
-  path.lineTo(
-      width,
-      y + height * (1.0 - UnitHeight(laps_[kMaxSamples - 1].ToMillisecondsF(),
-                                     max_unit_interval)));
-  path.lineTo(width, height);
-  path.close();
-
-  // Draw the graph.
-  paint.setColor(0xAA0000FF);
-  cache_canvas->drawPath(path, paint);
-}
-
-void Stopwatch::Visualize(DlCanvas* canvas, const SkRect& rect) const {
-  // Initialize visualize cache if it has not yet been initialized.
-  InitVisualizeSurface(SkISize::Make(rect.width(), rect.height()));
-
-  SkCanvas* cache_canvas = visualize_cache_surface_->getCanvas();
-  SkPaint paint;
-
-  // Establish the graph position.
-  const SkScalar x = 0;
-  const SkScalar y = 0;
-  const SkScalar width = visualize_cache_surface_->width();
-  const SkScalar height = visualize_cache_surface_->height();
-
-  // Scale the graph to show frame times up to those that are 3 times the frame
-  // time.
-  const double one_frame_ms = GetFrameBudget().count();
-  const double max_interval = one_frame_ms * 3.0;
-  const double max_unit_interval = UnitFrameInterval(max_interval);
-
-  const double sample_unit_width = (1.0 / kMaxSamples);
-
-  // Draw vertical replacement bar to erase old/stale pixels.
-  paint.setColor(0x99FFFFFF);
-  paint.setStyle(SkPaint::Style::kFill_Style);
-  paint.setBlendMode(SkBlendMode::kSrc);
-  double sample_x =
-      x + width * (static_cast<double>(prev_drawn_sample_index_) / kMaxSamples);
-  const auto eraser_rect = SkRect::MakeLTRB(
-      sample_x, y, sample_x + width * sample_unit_width, height);
-  cache_canvas->drawRect(eraser_rect, paint);
-
-  // Draws blue timing bar for new data.
-  paint.setColor(0xAA0000FF);
-  paint.setBlendMode(SkBlendMode::kSrcOver);
-  const auto bar_rect = SkRect::MakeLTRB(
-      sample_x,
-      y + height * (1.0 -
-                    UnitHeight(laps_[current_sample_ == 0 ? kMaxSamples - 1
-                                                          : current_sample_ - 1]
-                                   .ToMillisecondsF(),
-                               max_unit_interval)),
-      sample_x + width * sample_unit_width, height);
-  cache_canvas->drawRect(bar_rect, paint);
-
-  // Draw horizontal frame markers.
-  paint.setStrokeWidth(0);  // hairline
-  paint.setStyle(SkPaint::Style::kStroke_Style);
-  paint.setColor(0xCC000000);
-
-  if (max_interval > one_frame_ms) {
-    // Paint the horizontal markers
-    size_t frame_marker_count =
-        static_cast<size_t>(max_interval / one_frame_ms);
-
-    // Limit the number of markers displayed. After a certain point, the graph
-    // becomes crowded
-    if (frame_marker_count > kMaxFrameMarkers) {
-      frame_marker_count = 1;
-    }
-
-    for (size_t frame_index = 0; frame_index < frame_marker_count;
-         frame_index++) {
-      const double frame_height =
-          height * (1.0 - (UnitFrameInterval((frame_index + 1) * one_frame_ms) /
-                           max_unit_interval));
-      cache_canvas->drawLine(x, y + frame_height, width, y + frame_height,
-                             paint);
-    }
-  }
-
-  // Paint the vertical marker for the current frame.
-  // We paint it over the current frame, not after it, because when we
-  // paint this we don't yet have all the times for the current frame.
-  paint.setStyle(SkPaint::Style::kFill_Style);
-  paint.setBlendMode(SkBlendMode::kSrcOver);
-  if (UnitFrameInterval(LastLap().ToMillisecondsF()) > 1.0) {
-    // budget exceeded
-    paint.setColor(SK_ColorRED);
-  } else {
-    // within budget
-    paint.setColor(SK_ColorGREEN);
-  }
-  sample_x = x + width * (static_cast<double>(current_sample_) / kMaxSamples);
-  const auto marker_rect = SkRect::MakeLTRB(
-      sample_x, y, sample_x + width * sample_unit_width, height);
-  cache_canvas->drawRect(marker_rect, paint);
-  prev_drawn_sample_index_ = current_sample_;
-
-  // Draw the cached surface onto the output canvas.
-  auto image = DlImage::Make(visualize_cache_surface_->makeImageSnapshot());
-  canvas->DrawImage(image, {rect.x(), rect.y()},
-                    DlImageSampling::kNearestNeighbor);
-}
-
 fml::Milliseconds Stopwatch::GetFrameBudget() const {
   return refresh_rate_updater_.GetFrameBudget();
 }
diff --git a/flow/stopwatch.h b/flow/stopwatch.h
index 005697d..d6e8f0d 100644
--- a/flow/stopwatch.h
+++ b/flow/stopwatch.h
@@ -12,8 +12,6 @@
 #include "flutter/fml/time/time_delta.h"
 #include "flutter/fml/time/time_point.h"
 
-#include "third_party/skia/include/core/SkSurface.h"
-
 namespace flutter {
 
 class Stopwatch {
@@ -32,16 +30,16 @@
 
   ~Stopwatch();
 
+  const fml::TimeDelta& GetLap(size_t index) const;
+
+  size_t GetCurrentSample() const;
+
   const fml::TimeDelta& LastLap() const;
 
   fml::TimeDelta MaxDelta() const;
 
   fml::TimeDelta AverageDelta() const;
 
-  void InitVisualizeSurface(SkISize size) const;
-
-  void Visualize(DlCanvas* canvas, const SkRect& rect) const;
-
   void Start();
 
   void Stop();
@@ -52,20 +50,11 @@
   fml::Milliseconds GetFrameBudget() const;
 
  private:
-  inline double UnitFrameInterval(double time_ms) const;
-  inline double UnitHeight(double time_ms, double max_height) const;
-
   const RefreshRateUpdater& refresh_rate_updater_;
   fml::TimePoint start_;
   std::vector<fml::TimeDelta> laps_;
   size_t current_sample_;
 
-  // Mutable data cache for performance optimization of the graphs. Prevents
-  // expensive redrawing of old data.
-  mutable bool cache_dirty_;
-  mutable sk_sp<SkSurface> visualize_cache_surface_;
-  mutable size_t prev_drawn_sample_index_;
-
   FML_DISALLOW_COPY_AND_ASSIGN(Stopwatch);
 };
 
@@ -91,6 +80,38 @@
   FixedRefreshRateUpdater fixed_delegate_;
 };
 
+//------------------------------------------------------------------------------
+/// @brief        Abstract class for visualizing (i.e. drawing) a stopwatch.
+///
+/// @note         This was originally folded into the |Stopwatch| class, but
+///               was separated out to make it easier to change the underlying
+///               implementation (which relied directly on Skia, not showing on
+///               Impeller: https://github.com/flutter/flutter/issues/126009).
+class StopwatchVisualizer {
+ public:
+  explicit StopwatchVisualizer(const Stopwatch& stopwatch)
+      : stopwatch_(stopwatch) {}
+
+  virtual ~StopwatchVisualizer() = default;
+
+  /// @brief      Renders the stopwatch as a graph.
+  ///
+  /// @param      canvas  The canvas to draw on.
+  /// @param[in]  rect    The rectangle to draw in.
+  virtual void Visualize(DlCanvas* canvas, const SkRect& rect) const = 0;
+
+  FML_DISALLOW_COPY_AND_ASSIGN(StopwatchVisualizer);
+
+ protected:
+  /// @brief      Converts a raster time to a unit interval.
+  double UnitFrameInterval(double time_ms) const;
+
+  /// @brief      Converts a raster time to a unit height.
+  double UnitHeight(double time_ms, double max_height) const;
+
+  const Stopwatch& stopwatch_;
+};
+
 }  // namespace flutter
 
 #endif  // FLUTTER_FLOW_INSTRUMENTATION_H_
diff --git a/flow/stopwatch_sk.cc b/flow/stopwatch_sk.cc
new file mode 100644
index 0000000..16a3411
--- /dev/null
+++ b/flow/stopwatch_sk.cc
@@ -0,0 +1,187 @@
+// 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/stopwatch_sk.h"
+#include "include/core/SkCanvas.h"
+#include "include/core/SkImageInfo.h"
+#include "include/core/SkPaint.h"
+#include "include/core/SkPath.h"
+#include "include/core/SkSize.h"
+#include "include/core/SkSurface.h"
+
+namespace flutter {
+
+static const size_t kMaxSamples = 120;
+static const size_t kMaxFrameMarkers = 8;
+
+void SkStopwatchVisualizer::InitVisualizeSurface(SkISize size) const {
+  // Mark as dirty if the size has changed.
+  if (visualize_cache_surface_) {
+    if (size.width() != visualize_cache_surface_->width() ||
+        size.height() != visualize_cache_surface_->height()) {
+      cache_dirty_ = true;
+    };
+  }
+
+  if (!cache_dirty_) {
+    return;
+  }
+  cache_dirty_ = false;
+
+  // TODO(garyq): Use a GPU surface instead of a CPU surface.
+  visualize_cache_surface_ =
+      SkSurfaces::Raster(SkImageInfo::MakeN32Premul(size));
+
+  SkCanvas* cache_canvas = visualize_cache_surface_->getCanvas();
+
+  // Establish the graph position.
+  const SkScalar x = 0;
+  const SkScalar y = 0;
+  const SkScalar width = size.width();
+  const SkScalar height = size.height();
+
+  SkPaint paint;
+  paint.setColor(0x99FFFFFF);
+  cache_canvas->drawRect(SkRect::MakeXYWH(x, y, width, height), paint);
+
+  // Scale the graph to show frame times up to those that are 3 times the frame
+  // time.
+  const double one_frame_ms = stopwatch_.GetFrameBudget().count();
+  const double max_interval = one_frame_ms * 3.0;
+  const double max_unit_interval = UnitFrameInterval(max_interval);
+
+  // Draw the old data to initially populate the graph.
+  // Prepare a path for the data. We start at the height of the last point, so
+  // it looks like we wrap around
+  SkPath path;
+  path.setIsVolatile(true);
+  path.moveTo(x, height);
+  path.lineTo(
+      x, y + height * (1.0 - UnitHeight(stopwatch_.GetLap(0).ToMillisecondsF(),
+                                        max_unit_interval)));
+  double unit_x;
+  double unit_next_x = 0.0;
+  for (size_t i = 0; i < kMaxSamples; i += 1) {
+    unit_x = unit_next_x;
+    unit_next_x = (static_cast<double>(i + 1) / kMaxSamples);
+    const double sample_y =
+        y + height * (1.0 - UnitHeight(stopwatch_.GetLap(i).ToMillisecondsF(),
+                                       max_unit_interval));
+    path.lineTo(x + width * unit_x, sample_y);
+    path.lineTo(x + width * unit_next_x, sample_y);
+  }
+  path.lineTo(
+      width,
+      y + height *
+              (1.0 -
+               UnitHeight(stopwatch_.GetLap(kMaxSamples - 1).ToMillisecondsF(),
+                          max_unit_interval)));
+  path.lineTo(width, height);
+  path.close();
+
+  // Draw the graph.
+  paint.setColor(0xAA0000FF);
+  cache_canvas->drawPath(path, paint);
+}
+
+void SkStopwatchVisualizer::Visualize(DlCanvas* canvas,
+                                      const SkRect& rect) const {
+  // Initialize visualize cache if it has not yet been initialized.
+  InitVisualizeSurface(SkISize::Make(rect.width(), rect.height()));
+
+  SkCanvas* cache_canvas = visualize_cache_surface_->getCanvas();
+  SkPaint paint;
+
+  // Establish the graph position.
+  const SkScalar x = 0;
+  const SkScalar y = 0;
+  const SkScalar width = visualize_cache_surface_->width();
+  const SkScalar height = visualize_cache_surface_->height();
+
+  // Scale the graph to show frame times up to those that are 3 times the frame
+  // time.
+  const double one_frame_ms = stopwatch_.GetFrameBudget().count();
+  const double max_interval = one_frame_ms * 3.0;
+  const double max_unit_interval = UnitFrameInterval(max_interval);
+
+  const double sample_unit_width = (1.0 / kMaxSamples);
+
+  // Draw vertical replacement bar to erase old/stale pixels.
+  paint.setColor(0x99FFFFFF);
+  paint.setStyle(SkPaint::Style::kFill_Style);
+  paint.setBlendMode(SkBlendMode::kSrc);
+  double sample_x =
+      x + width * (static_cast<double>(prev_drawn_sample_index_) / kMaxSamples);
+  const auto eraser_rect = SkRect::MakeLTRB(
+      sample_x, y, sample_x + width * sample_unit_width, height);
+  cache_canvas->drawRect(eraser_rect, paint);
+
+  // Draws blue timing bar for new data.
+  paint.setColor(0xAA0000FF);
+  paint.setBlendMode(SkBlendMode::kSrcOver);
+  const auto bar_rect = SkRect::MakeLTRB(
+      sample_x,
+      y + height *
+              (1.0 -
+               UnitHeight(stopwatch_
+                              .GetLap(stopwatch_.GetCurrentSample() == 0
+                                          ? kMaxSamples - 1
+                                          : stopwatch_.GetCurrentSample() - 1)
+                              .ToMillisecondsF(),
+                          max_unit_interval)),
+      sample_x + width * sample_unit_width, height);
+  cache_canvas->drawRect(bar_rect, paint);
+
+  // Draw horizontal frame markers.
+  paint.setStrokeWidth(0);  // hairline
+  paint.setStyle(SkPaint::Style::kStroke_Style);
+  paint.setColor(0xCC000000);
+
+  if (max_interval > one_frame_ms) {
+    // Paint the horizontal markers
+    size_t frame_marker_count =
+        static_cast<size_t>(max_interval / one_frame_ms);
+
+    // Limit the number of markers displayed. After a certain point, the graph
+    // becomes crowded
+    if (frame_marker_count > kMaxFrameMarkers) {
+      frame_marker_count = 1;
+    }
+
+    for (size_t frame_index = 0; frame_index < frame_marker_count;
+         frame_index++) {
+      const double frame_height =
+          height * (1.0 - (UnitFrameInterval((frame_index + 1) * one_frame_ms) /
+                           max_unit_interval));
+      cache_canvas->drawLine(x, y + frame_height, width, y + frame_height,
+                             paint);
+    }
+  }
+
+  // Paint the vertical marker for the current frame.
+  // We paint it over the current frame, not after it, because when we
+  // paint this we don't yet have all the times for the current frame.
+  paint.setStyle(SkPaint::Style::kFill_Style);
+  paint.setBlendMode(SkBlendMode::kSrcOver);
+  if (UnitFrameInterval(stopwatch_.LastLap().ToMillisecondsF()) > 1.0) {
+    // budget exceeded
+    paint.setColor(SK_ColorRED);
+  } else {
+    // within budget
+    paint.setColor(SK_ColorGREEN);
+  }
+  sample_x = x + width * (static_cast<double>(stopwatch_.GetCurrentSample()) /
+                          kMaxSamples);
+  const auto marker_rect = SkRect::MakeLTRB(
+      sample_x, y, sample_x + width * sample_unit_width, height);
+  cache_canvas->drawRect(marker_rect, paint);
+  prev_drawn_sample_index_ = stopwatch_.GetCurrentSample();
+
+  // Draw the cached surface onto the output canvas.
+  auto image = DlImage::Make(visualize_cache_surface_->makeImageSnapshot());
+  canvas->DrawImage(image, {rect.x(), rect.y()},
+                    DlImageSampling::kNearestNeighbor);
+}
+
+}  // namespace flutter
diff --git a/flow/stopwatch_sk.h b/flow/stopwatch_sk.h
new file mode 100644
index 0000000..c1dfb24
--- /dev/null
+++ b/flow/stopwatch_sk.h
@@ -0,0 +1,40 @@
+// 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_STOPWATCH_SK_H_
+#define FLUTTER_FLOW_STOPWATCH_SK_H_
+
+#include "flow/stopwatch.h"
+#include "include/core/SkSurface.h"
+
+namespace flutter {
+
+//------------------------------------------------------------------------------
+/// A stopwatch visualizer that uses Skia (|SkCanvas|) to draw the stopwatch.
+///
+/// @see DlStopwatchVisualizer for the newer non-backend specific version.
+class SkStopwatchVisualizer : public StopwatchVisualizer {
+ public:
+  explicit SkStopwatchVisualizer(const Stopwatch& stopwatch)
+      : StopwatchVisualizer(stopwatch) {}
+
+  void Visualize(DlCanvas* canvas, const SkRect& rect) const override;
+
+ private:
+  /// Initializes the |SkSurface| used for drawing the stopwatch.
+  ///
+  /// Draws the base background and any timing data from before the initial
+  /// call to |Visualize|.
+  void InitVisualizeSurface(SkISize size) const;
+
+  // Mutable data cache for performance optimization of the graphs.
+  // Prevents expensive redrawing of old data.
+  mutable bool cache_dirty_ = true;
+  mutable sk_sp<SkSurface> visualize_cache_surface_;
+  mutable size_t prev_drawn_sample_index_ = 0;
+};
+
+}  // namespace flutter
+
+#endif  // FLUTTER_FLOW_STOPWATCH_SK_H_
diff --git a/flow/stopwatch_unittests.cc b/flow/stopwatch_unittests.cc
index 76893c7..3ded514 100644
--- a/flow/stopwatch_unittests.cc
+++ b/flow/stopwatch_unittests.cc
@@ -42,5 +42,20 @@
   EXPECT_EQ(frame_budget_90fps, actual_frame_budget);
 }
 
+TEST(Instrumentation, GetLapByIndexTest) {
+  fml::Milliseconds frame_budget_90fps = fml::RefreshRateToFrameBudget(90);
+  FixedRefreshRateStopwatch stopwatch(frame_budget_90fps);
+  stopwatch.SetLapTime(fml::TimeDelta::FromMilliseconds(10));
+  EXPECT_EQ(stopwatch.GetLap(1), fml::TimeDelta::FromMilliseconds(10));
+}
+
+TEST(Instrumentation, GetCurrentSampleTest) {
+  fml::Milliseconds frame_budget_90fps = fml::RefreshRateToFrameBudget(90);
+  FixedRefreshRateStopwatch stopwatch(frame_budget_90fps);
+  stopwatch.Start();
+  stopwatch.Stop();
+  EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(1));
+}
+
 }  // namespace testing
 }  // namespace flutter
diff --git a/testing/resources/performance_overlay_gold_120fps.png b/testing/resources/performance_overlay_gold_120fps.png
index c19d1eb..9677724 100644
--- a/testing/resources/performance_overlay_gold_120fps.png
+++ b/testing/resources/performance_overlay_gold_120fps.png
Binary files differ
diff --git a/testing/resources/performance_overlay_gold_60fps.png b/testing/resources/performance_overlay_gold_60fps.png
index 4e18fa1..0d45210 100644
--- a/testing/resources/performance_overlay_gold_60fps.png
+++ b/testing/resources/performance_overlay_gold_60fps.png
Binary files differ
diff --git a/testing/resources/performance_overlay_gold_90fps.png b/testing/resources/performance_overlay_gold_90fps.png
index 962e287..d6fb7e8 100644
--- a/testing/resources/performance_overlay_gold_90fps.png
+++ b/testing/resources/performance_overlay_gold_90fps.png
Binary files differ