Flutter Engine 内部计时:精确测量 Render/Layout/Paint 阶段的 C++ 耗时

Flutter Engine 内部计时:精确测量 Render/Layout/Paint 阶段的 C++ 耗时

在现代用户界面框架中,性能是用户体验的基石。任何微小的卡顿或延迟都可能导致用户感知上的不流畅,从而影响应用的整体质量。Flutter 作为一个高性能的跨平台 UI 框架,其底层渲染引擎是用 C++ 实现的,这使得它能够充分利用系统资源,提供接近原生应用的性能。然而,正是这种底层的 C++ 实现,也为性能瓶颈的诊断带来了挑战。当我们面对一个复杂的 Flutter 应用性能问题时,往往需要深入到引擎内部,精确测量其核心渲染流程中 C++ 代码的执行耗时,特别是布局(Layout)、绘制(Paint)和光栅化(Rasterization)这三个关键阶段。

本讲座将深入探讨如何在 Flutter Engine 内部实现这种精确的 C++ 计时。我们将从理解 Flutter 渲染管道的结构开始,逐步识别 C++ 代码中对应的测量点,选择合适的计时机制,并提供详细的代码示例来演示如何集成这些测量工具,最终收集、报告和解读这些性能数据。

1. 性能测量的必然性与 Flutter 渲染管线的概述

1.1 性能测量的必然性

对于任何高性能的 UI 框架,精确的性能测量都是不可或缺的。它提供了量化的数据,帮助开发者:

  • 识别瓶颈: 快速定位导致应用卡顿或响应缓慢的具体代码区域。
  • 优化决策: 基于数据而非猜测进行优化,确保投入的精力能带来实际的性能提升。
  • 回归检测: 在代码变更后,验证性能是否受到负面影响。
  • 用户体验保障: 确保应用在各种设备和场景下都能提供流畅、响应迅速的用户体验。

Flutter 的独特之处在于其“自带渲染引擎”的架构。它不依赖于平台原生的 UI 组件,而是通过 Skia 渲染引擎直接在画布上绘制像素。这意味着,从 Dart 层的 Widget 树到屏幕上的最终像素,整个渲染过程的大部分关键步骤都在 C++ 层完成。因此,要深入理解 Flutter 应用的性能特征,特别是当 Dart 层的优化空间有限时,对 C++ 引擎内部的精确计时变得尤为重要。

1.2 Flutter 渲染管线的高层视图

Flutter 的渲染管线是一个多阶段的过程,它将 Dart 层的声明式 UI 定义转换为屏幕上的像素。虽然 Dart 层处理了 Widget 树的构建和 Element 树的更新,但核心的布局和绘制工作则下放到了 C++ 层。以下是关键阶段及其与 C++ 的关系:

  1. 构建阶段(Build Phase – Dart):

    • 开发者通过 Widget 树描述 UI。
    • Flutter 构建 Element 树,代表 Widget 树的实例。
    • 创建或更新 RenderObject 树,这是实际进行布局和绘制的对象。
    • C++ 关联: 此阶段主要在 Dart VM 中执行,但 RenderObject 对象的创建和生命周期管理会涉及 C++ 层的内存分配和对象管理。
  2. 布局阶段(Layout Phase – C++ / Skia):

    • RenderObject 树接收父级提供的约束(constraints)。
    • 每个 RenderObject 根据其子节点的布局结果和自身逻辑计算自身的大小和位置。
    • 这是一个自底向上的过程。
    • C++ 关联: RenderObject 的核心布局逻辑(如 performLayout 方法)虽然在 Dart 中定义,但其在引擎中的调用栈会经过 C++ 层。更重要的是,在将 RenderObject 树转换为 Layer 树的过程中,会有一个“预渲染”(Preroll)阶段,其中包含了大量的布局相关计算,这部分完全在 C++ 中执行。
  3. 绘制阶段(Paint Phase – C++ / Skia):

    • 每个 RenderObject 调用其 paint 方法,向一个抽象的 Canvas 对象发出绘制命令。
    • 这些命令被收集起来,形成一个 Picture 或一系列 Layer
    • C++ 关联: 绘制命令的生成、管理和分发,以及 Layer 树的构建,都是在 C++ 层完成的。SkCanvas 是 Skia 提供的 C++ 接口,用于接收绘制命令。
  4. 光栅化阶段(Rasterization Phase – C++ / Skia / GPU):

    • 收集到的绘制命令(PictureLayer 树)被发送到 GPU 线程。
    • Skia 引擎将这些高层级的绘制命令转换为底层的 GPU 指令(如 OpenGL ES 或 Vulkan)。
    • GPU 执行这些指令,将像素写入帧缓冲区。
    • C++ 关联: 整个光栅化过程,包括 Skia 上下文管理、GPU 资源分配、命令提交等,都完全在 C++ 中执行。这是 CPU 和 GPU 之间交互最密集的地方。
  5. 合成与显示(Composition & Display):

    • 最终的帧缓冲区内容被提交给显示硬件,在 VSync 信号到来时显示到屏幕上。
    • C++ 关联: 平台相关的合成器接口(如 EGLMetalDirectX)由 C++ 层管理。

在本讲座中,我们的关注点将集中在 布局、绘制和光栅化 这三个主要在 C++ 层执行的阶段,因为它们是 Flutter Engine 最核心的性能瓶颈所在。

2. 识别 Flutter Engine 源代码中的测量目标

为了精确测量 C++ 耗时,我们首先需要深入 Flutter Engine 的源代码,找出对应于布局、绘制和光栅化阶段的关键函数和代码路径。Flutter Engine 的代码结构复杂,但通过跟踪渲染管线的流程,我们可以逐步缩小范围。

2.1 核心概念与关键类

在 Flutter Engine 中,与渲染直接相关的 C++ 核心库是 flowlib/ui

  • lib/ui (Dart UI Library): 这是 Dart 层与 C++ Engine 交互的桥梁,包含了 SceneSceneBuilderPictureCanvas 等对象的 C++ 实现。Dart 代码通过 dart:ui 库调用这些 C++ 接口。
  • flow (Compositor): 这是 Flutter 的合成器,负责管理 Layer 树,执行预渲染(Preroll)、绘制(Paint)和光栅化(Rasterization)等核心任务。它将 Dart UI Library 产生的 PictureLayer 转换为 GPU 可执行的命令。

2.2 布局阶段 (Layout) 的测量点

尽管 Dart RenderObjectperformLayout 方法是布局的核心,但其调用栈在 C++ 引擎中表现为 LayerTreePreroll 阶段。Preroll 的主要目的是遍历 Layer 树,计算每个 Layer 的几何信息,并执行一些预处理操作,这与布局的概念紧密相连。

  • flow/layer_tree.cc 中的 LayerTree::Preroll 方法: 这是整个 Layer 树预渲染的入口点。在此处开始计时,可以捕获整个树的布局前计算耗时。
  • flow/layers/container_layer.cc 及其子类中的 Preroll 方法: 每个具体的 Layer 类型都会实现自己的 Preroll 逻辑,这些是更细粒度的测量点。

2.3 绘制阶段 (Paint) 的测量点

绘制阶段涉及将 Layer 树的内容转换为 Skia 绘制命令。

  • flow/layer_tree.cc 中的 LayerTree::Paint 方法: 这是整个 Layer 树绘制的入口点。在此处开始计时,可以捕获整个树的绘制命令生成耗时。
  • flow/layers/container_layer.cc 及其子类中的 Paint 方法: 具体的 Layer 类型(如 PictureLayer, TextureLayer 等)会实现各自的 Paint 逻辑,将自身内容绘制到 SkCanvas 上。
  • flow/paint_context.h/cc PaintContext 管理着绘制过程中的 SkCanvas 和其他状态。它内部可能包含一些绘制前的设置和绘制后的清理操作,也可能提供一些聚合绘制命令的机制。

2.4 光栅化阶段 (Rasterization) 的测量点

光栅化是将 Skia 绘制命令提交给 GPU 并等待其执行的过程。这通常发生在 GPU 线程上。

  • flow/compositor_context.cc 中的 CompositorContext::Raster 方法: 这是引擎中触发光栅化的主要入口点。它接收一个 LayerTree,并负责将其渲染到帧缓冲区。
  • flow/compositor_context.cc 内部对 GrContext::flushAndSync() 或类似 GPU 提交操作的调用: 这是将 Skia 内部的绘制缓冲区刷新到 GPU 的关键点。flushAndSync 会阻塞 CPU,直到 GPU 完成提交的工作,因此测量其耗时可以近似反映 GPU 提交的延迟。然而,需要注意的是,GPU 实际的渲染工作是异步的,flushAndSync 只是提交命令,并非等待所有像素渲染完成。要测量实际的 GPU 渲染时间,需要更底层的 GPU 计时 API(如 OpenGL GL_TIMESTAMP 或 Vulkan VK_QUERY_TYPE_TIMESTAMP),这超出了本讲座的 C++ CPU 计时范围,但值得提及。
  • shell/platform/common/engine.cc 中的 Engine::Render 这是从 Dart VM 调用到 C++ 引擎进行渲染的最高层级入口之一,它会调用 CompositorContext::Raster

总结测量点表格:

阶段 核心 C++ 文件/类 关键方法/操作 计时范围
布局 (Layout) flow/layer_tree.cc LayerTree::Preroll 整个 Layer 树的预渲染计算,包括几何信息确定和部分资源预加载。
flow/layers/*.cc Layer::Preroll 单个 Layer 的预渲染逻辑。
绘制 (Paint) flow/layer_tree.cc LayerTree::Paint 整个 Layer 树的绘制命令生成,将 Layer 内容转换为 Skia 绘制指令。
flow/layers/*.cc Layer::Paint 单个 Layer 的绘制逻辑,向 SkCanvas 发出绘制命令。
lib/ui/painting/scene.cc Scene::build Dart SceneBuilder 最终提交 LayerTree 的 C++ 入口。
光栅化 (Rasterization) flow/compositor_context.cc CompositorContext::Raster 负责将 LayerTree 转换为 GPU 可执行指令,并提交给 GPU。
flow/compositor_context.cc 内部对 GrContext::flushAndSync() 或类似调用 将 Skia 绘制命令刷新到 GPU 驱动,等待提交完成(CPU 侧等待)。

3. 选择合适的 C++ 计时机制

在 C++ 中进行高精度计时有多种选择。我们需要考虑精度、开销、可移植性和与现有工具的集成能力。

3.1 高精度计时器:std::chrono::high_resolution_clock

这是 C++11 及更高版本提供的标准库计时器,是测量代码执行时间的最佳选择,因为它:

  • 高精度: 通常映射到操作系统提供的最高精度计时器(如 QueryPerformanceCounter on Windows, mach_absolute_time on macOS, clock_gettime on Linux/Android)。
  • 可移植性: 跨平台一致的 API。
  • 易用性: 提供了清晰的 time_pointduration 类型,支持各种时间单位的转换。

示例:std::chrono 基本用法

#include <chrono>
#include <iostream>

int main() {
    auto start = std::chrono::high_resolution_clock::now();

    // 模拟一段耗时代码
    long long sum = 0;
    for (int i = 0; i < 1000000; ++i) {
        sum += i;
    }

    auto end = std::chrono::high_resolution_clock::now();

    // 计算持续时间
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    std::cout << "Elapsed time: " << duration.count() << " microseconds" << std::endl;
    std::cout << "Sum: " << sum << std::endl;

    return 0;
}

3.2 CPU 周期计数器 (rdtsc)

  • rdtsc (Read Time-Stamp Counter): 这是一个 x86/x64 处理器指令,直接读取 CPU 内部的 TSC 寄存器,提供极高的精度(每个 CPU 周期)。
  • 优点: 极度精确,开销极低。
  • 缺点:
    • 平台依赖: 仅限于 x86/x64 架构。
    • 非时间单位: TSC 值是 CPU 周期数,需要转换为实际时间(秒/毫秒),这取决于 CPU 频率。
    • 频率变化: 现代 CPU 频率动态调整(睿频、降频),使得 TSC 到时间的转换复杂且不稳定。
    • 多核同步: 不同核心的 TSC 可能不同步。
    • 上下文切换: 操作系统上下文切换可能导致测量不准确。
  • 结论: 对于通用、可移植且稳定的计时,std::chrono 更优。rdtsc 适用于极度底层的微基准测试,且需要精确控制 CPU 状态的场景。在 Flutter Engine 这种复杂的多线程环境中,不推荐直接使用 rdtsc

3.3 Flutter 内部追踪宏:FML_TRACE_EVENT

Flutter Engine 已经内置了一套基于 Chromium base/trace_event 的追踪系统,通过 fml/trace_event.h 提供宏。

  • 优点:
    • 与 DevTools 集成: 通过这些宏记录的事件可以直接被 Dart DevTools 或 Chrome Tracing 工具可视化,极大地简化了数据分析。
    • 低开销: 在未启用追踪时,这些宏是无操作的,几乎没有性能影响。
    • 上下文信息: 可以记录事件名称、类别、线程 ID、时间戳以及任意键值对参数。
    • 线程安全: 追踪系统设计考虑了多线程环境。
  • 缺点:
    • 虽然低开销,但启用追踪本身会引入一些运行时开销(字符串复制、内存分配、锁)。
    • 计时精度依赖于内部实现,通常不如 std::chrono::high_resolution_clock 直接测量那么“裸”。

3.4 决策:

对于 Flutter Engine 内部的精确 C++ 耗时测量,我们推荐结合使用:

  1. std::chrono::high_resolution_clock 用于在关键代码路径中实现一个通用的、高精度的 Stopwatch 工具类。这提供了最原始、最精确的计时数据。
  2. FML_TRACE_EVENT 用于将 Stopwatch 收集到的精确时间数据报告给外部工具(如 DevTools),实现可视化和集中管理。

这种组合方式既能保证测量的精度,又能利用 Flutter 现有工具链的优势进行数据展示和分析。

4. 实现精确计时:C++ Stopwatch 工具类与引擎集成

我们将首先创建一个通用的 C++ Stopwatch 工具类,然后演示如何将其集成到 Flutter Engine 的布局、绘制和光栅化阶段。

4.1 Stopwatch C++ 工具类

这个 Stopwatch 类将使用 std::chrono::high_resolution_clock 来测量时间,并提供一个简单易用的接口。

stopwatch.h

#ifndef FLUTTER_ENGINE_PERF_STOPWATCH_H_
#define FLUTTER_ENGINE_PERF_STOPWATCH_H_

#include <chrono>
#include <string>
#include <iostream> // For optional direct output
#include <utility>  // For std::move

// A simple high-resolution stopwatch for measuring elapsed time.
class Stopwatch {
public:
    // Constructor. Takes an optional name for identification in logging.
    explicit Stopwatch(std::string name = "")
        : name_(std::move(name)),
          running_(false),
          elapsed_microseconds_(0) {}

    // Destructor. If the stopwatch is still running, it will stop and print elapsed time.
    ~Stopwatch() {
        if (running_) {
            Stop();
            // In a production engine, you might not want to print directly in destructor.
            // Instead, report to a dedicated logging/tracing system.
            // PrintElapsedTime();
        }
    }

    // Starts the stopwatch. If already running, it continues from the current elapsed time.
    void Start() {
        if (!running_) {
            start_time_ = std::chrono::high_resolution_clock::now();
            running_ = true;
        }
    }

    // Stops the stopwatch and accumulates the elapsed time.
    void Stop() {
        if (running_) {
            end_time_ = std::chrono::high_resolution_clock::now();
            elapsed_microseconds_ += std::chrono::duration_cast<std::chrono::microseconds>(end_time_ - start_time_).count();
            running_ = false;
        }
    }

    // Resets the stopwatch to zero elapsed time and stops it.
    void Reset() {
        running_ = false;
        elapsed_microseconds_ = 0;
    }

    // Returns the total elapsed time in microseconds.
    // If the stopwatch is running, it returns the time from start until now.
    long long GetElapsedTimeMicroseconds() const {
        if (running_) {
            auto current_end = std::chrono::high_resolution_clock::now();
            return elapsed_microseconds_ + std::chrono::duration_cast<std::chrono::microseconds>(current_end - start_time_).count();
        }
        return elapsed_microseconds_;
    }

    // Returns the total elapsed time in milliseconds.
    long long GetElapsedTimeMilliseconds() const {
        return GetElapsedTimeMicroseconds() / 1000;
    }

    // Prints the elapsed time to standard output.
    void PrintElapsedTime() const {
        if (!name_.empty()) {
            std::cout << "[" << name_ << "] ";
        }
        std::cout << "Elapsed time: " << GetElapsedTimeMicroseconds() << " us" << std::endl;
    }

    // Check if the stopwatch is currently running.
    bool IsRunning() const {
        return running_;
    }

private:
    std::string name_;
    std::chrono::high_resolution_clock::time_point start_time_;
    std::chrono::high_resolution_clock::time_point end_time_;
    bool running_;
    long long elapsed_microseconds_; // Accumulated time in microseconds
};

#endif // FLUTTER_ENGINE_PERF_STOPWATCH_H_

4.2 将 Stopwatch 集成到 Flutter Engine

以下示例将演示如何将 Stopwatch 集成到之前识别出的关键测量点。为了保持清晰,我们将使用假设的、简化的代码路径,但这些路径与真实的 Flutter Engine 结构高度对应。

在实际集成时,你需要:

  1. stopwatch.h 添加到 Flutter Engine 源代码的某个合适位置(例如 fml/flow/ 目录下,或者专门的 perf/ 目录)。
  2. 更新相应的 CMakeLists.txtgn 文件,确保 stopwatch.h 可以被包含。
  3. 在需要测量的 C++ 文件中包含 stopwatch.hfml/trace_event.h

4.2.1 测量布局阶段 (Layout Phase)

我们将在 LayerTree::Preroll 方法中进行计时。

修改 flow/layer_tree.cc (示例)

#include "flow/layer_tree.h"
#include "flow/paint_context.h"
#include "fml/trace_event.h" // For FML_TRACE_EVENT
#include "path/to/stopwatch.h" // Assume stopwatch.h is here

namespace flutter {
namespace flow {

LayerTree::LayerTree() = default;

LayerTree::~LayerTree() = default;

void LayerTree::Preroll(const PrerollContext& context, SkCanvas* canvas) {
    // Start timing for the entire Preroll phase (Layout-related work)
    Stopwatch preroll_timer("LayerTree::Preroll (Layout)");
    preroll_timer.Start();

    FML_TRACE_EVENT("Flutter", "Preroll_Start"); // Trace event for DevTools

    // Original Preroll logic
    if (root_layer_) {
        root_layer_->Preroll(context, canvas);
    }

    // Stop timing
    preroll_timer.Stop();
    long long elapsed_us = preroll_timer.GetElapsedTimeMicroseconds();

    // Report the duration to FML_TRACE_EVENT
    // This allows DevTools to display the duration of this event.
    FML_TRACE_EVENT("Flutter", "Preroll_End", "duration_us", elapsed_us);

    // Optionally, store this duration in a performance metrics object
    // Or print to console for immediate debugging.
    // preroll_timer.PrintElapsedTime();
}

// ... other LayerTree methods
} // namespace flow
} // namespace flutter

解释:

  • 我们创建了一个 Stopwatch 实例 preroll_timer
  • Preroll 方法的开始处调用 preroll_timer.Start()
  • Preroll 方法的结束处(在所有 root_layer_->Preroll 工作完成后)调用 preroll_timer.Stop()
  • 通过 preroll_timer.GetElapsedTimeMicroseconds() 获取精确的耗时。
  • 使用 FML_TRACE_EVENT 宏记录事件。这里的 Preroll_End 事件带有 duration_us 参数,DevTools 会自动识别并显示为事件的持续时间。

4.2.2 测量绘制阶段 (Paint Phase)

我们将在 LayerTree::Paint 方法中进行计时。

修改 flow/layer_tree.cc (示例)

#include "flow/layer_tree.h"
#include "flow/paint_context.h"
#include "fml/trace_event.h"
#include "path/to/stopwatch.h"

namespace flutter {
namespace flow {

// ... existing LayerTree methods ...

void LayerTree::Paint(PaintContext& context, SkCanvas* canvas) {
    // Start timing for the entire Paint phase (drawing command generation)
    Stopwatch paint_timer("LayerTree::Paint (Paint)");
    paint_timer.Start();

    FML_TRACE_EVENT("Flutter", "Paint_Start");

    // Original Paint logic
    if (root_layer_) {
        root_layer_->Paint(context, canvas);
    }

    // Stop timing
    paint_timer.Stop();
    long long elapsed_us = paint_timer.GetElapsedTimeMicroseconds();

    // Report the duration
    FML_TRACE_EVENT("Flutter", "Paint_End", "duration_us", elapsed_us);

    // paint_timer.PrintElapsedTime();
}

// ... other LayerTree methods
} // namespace flow
} // namespace flutter

解释:
与布局阶段类似,我们在 LayerTree::Paint 方法的入口和出口处放置了 StopwatchStartStop 调用,并通过 FML_TRACE_EVENT 报告结果。

4.2.3 测量光栅化阶段 (Rasterization Phase)

光栅化是 CPU 将绘制命令提交给 GPU 的过程。我们将在 CompositorContext::Raster 方法中进行计时,并特别关注 GrContext::flushAndSync() 调用。

修改 flow/compositor_context.cc (示例)

#include "flow/compositor_context.h"
#include "flow/layer_tree.h"
#include "fml/trace_event.h"
#include "path/to/stopwatch.h"
// Assume other necessary Skia and platform headers are included

namespace flutter {
namespace flow {

// ... existing CompositorContext methods ...

std::unique_ptr<FrameTimings> CompositorContext::Raster(
    std::unique_ptr<LayerTree> layer_tree,
    std::unique_ptr<FrameTimings> frame_timings) {

    if (!layer_tree) {
        return frame_timings;
    }

    // Start timing for the entire Rasterization phase
    Stopwatch raster_timer("CompositorContext::Raster (Rasterization)");
    raster_timer.Start();

    FML_TRACE_EVENT("Flutter", "Raster_Start");

    // Get the current render target (e.g., the frame buffer)
    GrBackendRenderTarget render_target;
    // ... setup render_target (platform specific) ...

    // Get the Skia GrContext and SkSurface
    sk_sp<GrContext> gr_context = GetGrContext();
    sk_sp<SkSurface> surface = CreateRenderSurface(gr_context.get(), render_target);
    if (!surface) {
        FML_LOG(ERROR) << "Could not create SkSurface for rasterization.";
        raster_timer.Stop();
        FML_TRACE_EVENT("Flutter", "Raster_End", "duration_us", raster_timer.GetElapsedTimeMicroseconds());
        return frame_timings;
    }

    SkCanvas* canvas = surface->getCanvas();
    if (!canvas) {
        FML_LOG(ERROR) << "Could not get SkCanvas from surface.";
        raster_timer.Stop();
        FML_TRACE_EVENT("Flutter", "Raster_End", "duration_us", raster_timer.GetElapsedTimeMicroseconds());
        return frame_timings;
    }

    // Perform the actual painting of the layer tree onto the canvas
    PaintContext paint_context = {
        .gr_context = gr_context.get(),
        .surface = surface.get(),
        .canvas = canvas,
        .raster_thread_merger = raster_thread_merger_,
        .texture_registry = texture_registry_,
        .impeller_context = impeller_context_.get(),
        .content_is_unoccluded = true, // Simplified
        .view_is_on_screen = true,     // Simplified
    };

    // The LayerTree::Paint method is called here, which we've already timed.
    layer_tree->Paint(paint_context, canvas);

    // --- Critical point for GPU submission ---
    Stopwatch gpu_flush_timer("GPU Flush & Sync");
    gpu_flush_timer.Start();
    FML_TRACE_EVENT("Flutter", "GPU_Flush_Start");

    // Flush Skia commands to the GPU driver and block until submission is complete.
    // This is where CPU waits for GPU command queue to be processed.
    gr_context->flushAndSync();

    gpu_flush_timer.Stop();
    long long flush_elapsed_us = gpu_flush_timer.GetElapsedTimeMicroseconds();
    FML_TRACE_EVENT("Flutter", "GPU_Flush_End", "duration_us", flush_elapsed_us);
    // gpu_flush_timer.PrintElapsedTime();

    // Finalize the frame (e.g., swap buffers, present)
    // ... platform specific presentation logic ...

    // Stop timing for the entire Rasterization phase
    raster_timer.Stop();
    long long raster_elapsed_us = raster_timer.GetElapsedTimeMicroseconds();
    FML_TRACE_EVENT("Flutter", "Raster_End", "duration_us", raster_elapsed_us);

    // Update frame_timings object with measured durations.
    // frame_timings->SetRasterDuration(raster_elapsed_us); // Assuming FrameTimings has such a method
    // frame_timings->SetGpuFlushDuration(flush_elapsed_us);

    return frame_timings;
}

// ... other CompositorContext methods
} // namespace flow
} // namespace flutter

解释:

  • 我们创建了两个 Stopwatchraster_timer 用于测量整个光栅化过程,gpu_flush_timer 专门用于测量 gr_context->flushAndSync() 的耗时。
  • flushAndSync() 是关键,它代表了 CPU 将绘制命令发送到 GPU 驱动并等待其处理完成(至少是提交到 GPU 队列)所花费的时间。这可以揭示 CPU-GPU 同步的开销。
  • 同样,所有时间都会通过 FML_TRACE_EVENT 报告。

4.2.4 自动化计时器 (ScopeTimer)

为了避免手动调用 Start()Stop() 的繁琐,可以创建一个 RAII (Resource Acquisition Is Initialization) 风格的范围计时器。

scope_timer.h

#ifndef FLUTTER_ENGINE_PERF_SCOPE_TIMER_H_
#define FLUTTER_ENGINE_PERF_SCOPE_TIMER_H_

#include "path/to/stopwatch.h"
#include "fml/trace_event.h" // For FML_TRACE_EVENT
#include <string>
#include <utility> // For std::move

// A RAII-style timer that starts on construction and stops on destruction,
// reporting its duration via FML_TRACE_EVENT.
class ScopeTimer {
public:
    // Constructor. Takes a trace category and event name.
    ScopeTimer(const char* category, std::string event_name)
        : category_(category),
          event_name_(std::move(event_name)),
          stopwatch_(event_name_) { // Use the event name for the stopwatch
        stopwatch_.Start();
        // Emit a begin trace event. The duration will be reported by the end event.
        FML_TRACE_EVENT_BEGIN(category_, event_name_.c_str());
    }

    // Destructor. Stops the timer and reports the elapsed time.
    ~ScopeTimer() {
        stopwatch_.Stop();
        long long elapsed_us = stopwatch_.GetElapsedTimeMicroseconds();
        // Emit an end trace event with the duration.
        FML_TRACE_EVENT_END(category_, event_name_.c_str(), "duration_us", elapsed_us);
        // stopwatch_.PrintElapsedTime(); // For debug
    }

    // Prevent copy and assignment
    ScopeTimer(const ScopeTimer&) = delete;
    ScopeTimer& operator=(const ScopeTimer&) = delete;

private:
    const char* category_;
    std::string event_name_;
    Stopwatch stopwatch_;
};

// Convenience macro for creating a ScopeTimer.
// Usage: FML_SCOPED_ENGINE_TIMER("MyCategory", "MyFunction");
#define FML_SCOPED_ENGINE_TIMER(category, event_name) 
    ScopeTimer _scoped_timer_##__LINE__(category, event_name);

#endif // FLUTTER_ENGINE_PERF_SCOPE_TIMER_H_

现在,我们可以用更简洁的方式重写之前的计时代码:

使用 FML_SCOPED_ENGINE_TIMER 宏 (示例)

// flow/layer_tree.cc
#include "flow/layer_tree.h"
#include "flow/paint_context.h"
#include "path/to/scope_timer.h" // Include the new scope timer

namespace flutter {
namespace flow {
// ...

void LayerTree::Preroll(const PrerollContext& context, SkCanvas* canvas) {
    FML_SCOPED_ENGINE_TIMER("Flutter", "LayerTree::Preroll (Layout)"); // One line!

    if (root_layer_) {
        root_layer_->Preroll(context, canvas);
    }
}

void LayerTree::Paint(PaintContext& context, SkCanvas* canvas) {
    FML_SCOPED_ENGINE_TIMER("Flutter", "LayerTree::Paint (Paint)"); // One line!

    if (root_layer_) {
        root_layer_->Paint(context, canvas);
    }
}

// flow/compositor_context.cc
#include "flow/compositor_context.h"
#include "path/to/scope_timer.h"

namespace flutter {
namespace flow {
// ...

std::unique_ptr<FrameTimings> CompositorContext::Raster(
    std::unique_ptr<LayerTree> layer_tree,
    std::unique_ptr<FrameTimings> frame_timings) {

    FML_SCOPED_ENGINE_TIMER("Flutter", "CompositorContext::Raster (Rasterization)"); // Main raster timer

    if (!layer_tree) {
        return frame_timings;
    }

    // ... (surface and canvas setup) ...

    PaintContext paint_context = { /* ... */ };
    layer_tree->Paint(paint_context, canvas);

    // --- Critical point for GPU submission ---
    { // Use a block to limit the scope of gpu_flush_timer
        FML_SCOPED_ENGINE_TIMER("Flutter", "GrContext::flushAndSync");
        gr_context->flushAndSync();
    }

    // ... (platform specific presentation logic) ...

    return frame_timings;
}
// ...
} // namespace flow
} // namespace flutter

使用 FML_SCOPED_ENGINE_TIMER 宏极大地简化了代码,减少了出错的可能性,并确保了 Start/Stop 调用的配对。

5. 数据收集、聚合与报告

仅仅测量时间是不够的,我们还需要有效地收集、聚合这些数据,并以易于理解的方式报告出来。

5.1 数据存储与聚合

  • FrameTimings 对象: Flutter Engine 已经有一个 flow/frame_timings.h 文件,定义了一个 FrameTimings 类,用于存储一帧的各种时间数据(如 VSync 间隔、构建时间、光栅化时间等)。我们可以扩展这个类,添加我们精确测量的布局、绘制、GPU 刷新时间字段。

    // flow/frame_timings.h (hypothetical extension)
    #ifndef FLUTTER_ENGINE_FRAME_TIMINGS_H_
    #define FLUTTER_ENGINE_FRAME_TIMINGS_H_
    
    #include <chrono>
    
    namespace flutter {
    namespace flow {
    
    class FrameTimings {
    public:
        // ... existing methods and members ...
    
        void SetLayoutDuration(long long us) { layout_duration_us_ = us; }
        long long GetLayoutDuration() const { return layout_duration_us_; }
    
        void SetPaintDuration(long long us) { paint_duration_us_ = us; }
        long long GetPaintDuration() const { return paint_duration_us_; }
    
        void SetRasterizationDuration(long long us) { rasterization_duration_us_ = us; }
        long long GetRasterizationDuration() const { return rasterization_duration_us_; }
    
        void SetGpuFlushDuration(long long us) { gpu_flush_duration_us_ = us; }
        long long GetGpuFlushDuration() const { return gpu_flush_duration_us_; }
    
    private:
        // ... existing private members ...
        long long layout_duration_us_ = 0;
        long long paint_duration_us_ = 0;
        long long rasterization_duration_us_ = 0;
        long long gpu_flush_duration_us_ = 0;
    };
    
    } // namespace flow
    } // namespace flutter
    #endif // FLUTTER_ENGINE_FRAME_TIMINGS_H_

    然后在 CompositorContext::Raster 等方法中,将测量结果更新到 FrameTimings 实例:

    // flow/compositor_context.cc
    // ... inside CompositorContext::Raster ...
    FML_SCOPED_ENGINE_TIMER("Flutter", "CompositorContext::Raster (Rasterization)");
    // ...
    if (frame_timings) {
        frame_timings->SetRasterizationDuration(raster_elapsed_us);
    }
    // ...
    {
        FML_SCOPED_ENGINE_TIMER("Flutter", "GrContext::flushAndSync");
        gr_context->flushAndSync();
        if (frame_timings) {
            frame_timings->SetGpuFlushDuration(flush_elapsed_us);
        }
    }
    // ...
    // Similarly, LayerTree::Preroll and LayerTree::Paint would need to be able to access
    // and update a shared FrameTimings object, possibly passed down as a parameter
    // or through a thread-local storage mechanism if timing is cross-thread.
    // For simplicity, we assume these are collected and aggregated at a higher level,
    // e.g., in the Raster method if all happen on the same thread or are passed up.
  • 全局性能统计对象: 对于更长期的统计(如平均值、最大值、最小值、标准差),可以维护一个全局的 PerformanceStatistics 对象,它包含一个环形缓冲区来存储最近 N 帧的数据,并提供聚合报告功能。

5.2 报告机制

  1. Flutter DevTools 集成 (推荐):
    正如我们之前所示,FML_TRACE_EVENT 是与 DevTools 集成最有效的方式。当 DevTools 连接到运行中的 Flutter 应用时,它会自动收集这些追踪事件,并在“性能”或“时间线”视图中以图形化的方式展示出来。

    • 事件类别 (Category): FML_TRACE_EVENT 的第一个参数是类别(例如 "Flutter", "Engine")。这有助于在 DevTools 中过滤和组织事件。
    • 事件名称 (Name): 第二个参数是事件名称(例如 "LayerTree::Preroll", "GrContext::flushAndSync")。
    • 参数 (Arguments): 额外的键值对参数(例如 "duration_us", "frame_id")提供了更丰富的信息。DevTools 会自动解析 duration_us 并将其作为事件的持续时间显示。

    通过 DevTools,你可以:

    • 查看每个阶段的耗时,精确到微秒。
    • 观察不同帧之间的耗时波动。
    • 与 Dart 层的事件(如 Widget Build, Layout, Paint)关联起来,形成完整的性能视图。
    • 识别导致长帧的特定 C++ 操作。
  2. 控制台输出 (调试用):
    在开发和调试阶段,直接将计时结果打印到控制台非常方便。

    // In Stopwatch::PrintElapsedTime()
    // std::cout << "[" << name_ << "] Elapsed time: " << GetElapsedTimeMicroseconds() << " us" << std::endl;

    这在没有 DevTools 连接或需要快速验证时很有用,但缺乏可视化和聚合能力。

  3. 平台特定日志:

    • Android: 使用 FML_LOG 宏(它会映射到 __android_log_print)或直接使用 __android_log_print 将数据写入 logcat
    • iOS/macOS: 使用 os_log 框架进行日志记录。
      这些日志可以在各自平台的开发工具中查看。
  4. 自定义 IPC/共享内存:
    对于更高级的场景,例如需要与自定义外部性能监控工具集成,可以考虑通过进程间通信 (IPC) 或共享内存机制将性能数据实时传输出去。但这会增加引擎的复杂性。

5.3 开销考虑与条件编译

  • 测量本身有开销: 即使是 std::chrono,每次调用 now()、计算 duration 也有微小的 CPU 开销。FML_TRACE_EVENT 在启用时也会有字符串复制、内存分配和同步的开销。
  • 最小化开销:
    • 尽量减少在热路径中的字符串操作和内存分配。
    • Stopwatch 类设计时应避免在 Start/Stop 中进行复杂的逻辑。
  • 条件编译: 在生产环境中,通常不希望这些性能测量工具始终处于激活状态,因为它们会引入额外的开销。可以使用宏进行条件编译,在 Debug/Profile 构建中启用,而在 Release 构建中禁用。

    // In stopwatch.h and scope_timer.h
    #ifdef FLUTTER_ENABLE_PERF_MEASUREMENTS
    // ... Stopwatch and ScopeTimer definitions ...
    #else
    // Dummy definitions for Release builds to minimize overhead
    class Stopwatch { /* ... empty methods ... */ };
    #define FML_SCOPED_ENGINE_TIMER(category, event_name) (void)0
    // ...
    #endif

    然后在构建系统(CMakeLists.txtgn)中定义 FLUTTER_ENABLE_PERF_MEASUREMENTS 宏,例如通过构建参数 -D FLUTTER_ENABLE_PERF_MEASUREMENTS=1

6. 高级考量与挑战

6.1 多线程环境下的计时

Flutter Engine 是一个多线程系统,主要有:

  • UI 线程 (Platform Thread): 处理 Dart VM 消息,构建 LayerTree,执行 PrerollPaint(生成 Skia 命令)。
  • GPU 线程 (Raster Thread): 接收 UI 线程提交的 LayerTree,执行 CompositorContext::Raster,将 Skia 命令光栅化为像素,并提交给 GPU。
  • IO 线程: 处理图片解码、文件读写等。

挑战:

  • 时间归属: 一个帧的渲染工作可能跨越多个线程。例如,布局和绘制在 UI 线程完成,而光栅化在 GPU 线程完成。
  • 同步开销: 线程间通信(如 fml::MessageLoop::PostTask)和同步原语(互斥锁、条件变量)本身也会引入开销,且难以精确归属。

解决方案:

  • FML_TRACE_EVENT 宏会自动记录事件发生的线程 ID。DevTools 可以基于此在时间线视图中清晰地展示不同线程上的事件。
  • 如果一个逻辑操作(如一帧的完整渲染)跨越了多个线程,可以在每个线程上使用不同的 FML_TRACE_EVENT 名称或类别来标记其子阶段,并通过一个共同的 frame_id 参数将它们逻辑关联起来。
  • 对于像 flushAndSync() 这种跨越 CPU-GPU 边界的操作,CPU 侧的计时测量的是 CPU 等待 GPU 驱动响应的时间,而不是 GPU 实际执行渲染的时间。

6.2 异步操作与 GPU 计时

  • GPU 异步性: GPU 是高度并行的异步设备。当 CPU 调用 flushAndSync() 时,它只是将命令提交到 GPU 驱动的队列中。GPU 可能在稍后的时间才真正执行这些命令。这意味着 CPU 侧测量到的“光栅化”时间,特别是 flushAndSync 的时间,并不能完全代表 GPU 完成渲染所花费的实际时间。
  • 真正的 GPU 计时: 要精确测量 GPU 渲染时间,需要使用平台特定的 GPU 计时 API:
    • OpenGL ES: glQueryCounterGL_TIMESTAMP
    • Vulkan: VK_QUERY_TYPE_TIMESTAMP
    • Metal: MTLCommandBuffer 的完成处理程序。
    • DirectX: ID3D12CommandQueue::GetTimestampFrequencyID3D12GraphicsCommandList::EndQuery
      这些 API 需要更深入地集成到渲染后端,通常涉及查询对象、等待 GPU 结果并将其映射回 CPU 可读的时间。这超出了本讲座的 C++ CPU 计时范畴,但对于完整的性能分析至关重要。

6.3 CPU 频率缩放与上下文切换

  • CPU 频率缩放: 现代处理器会动态调整频率以节省电量或提高性能。这会导致基于 CPU 周期计数的计时器(如 rdtsc)测量不准确。std::chrono::high_resolution_clock 通常会通过 OS API 抽象掉这些问题,但极端情况仍可能受影响。
  • 上下文切换: 操作系统调度器可能会在你的代码执行期间切换到另一个进程或线程。这会导致计时器测量到的时间包含其他任务的执行时间,从而引入噪音。通过多次测量取平均值、丢弃异常值可以减轻影响。
  • 解决方案:
    • 进行多次测量并计算平均值或中位数。
    • 在受控环境中进行基准测试,尽量减少系统负载。
    • 在性能分析时,不仅看绝对时间,更要看相对变化趋势。

6.4 测量粒度与开销的平衡

  • 过细的测量粒度会引入过多的计时器开销,从而影响被测量代码的真实性能。
  • 过粗的粒度可能无法定位具体瓶颈。
  • 策略: 从粗粒度(整个布局/绘制/光栅化阶段)开始,如果发现某个阶段耗时异常,再逐步深入到该阶段内部的子函数进行更细粒度的测量。

7. 实践应用与结果解读

精确测量只是第一步,更重要的是如何利用这些数据来指导优化。

7.1 数据的含义

| 阶段 | 高耗时可能原因 |
| 布局 (Layout) | Dart Widget 树过于深层或复杂。 RenderObjectperformLayout 方法实现低效。 频繁的 markNeedsLayout 调用。 |
| Paint (Paint) | 频繁的 markNeedsPaint 调用。 CustomPainter 实现复杂或低效,例如在 paint 方法中执行耗时计算,或者重复绘制复杂几何图形。 图像或纹理过大,或者进行复杂的像素操作(如模糊、滤镜)。 过多的透明层或混合操作。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注