Trace 汇总

本指南解释如何使用 Perfetto 的 trace 汇总功能从 trace 中提取结构化、可操作的数据。

为什么要使用 Trace 汇总?

PerfettoSQL 是一个强大的工具,用于交互式探索 trace。你可以编写任何想要的查询,结果立即可用。但是,这种灵活性给自动化和大规模分析带来了挑战。SELECT 语句的输出具有任意架构(列名和类型),可能会从一个查询更改为下一个查询。这使得构建消费此数据的通用工具、仪表板或回归检测系统变得困难,因为它们无法依赖稳定的数据结构。

Trace 汇总解决了这个问题。它提供了一种为要从 trace 中提取的数据定义稳定、结构化架构的方法。它不是生成任意表,而是生成一致的 protobuf 消息(TraceSummary),工具很容易解析和处理。

这对于跨 trace 分析尤其强大。通过在数百或数千个 trace 上运行相同的汇总规范,你可以可靠地聚合结果,以跟踪一段时间内的性能 metrics,比较应用程序的不同版本,并自动检测回归。

简而言之,当你需要以下情况时,使用 trace 汇总:

使用标准库生成汇总

最简单的方法是使用 [PerfettoSQL 标准库](/docs/analysis/stdlib-docs.autogen)中的模块。

让我们通过一个示例。假设我们想要计算 trace 中每个进程的平均内存使用量(具体来说,RSS + Swap)。linux.memory.process 模块已经提供了一个表 memory_rss_and_swap_per_process,非常适合此目的。

我们可以定义一个 TraceSummarySpec 来计算此 metrics:

// spec.textproto metric_spec { id: "memory_per_process" dimensions: "process_name" value: "avg_rss_and_swap" query: { table: { table_name: "memory_rss_and_swap_per_process" } referenced_modules: "linux.memory.process" group_by: { column_names: "process_name" aggregates: { column_name: "rss_and_swap" op: DURATION_WEIGHTED_MEAN result_column_name: "avg_rss_and_swap" } } } }

要运行此操作,请将上述内容保存为 spec.textproto 并使用你首选的工具。

from perfetto.trace_processor import TraceProcessor with open('spec.textproto', 'r') as f: spec_text = f.read() with TraceProcessor(trace='my_trace.pftrace') as tp: summary = tp.trace_summary( specs=[spec_text], metric_ids=["memory_per_process"] ) print(summary)

trace_processor_shell --summary \ --summary-spec spec.textproto \ --summary-metrics-v2 memory_per_process \ my_trace.pftrace

使用模板减少重复

通常,你希望计算几个相关的 metrics,这些 metrics 共享相同的底层查询和维度。例如,对于给定进程,你可能想知道最小、最大和平均内存使用量。

与其为每个 metrics 编写单独的 metric_spec,这将涉及重复相同的 querydimensions 块,你可以使用TraceMetricV2TemplateSpec。这更简洁,更不容易出错,并且性能更高,因为底层查询只运行一次。

让我们扩展我们的内存示例,计算每个进程的 RSS+Swap 的最小值、最大值和持续时间加权平均值。

// spec.textproto metric_template_spec { id_prefix: "memory_per_process" dimensions: "process_name" value_columns: "min_rss_and_swap" value_columns: "max_rss_and_swap" value_columns: "avg_rss_and_swap" query: { table: { table_name: "memory_rss_and_swap_per_process" } referenced_modules: "linux.memory.process" group_by: { column_names: "process_name" aggregates: { column_name: "rss_and_swap" op: MIN result_column_name: "min_rss_and_swap" } aggregates: { column_name: "rss_and_swap" op: MAX result_column_name: "max_rss_and_swap" } aggregates: { column_name: "rss_and_swap" op: DURATION_WEIGHTED_MEAN result_column_name: "avg_rss_and_swap" } } } }

这个单一模板生成三个 metrics:

然后你可以运行它,请求任何或所有生成的 metrics,如下所示。

from perfetto.trace_processor import TraceProcessor with open('spec.textproto', 'r') as f: spec_text = f.read() with TraceProcessor(trace='my_trace.pftrace') as tp: summary = tp.trace_summary( specs=[spec_text], metric_ids=[ "memory_per_process_min_rss_and_swap", "memory_per_process_max_rss_and_swap", "memory_per_process_avg_rss_and_swap", ] ) print(summary)

trace_processor_shell --summary \ --summary-spec spec.textproto \ --summary-metrics-v2 memory_per_process_min_rss_and_swap,memory_per_process_max_rss_and_swap,memory_per_process_avg_rss_and_swap \ my_trace.pftrace

添加单位和极性

为了使 metrics 的自动化 profile 和可视化更强大,你可以为 metrics 添加单位和极性(即,较高还是较低的值更好)。

这是通过在 TraceMetricV2TemplateSpec 中使用 value_column_specs 字段而不是简单的 value_columns 来完成的。这允许你为模板生成的每个 metrics 指定 unitpolarity

让我们调整之前的内存示例以包含此信息。我们将指定内存值以 BYTES 为单位,较低的值更好。

// spec.textproto metric_template_spec { id_prefix: "memory_per_process" dimensions: "process_name" value_column_specs: { name: "min_rss_and_swap" unit: BYTES polarity: LOWER_IS_BETTER } value_column_specs: { name: "max_rss_and_swap" unit: BYTES polarity: LOWER_IS_BETTER } value_column_specs: { name: "avg_rss_and_swap" unit: BYTES polarity: LOWER_IS_BETTER } query: { table: { table_name: "memory_rss_and_swap_per_process" } referenced_modules: "linux.memory.process" group_by: { column_names: "process_name" aggregates: { column_name: "rss_and_swap" op: MIN result_column_name: "min_rss_and_swap" } aggregates: { column_name: "rss_and_swap" op: MAX result_column_name: "max_rss_and_swap" } aggregates: { column_name: "rss_and_swap" op: DURATION_WEIGHTED_MEAN result_column_name: "avg_rss_and_swap" } } } }

这将为每个生成 metrics 的 TraceMetricV2Spec 添加指定的 unitpolarity,使输出更丰富,对自动化工具更有用。

使用自定义 SQL 模块生成汇总

虽然标准库很强大,但你经常需要分析特定于你的应用程序的自定义事件。你可以通过编写自己的 SQL 模块并将它们加载到 Trace Processor 中来实现此目的。

SQL 包只是一个包含 .sql 文件的目录。此目录可以加载到 Trace Processor 中,其文件可作为模块使用。

假设你有名为 game_frame 的自定义 slice,并且想要计算平均、最小和最大帧持续时间。

1. 创建自定义 SQL 模块:

创建这样的目录结构:

my_sql_modules/ └── my_game/ └── metrics.sql

metrics.sql 内部,定义一个计算帧统计的视图:

-- my_sql_modules/my_game/metrics.sql CREATE PERFETTO VIEW game_frame_stats AS SELECT 'game_frame' AS frame_type, MIN(dur) AS min_duration_ns, MAX(dur) AS max_duration_ns, AVG(dur) AS avg_duration_ns FROM slice WHERE name = 'game_frame' GROUP BY 1;

2. 在汇总规范中使用模板:

同样,我们可以使用 TraceMetricV2TemplateSpec 从单个共享配置生成这些相关 metrics。

创建一个引用你的自定义模块和视图的 spec.textproto:

// spec.textproto metric_template_spec { id_prefix: "game_frame" dimensions: "frame_type" value_columns: "min_duration_ns" value_columns: "max_duration_ns" value_columns: "avg_duration_ns" query: { table: { // 模块名称是相对于包根目录的目录路径, // 去掉了 .sql 扩展名。 table_name: "game_frame_stats" } referenced_modules: "my_game.metrics" } }

3. 使用你的自定义包运行汇总:

你现在可以使用 Python API 或命令行 shell 计算汇总,告诉 Trace Processor 在哪里找到你的自定义包。

TraceProcessorConfig 中使用 add_sql_packages 参数。

from perfetto.trace_processor import TraceProcessor, TraceProcessorConfig # 自定义 SQL 模块目录的路径 sql_package_path = './my_sql_modules' config = TraceProcessorConfig( add_sql_packages=[sql_package_path] ) with open('spec.textproto', 'r') as f: spec_text = f.read() with TraceProcessor(trace='my_trace.pftrace', config=config) as tp: # 请求一个、一些或所有生成的 metrics。 summary = tp.trace_summary( specs=[spec_text], metric_ids=[ "game_frame_min_duration_ns", "game_frame_max_duration_ns", "game_frame_avg_duration_ns" ] ) print(summary)

使用 --add-sql-package 标志。你可以显式列出 metrics 或使用 all 关键字。

trace_processor_shell --summary \ --add-sql-package ./my_sql_modules \ --summary-spec spec.textproto \ --summary-metrics-v2 game_frame_min_duration_ns,game_frame_max_duration_ns,game_frame_avg_duration_ns \ my_trace.pftrace

常见模式和技巧

列转换

select_columns 字段提供了一种强大的方法来操作查询结果的列。你可以使用 SQL 表达式重命名列并执行转换。

每个 SelectColumn 消息有两个字段:

示例:重命名和转换列

此示例显示如何从 slice 表中选择 tsdur 列,将 ts 重命名为 timestamp,并通过将 dur 从纳秒转换为毫秒来创建新列 dur_ms

query: { table: { table_name: "slice" } select_columns: { column_name_or_expression: "ts" alias: "timestamp" } select_columns: { column_name_or_expression: "dur / 1000" alias: "dur_ms" } }

使用 interval_intersect 分析时间间隔

常见的分析模式是分析来自一个源(例如,CPU 使用率)的数据在来自另一个源(例如,"关键用户旅程"slice)的特定时间窗口内。interval_intersect 查询使这变得容易。

它的工作原理是采用一个 base 查询和一个或多个 interval 查询。结果仅包含与 每个 interval 查询的至少一行在时间上重叠的 base 查询的行。

用例:

示例:特定 CUJ Slice 期间的 CPU 时间

此示例演示如何使用 interval_intersect 查找线程 bar 在来自 "system_server" 进程的任何 "baz_*" slice 的持续时间内的总 CPU 时间。

// 在 id 为 "bar_cpu_time_during_baz_cujs" 的 metric_spec 中 query: { interval_intersect: { base: { // 基础数据是每个线程的 CPU 时间。 table: { table_name: "thread_slice_cpu_time" } referenced_modules: "slices.cpu_time" filters: { column_name: "thread_name" op: EQUAL string_rhs: "bar" } } interval_intersect: { // 间隔是 "baz_*" slice。 simple_slices: { slice_name_glob: "baz_*" process_name_glob: "system_server" } } } group_by: { // 我们对相交间隔的 CPU 时间求和。 aggregates: { column_name: "cpu_time" op: SUM result_column_name: "total_cpu_time" } } }

使用 dependencies 组合查询

Sql 源中的 dependencies 字段允许你通过从其他结构化查询组合它们来构建复杂查询。这对于将复杂分析分解为更小的、可重用的部分特别有用。

每个依赖项都有一个 alias,这是一个字符串,可以在 SQL 查询中用于引用依赖项的结果。SQL 查询然后可以像使用表一样使用此别名。

示例:将 CPU 数据与 CUJ Slice 连接

此示例显示如何使用 dependencies 将 CPU 调度数据与 CUJ slice 连接。我们定义两个依赖项,一个用于 CPU 数据,一个用于 CUJ slice,然后在主 SQL 查询中连接它们。

query: { sql: { sql: "SELECT s.id, s.ts, s.dur, t.track_name FROM $slice_table s JOIN $track_table t ON s.track_id = t.id" column_names: "id" column_names: "ts" column_names: "dur" column_names: "track_name" dependencies: { alias: "slice_table" query: { table: { table_name: "slice" } } } dependencies: { alias: "track_table" query: { table: { table_name: "track" } } } } }

添加 trace 范围的元数据

你可以将键值元数据添加到汇总中,以为 metrics 提供上下文,例如设备型号或操作系统版本。这在分析多个 trace 时特别有用,因为它允许你基于此元数据分组或过滤结果。

元数据与你在同一运行中请求的任何 metrics 一起计算。

1. 在规范中定义元数据查询:

此查询必须返回 "key" 和 "value" 列。

// 在 spec.textproto 中,与你的 metric_spec 定义一起 query { id: "device_info_query" sql { sql: "SELECT 'device_name' AS key, 'Pixel Test' AS value" column_names: "key" column_names: "value" } }

2. 使用 metrics 和元数据运行汇总:

运行汇总时,你指定要计算的 metrics 和用于元数据的查询。

传递 metric_idsmetadata_query_id:

summary = tp.trace_summary( specs=[spec_text], metric_ids=["game_frame_avg_duration_ns"], metadata_query_id="device_info_query" )

使用 --summary-metrics-v2--summary-metadata-query:

trace_processor_shell --summary \\ --summary-spec spec.textproto \\ --summary-metrics-v2 game_frame_avg_duration_ns \\ --summary-metadata-query device_info_query \\ my_trace.pftrace

输出格式

汇总的结果是 TraceSummary protobuf 消息。此消息包含 metric_bundles 字段,这是一个 TraceMetricV2Bundle 消息列表。

每个 bundle 可以包含一起计算的一个或多个 metrics 的结果。使用 TraceMetricV2TemplateSpec 是创建 bundle 的最常见方式。从单个模板生成的所有 metrics 都自动放置在同一个 bundle 中,共享相同的 specsrow 结构。这非常高效,因为维度值通常是重复的,每行只写入一次。

示例输出

对于 memory_per_process 模板示例,输出 TraceSummary 将包含一个 TraceMetricV2Bundle,如下所示:

# 在 TraceSummary 的 metric_bundles 字段中: metric_bundles { # 模板生成的所有三个 metrics 的规范。 specs { id: "memory_per_process_min_rss_and_swap" dimensions: "process_name" value: "min_rss_and_swap" # ... 查询详细信息 ... } specs { id: "memory_per_process_max_rss_and_swap" dimensions: "process_name" value: "max_rss_and_swap" # ... 查询详细信息 ... } specs { id: "memory_per_process_avg_rss_and_swap" dimensions: "process_name" value: "avg_rss_and_swap" # ... 查询详细信息 ... } # 每行包含一组维度和三个值,对应于 # `specs` 中的三个 metrics。 row { values { double_value: 100000 } # min values { double_value: 200000 } # max values { double_value: 123456.789 } # avg dimension { string_value: "com.example.app" } } row { values { double_value: 80000 } # min values { double_value: 150000 } # max values { double_value: 98765.432 } # avg dimension { string_value: "system_server" } } # ... }

与旧版 metrics 系统的比较

Perfetto 以前有一个不同的系统来计算 metrics,通常称为"v1 metrics"。Trace 汇总是此系统的后继者,设计为更健壮且更易于使用。

以下是主要区别:

参考

运行汇总

你可以使用不同的 Perfetto 工具计算汇总。

对于编程工作流,使用 TraceProcessor 类的 trace_summary 方法。

from perfetto.trace_processor import TraceProcessor # 假设 'tp' 是一个初始化的 TraceProcessor 实例 # 而 'spec_text' 包含你的 TraceSummarySpec。 summary_proto = tp.trace_summary( specs=[spec_text], metric_ids=["example_metric"], metadata_query_id="device_info_query" ) print(summary_proto)

trace_summary 方法接受以下参数:

  • specsTraceSummarySpec 定义的列表(作为文本或字节)。
  • metric_ids : 要计算的可选 metrics ID 列表。如果为 None,则计算规范中的所有 metrics。
  • metadata_query_id : 用于 trace 范围元数据的查询的可选 ID。

trace_processor_shell 允许你使用专用标志从 trace 文件计算 trace 汇总。

  • 按 ID 运行特定 metrics :使用 --summary-metrics-v2 标志提供逗号分隔的 metrics ID 列表。
trace_processor_shell --summary \\ --summary-spec YOUR_SPEC_FILE \\ --summary-metrics-v2 METRIC_ID_1,METRIC_ID_2 \\ TRACE_FILE
  • 运行规范中定义的所有 metrics :使用关键字 all
trace_processor_shell --summary \\ --summary-spec YOUR_SPEC_FILE \\ --summary-metrics-v2 all \\ TRACE_FILE
  • 输出格式 :使用 --summary-format 控制输出格式。
    • text:人类可读的文本 protobuf(默认)。
    • binary:二进制 protobuf。

TraceSummarySpec

用于配置汇总的顶级消息。它包含:

TraceSummary

汇总输出的顶级消息。它包含:

TraceMetricV2Spec

定义单个 metrics。

TraceMetricV2TemplateSpec

定义用于从单个共享配置生成多个相关 metrics 的模板。当你有几个共享相同查询和维度的 metrics 时,这对于减少重复很有用。

使用模板会自动将生成的 metrics 捆绑到输出中的单个 TraceMetricV2Bundle 中。

TraceMetricV2Bundle

包含捆绑在一起的一个或多个 metrics 的结果。

PerfettoSqlStructuredQuery

PerfettoSqlStructuredQuery 消息提供了一种结构化的方法来定义 PerfettoSQL 查询。它是通过定义数据 source 然后选择性地应用 filtersgroup_by 操作和 select_columns 转换来构建的。

查询源

查询的源可以是以下之一:

查询操作

这些操作按顺序应用于来自源的数据:

聚合运算符

group_by 操作允许你使用以下聚合函数:

运算符 描述
COUNT 计算每个组中的行数。如果未指定 column_name,则变为 COUNT(*)(计算所有行)。
SUM 计算数值列的总和。
MIN 查找数值列的最小值。
MAX 查找数值列的最大值。
MEAN 计算数值列的平均值。
MEDIAN 计算数值列的第 50 百分位数。
DURATION_WEIGHTED_MEAN 计算数值列的持续时间加权平均值。这对于应根据持续时间加权的时序数据很有用。
PERCENTILE 计算数值列的给定百分位数。百分位数在 Aggregate 消息的 percentile 字段中指定。
聚合字段要求
示例:计算第 99 百分位数

此示例显示如何计算 slice 表中 dur 列的第 99 百分位数。

query: { table: { table_name: "slice" } group_by: { aggregates: { column_name: "dur" op: PERCENTILE result_column_name: "p99_dur" percentile: 99 } } }