Delete Skia-specific performance overlay implementation (#176364)

(Essentially a re-issue of
https://github.com/flutter/flutter/pull/174682 which ended up with
broken Google Testing links)

We had 2 different implementations of the rendering code for the
performance overlay layer. The skia version used some skia-specific code
to render the overlay incrementally into an offscreen surface and so we
created a different implementation for Impeller that only uses standard
rendering calls (and no surface cache). It turns out that the Impeller
version was faster anyway even on Skia so it is a simple change to
delete the old code and always use the new visualizer.

The new visualizer reduces the time to render the graph from just under
1ms to about .1ms on Skia.
The new visualizer takes .1ms longer to compute on the UI thread, but
overall we save time between the 2 threads.
The new visualizer is much faster on Impeller.

Some work could be done to save some of that time on the UI thread by
only incrementally updating the graph data, but for now we can take the
~.8-.9ms savings with just some deleted code.
This commit is contained in:
Jim Graham 2025-10-02 00:11:39 -07:00 committed by GitHub
parent 2bf5da46a5
commit b77acbf30b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 77 additions and 285 deletions

View File

@ -82,8 +82,6 @@ source_set("flow") {
"stopwatch.h",
"stopwatch_dl.cc",
"stopwatch_dl.h",
"stopwatch_sk.cc",
"stopwatch_sk.h",
"surface.cc",
"surface.h",
"surface_frame.cc",

View File

@ -12,7 +12,6 @@
#include "display_list/dl_text_skia.h"
#include "flow/stopwatch.h"
#include "flow/stopwatch_dl.h"
#include "flow/stopwatch_sk.h"
#include "third_party/skia/include/core/SkFont.h"
#include "third_party/skia/include/core/SkFontMgr.h"
#include "third_party/skia/include/core/SkTextBlob.h"
@ -44,12 +43,8 @@ void VisualizeStopWatch(DlCanvas* canvas,
if (show_graph) {
DlRect visualization_rect = DlRect::MakeXYWH(x, y, width, height);
if (impeller_enabled) {
DlStopwatchVisualizer(stopwatch, point_storage, color_storage)
.Visualize(canvas, visualization_rect);
} else {
SkStopwatchVisualizer(stopwatch).Visualize(canvas, visualization_rect);
}
DlStopwatchVisualizer(stopwatch, point_storage, color_storage)
.Visualize(canvas, visualization_rect);
}
if (show_labels) {

View File

@ -73,6 +73,7 @@ static void TestPerformanceOverlayLayerGold(int refresh_rate) {
.ui_time = mock_stopwatch,
.texture_registry = nullptr,
.raster_cache = nullptr,
.impeller_enabled = false,
// clang-format on
};
@ -141,7 +142,13 @@ class ImageSizeTextBlobInspector : public virtual DlOpReceiver,
const DlPoint& point,
DlImageSampling sampling,
bool render_with_attributes) override {
sizes_.push_back(image->GetBounds().GetSize());
// We no longer render performance overlays with temp images.
FML_UNREACHABLE();
}
void drawVertices(const std::shared_ptr<DlVertices>& vertices,
DlBlendMode mode) override {
sizes_.push_back(vertices->GetBounds().GetSize());
}
void drawText(const std::shared_ptr<DlText>& text,
@ -151,17 +158,17 @@ class ImageSizeTextBlobInspector : public virtual DlOpReceiver,
text_positions_.push_back(DlPoint(x, y));
}
const std::vector<DlISize>& sizes() { return sizes_; }
const std::vector<DlSize>& sizes() { return sizes_; }
const std::vector<std::shared_ptr<DlText>> texts() { return texts_; }
const std::vector<DlPoint> text_positions() { return text_positions_; }
private:
std::vector<DlISize> sizes_;
std::vector<DlSize> sizes_;
std::vector<std::shared_ptr<DlText>> texts_;
std::vector<DlPoint> text_positions_;
};
TEST_F(PerformanceOverlayLayerTest, PaintingEmptyLayerDies) {
TEST_F(PerformanceOverlayLayerTest, PaintingEmptyLayerDoesNotDie) {
const uint64_t overlay_opts = kVisualizeRasterizerStatistics;
auto layer = std::make_shared<PerformanceOverlayLayer>(overlay_opts);
@ -169,8 +176,7 @@ TEST_F(PerformanceOverlayLayerTest, PaintingEmptyLayerDies) {
EXPECT_EQ(layer->paint_bounds(), DlRect());
EXPECT_FALSE(layer->needs_painting(paint_context()));
// Crashes reading a nullptr.
EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), "");
layer->Paint(paint_context());
}
TEST_F(PerformanceOverlayLayerTest, InvalidOptions) {
@ -237,7 +243,7 @@ TEST_F(PerformanceOverlayLayerTest, MarkAsDirtyWhenResized) {
layer->set_paint_bounds(DlRect::MakeLTRB(0.0f, 0.0f, 48.0f, 48.0f));
layer->Preroll(preroll_context());
layer->Paint(display_list_paint_context());
DlISize first_draw_size;
DlSize first_draw_size;
{
ImageSizeTextBlobInspector inspector;
display_list()->Dispatch(inspector);

View File

@ -6,8 +6,6 @@
namespace flutter {
static const size_t kMaxSamples = 120;
Stopwatch::Stopwatch(const RefreshRateUpdater& updater)
: refresh_rate_updater_(updater), start_(fml::TimePoint::Now()) {
const fml::TimeDelta delta = fml::TimeDelta::Zero();
@ -26,11 +24,10 @@ FixedRefreshRateUpdater::FixedRefreshRateUpdater(
void Stopwatch::Start() {
start_ = fml::TimePoint::Now();
current_sample_ = (current_sample_ + 1) % kMaxSamples;
}
void Stopwatch::Stop() {
laps_[current_sample_] = fml::TimePoint::Now() - start_;
SetLapTime(fml::TimePoint::Now() - start_);
}
void Stopwatch::SetLapTime(const fml::TimeDelta& delta) {
@ -39,7 +36,7 @@ void Stopwatch::SetLapTime(const fml::TimeDelta& delta) {
}
const fml::TimeDelta& Stopwatch::LastLap() const {
return laps_[(current_sample_ - 1) % kMaxSamples];
return laps_[current_sample_];
}
const fml::TimeDelta& Stopwatch::GetLap(size_t index) const {

View File

@ -16,6 +16,9 @@ namespace flutter {
class Stopwatch {
public:
// The number of samples that will be accumulated for performance monitoring
static const size_t kMaxSamples = 120;
/// The refresh rate interface for `Stopwatch`.
class RefreshRateUpdater {
public:
@ -56,7 +59,7 @@ class Stopwatch {
const RefreshRateUpdater& refresh_rate_updater_;
fml::TimePoint start_;
std::vector<fml::TimeDelta> laps_;
size_t current_sample_ = 0;
size_t current_sample_ = kMaxSamples - 1u;
FML_DISALLOW_COPY_AND_ASSIGN(Stopwatch);
};

View File

@ -20,21 +20,21 @@ static const size_t kMaxFrameMarkers = 8;
void DlStopwatchVisualizer::Visualize(DlCanvas* canvas,
const DlRect& rect) const {
auto painter = DlVertexPainter(vertices_storage_, color_storage_);
DlVertexPainter painter(vertices_storage_, color_storage_);
DlPaint paint;
// Establish the graph position.
auto const x = rect.GetX();
auto const y = rect.GetY();
auto const width = rect.GetWidth();
auto const height = rect.GetHeight();
auto const bottom = rect.GetBottom();
const DlScalar x = rect.GetX();
const DlScalar y = rect.GetY();
const DlScalar width = rect.GetWidth();
const DlScalar height = rect.GetHeight();
const DlScalar bottom = rect.GetBottom();
// Scale the graph to show time frames up to those that are 3x the frame time.
auto const one_frame_ms = GetFrameBudget().count();
auto const max_interval = one_frame_ms * 3.0;
auto const max_unit_interval = UnitFrameInterval(max_interval);
auto const sample_unit_width = (1.0 / kMaxSamples);
const DlScalar one_frame_ms = GetFrameBudget().count();
const DlScalar max_interval = one_frame_ms * 3.0;
const DlScalar max_unit_interval = UnitFrameInterval(max_interval);
const DlScalar sample_unit_width = width / kMaxSamples;
// resize backing storage to match expected lap count.
size_t required_storage =
@ -50,20 +50,21 @@ void DlStopwatchVisualizer::Visualize(DlCanvas* canvas,
// Prepare a path for the data; we start at the height of the last point so
// it looks like we wrap around.
{
for (auto i = 0u; i < stopwatch_.GetLapsCount(); i++) {
auto const sample_unit_height =
(1.0 - UnitHeight(stopwatch_.GetLap(i).ToMillisecondsF(),
max_unit_interval));
DlScalar bar_left = x;
for (size_t i = 0u; i < stopwatch_.GetLapsCount(); i++) {
const double time_ms = stopwatch_.GetLap(i).ToMillisecondsF();
const DlScalar sample_unit_height = static_cast<DlScalar>(
height * UnitHeight(time_ms, max_unit_interval));
auto const bar_width = width * sample_unit_width;
auto const bar_height = height * sample_unit_height;
auto const bar_left = x + width * sample_unit_width * i;
const DlScalar bar_top = bottom - sample_unit_height;
const DlScalar bar_right = x + (i + 1) * sample_unit_width;
painter.DrawRect(DlRect::MakeLTRB(/*left=*/bar_left,
/*top=*/y + bar_height,
/*right=*/bar_left + bar_width,
/*top=*/bar_top,
/*right=*/bar_right,
/*bottom=*/bottom),
DlColor(0xAA0000FF));
bar_left = bar_right;
}
}
@ -71,22 +72,22 @@ void DlStopwatchVisualizer::Visualize(DlCanvas* canvas,
{
if (max_interval > one_frame_ms) {
// Paint the horizontal markers.
auto count = static_cast<size_t>(max_interval / one_frame_ms);
size_t count = static_cast<size_t>(max_interval / one_frame_ms);
// Limit the number of markers to a reasonable amount.
if (count > kMaxFrameMarkers) {
count = 1;
}
for (auto i = 0u; i < count; i++) {
auto const frame_height =
for (uint32_t i = 0u; i < count; i++) {
const DlScalar frame_height =
height * (1.0 - (UnitFrameInterval(i + 1) * one_frame_ms) /
max_unit_interval);
// Draw a skinny rectangle (i.e. a line).
painter.DrawRect(DlRect::MakeLTRB(/*left=*/x,
/*top=*/y + frame_height,
/*right=*/width,
/*right=*/x + width,
/*bottom=*/y + frame_height + 1),
DlColor(0xCC000000));
}
@ -100,12 +101,12 @@ void DlStopwatchVisualizer::Visualize(DlCanvas* canvas,
// budget exceeded.
color = DlColor::kRed();
}
auto const l =
x + width * (static_cast<double>(stopwatch_.GetCurrentSample()) /
kMaxSamples);
auto const t = y;
auto const r = l + width * sample_unit_width;
auto const b = rect.GetBottom();
size_t sample =
(stopwatch_.GetCurrentSample() + 1) % stopwatch_.GetLapsCount();
const DlScalar l = x + sample * sample_unit_width;
const DlScalar t = y;
const DlScalar r = l + sample_unit_width;
const DlScalar b = rect.GetBottom();
painter.DrawRect(DlRect::MakeLTRB(l, t, r, b), color);
}
@ -124,10 +125,10 @@ DlVertexPainter::DlVertexPainter(std::vector<DlPoint>& vertices_storage,
: vertices_(vertices_storage), colors_(color_storage) {}
void DlVertexPainter::DrawRect(const DlRect& rect, const DlColor& color) {
auto const left = rect.GetLeft();
auto const top = rect.GetTop();
auto const right = rect.GetRight();
auto const bottom = rect.GetBottom();
const DlScalar left = rect.GetLeft();
const DlScalar top = rect.GetTop();
const DlScalar right = rect.GetRight();
const DlScalar bottom = rect.GetBottom();
FML_DCHECK(6 + colors_offset_ <= vertices_.size());
FML_DCHECK(6 + colors_offset_ <= colors_.size());
@ -139,7 +140,7 @@ void DlVertexPainter::DrawRect(const DlRect& rect, const DlColor& color) {
vertices_[vertices_offset_++] = DlPoint(right, bottom); // tl
vertices_[vertices_offset_++] = DlPoint(left, bottom); // bl br
vertices_[vertices_offset_++] = DlPoint(left, top); //
for (auto i = 0u; i < 6u; i++) {
for (size_t i = 0u; i < 6u; i++) {
colors_[colors_offset_++] = color;
}
}

View File

@ -1,186 +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.
#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 = 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 DlRect& rect) const {
// Initialize visualize cache if it has not yet been initialized.
InitVisualizeSurface(SkISize::Make(rect.GetWidth(), rect.GetHeight()));
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(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.GetOrigin(), DlImageSampling::kNearestNeighbor);
}
} // namespace flutter

View File

@ -1,40 +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.
#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 DlRect& 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_

View File

@ -51,15 +51,33 @@ 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));
EXPECT_EQ(stopwatch.GetLap(0), fml::TimeDelta::FromMilliseconds(10));
}
TEST(Instrumentation, GetCurrentSampleTest) {
TEST(Instrumentation, GetCurrentSampleStartStopTest) {
fml::Milliseconds frame_budget_90fps = fml::RefreshRateToFrameBudget(90);
FixedRefreshRateStopwatch stopwatch(frame_budget_90fps);
// Stopwatch starts primed to place the first sample in slot 0 when
// the actual time is available.
EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(Stopwatch::kMaxSamples - 1u));
stopwatch.Start();
// CurrentSample still not updated because we are still measuring
// this frame.
EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(Stopwatch::kMaxSamples - 1u));
stopwatch.Stop();
EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(1));
// The most current sample is placed in slot #0.
EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(0));
}
TEST(Instrumentation, GetCurrentSampleSetLapTimeTest) {
fml::Milliseconds frame_budget_90fps = fml::RefreshRateToFrameBudget(90);
FixedRefreshRateStopwatch stopwatch(frame_budget_90fps);
// Stopwatch starts primed to place the first sample in slot 0 when
// the actual time is available.
EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(Stopwatch::kMaxSamples - 1u));
stopwatch.SetLapTime(fml::TimeDelta::FromMilliseconds(10));
// The most current sample is placed in slot #0.
EXPECT_EQ(stopwatch.GetCurrentSample(), size_t(0));
}
TEST(Instrumentation, GetLapsCount) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB