Perfetto - System profiling, app tracing and trace analysis
Perfetto 是一个用于性能检测和跟踪分析的生产级开源堆栈。它提供用于记录系统级和应用级跟踪的服务和库、本机 + Java 堆分析、使用 SQL 分析跟踪的库以及用于可视化和探索多 GB 跟踪的基于 Web 的 UI。
See https://perfetto.dev/docs or the /docs/ directory for documentation.
Perfetto 的设计初衷是取代 chrome://tracing 基础设施的内部结构。Chromium 中的跟踪及其内部结构基于所有主要平台(Android、CrOS、Linux、MacOS、Windows)上的 Perfetto 代码库。系统范围跟踪采用相同的基于服务的架构,但内部使用的是 Chromium Mojo IPC 系统,而不是 Perfetto 自己的 UNIX 套接字。
使用chrome://tracing分析chromium性能:
性能记录的数据大部分通过TRACE_EVENT宏记录:
TRACE_EVENT紧跟的数字表示arg的数量。
Perfetto库快速入门
步骤 1:定义事件类别
首先,在一个头文件中(例如 my_tracing.h
),定义您要追踪的事件类别:
// File: my_tracing.h
#include "perfetto.h"
PERFETTO_DEFINE_CATEGORIES(perfetto::Category("base"),perfetto::Category("v8"),perfetto::Category("cc"));
步骤 2:初始化静态存储
然后,在一个 .cc
文件中(例如 my_tracing.cc
),包含您的类别定义头文件,并初始化静态存储:
// File: my_tracing.cc#include "my_tracing.h"
PERFETTO_TRACK_EVENT_STATIC_STORAGE();
步骤 3:注册轨迹事件并记录事件
最后,在您的应用程序启动时注册轨迹事件。注册之后,您可以使用 TRACE_EVENT
宏记录事件:
// File: main.cc#include "my_tracing.h"int main() {perfetto::TrackEvent::Register();// 记录一个基本的轨迹事件,仅有事件名称。TRACE_EVENT("category", "MyEvent");// 记录一个带有调试注解的轨迹事件(最多两个)。TRACE_EVENT("category", "MyEvent", "parameter", 42);// 记录一个带有强类型参数的轨迹事件。TRACE_EVENT("category", "MyEvent", [](perfetto::EventContext ctx) {ctx.event()->set_foo(42);ctx.event()->set_bar(.5f);});return 0;
}
注意事项
-
轨迹事件必须一致地嵌套。例如,不允许出现以下情况:
TRACE_EVENT_BEGIN("a", "bar", ...); TRACE_EVENT_BEGIN("b", "foo", ...); TRACE_EVENT_END("a"); // 必须先结束 "foo" 事件,再结束 "bar" 事件。 TRACE_EVENT_END("b");
多命名空间支持(高级用法)
如果您的程序使用多个轨迹事件命名空间,则需要为每个命名空间分别进行类别和轨迹事件的注册。使用宏 PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE
来实现一个程序中多个轨迹事件类别集的支持。
每个编译单元可以恰好在一个轨迹事件命名空间中。如果需要,这允许整个程序使用多个轨迹事件数据源和类别列表。
总体数据流:
// The track event library consists of the following layers and components. The
// classes the internal namespace shouldn't be considered part of the public
// API.
// .--------------------------------.
// .----| TRACE_EVENT |----.
// write | | - App instrumentation point | | write
// event | '--------------------------------' | arguments
// V V
// .----------------------------------. .-----------------------------.
// | TrackEvent | | EventContext |
// | - Registry of event categories | | - One track event instance |
// '----------------------------------' '-----------------------------'
// | |
// | | look up
// | is | interning ids
// V V
// .----------------------------------. .-----------------------------.
// | internal::TrackEventDataSource | | TrackEventInternedDataIndex |
// | - Perfetto data source | | - Corresponds to a field in |
// | - Has TrackEventIncrementalState | | in interned_data.proto |
// '----------------------------------' '-----------------------------'
// | | ^
// | | owns (1:many) |
// | write event '-------------------------'
// V
// .----------------------------------.
// | internal::TrackEventInternal |
// | - Outlined code to serialize |
// | one track event |
// '----------------------------------'
//
Perfetto库中用到的有趣的C++技术
性能
一个事件记录库除了能强大的记录各种事件之外,其性能非常重要。
Perfetto 库通过多种方式来保证事件记录的性能:
-
条件记录:Perfetto 允许用户定义不同的事件类别,并且可以动态地控制哪些类别的事件是活跃的。通过这种方式,只有在特定类别被启用时才记录事件,这减少了不必要的性能开销。
-
编译时优化:通过使用宏(如
TRACE_EVENT
)和内联函数,Perfetto 允许编译器在编译时进行优化。例如,如果一个事件类别未被激活,相关的代码(包括参数的计算)可能会被完全移除。 -
高效的参数处理:Perfetto 宏在记录事件之前会检查事件是否开启,从而避免在跟踪被禁用的情况下评估事件参数。这意味着参数的计算只有在事件实际记录的时候才会发生。
-
线程局部存储:Perfetto 通常使用线程局部存储(TLS)来减少在跨线程共享数据时所需的同步开销。
-
增量状态:Perfetto 的跟踪数据源可以维护一种增量状态,这样某些信息(如字符串)就不需要在每个事件中重复记录,而是仅在首次出现时记录,之后通过索引引用,从而减少数据量。
-
避免锁竞争:Perfetto 设计了一套避免锁竞争的机制,例如使用无锁队列和缓冲区来收集事件数据,从而最小化线程间同步的需要。
-
序列化性能:Perfetto 的内部序列化代码高度优化,确保将事件数据快速转换为可以写入磁盘的格式时的效率。
-
内存管理:Perfetto 通过精心设计的内存管理策略来避免不必要的内存分配和复制。例如,它可以使用环形缓冲区来重复使用内存,以及通过字符串和其他数据的内部化减少内存使用。
-
低开销的上下文切换:对于跟踪点的插入,Perfetto 努力确保上下文切换的开销最小,即使是在高度并发的环境中。
-
分层设计:Perfetto 的设计允许开发者在不同层次上使用其 API,以便他们可以选择最合适他们需要的性能和灵活性的平衡点。
举例来说,通过constexpr + 特例化模板,实现将事件在编译期进行分类:
// By default no statically defined categories are dynamic, but this can be
// overridden with PERFETTO_DEFINE_TEST_CATEGORY_PREFIXES.
template <typename... T>
constexpr bool IsDynamicCategory(const char*) {return false;
}// Explicitly dynamic categories are always dynamic.
constexpr bool IsDynamicCategory(const ::perfetto::DynamicCategory&) {return true;
}
通过编译期计算,将分类字符串转换为分类索引:
// At compile time, turns a category name represented by a static string into an
// index into the current category registry. A build error will be generated if
// the category hasn't been registered or added to the list of allowed dynamic
// categories. See PERFETTO_DEFINE_CATEGORIES.
#define PERFETTO_GET_CATEGORY_INDEX(category) \PERFETTO_TRACK_EVENT_NAMESPACE::internal::kConstExprCategoryRegistry.Find( \category, \::PERFETTO_TRACK_EVENT_NAMESPACE::internal::IsDynamicCategory(category))
kConstExprCategoryRegistry.Find的实现如下:
// --------------------------------------------------------------------------// Trace point support// --------------------------------------------------------------------------//// (The following methods are used by the track event trace point// implementation and typically don't need to be called by other code.)// At compile time, turn a category name into an index into the registry.// Returns kInvalidCategoryIndex if the category was not found, or// kDynamicCategoryIndex if |is_dynamic| is true or a DynamicCategory was// passed in.static constexpr size_t kInvalidCategoryIndex = static_cast<size_t>(-1);static constexpr size_t kDynamicCategoryIndex = static_cast<size_t>(-2);constexpr size_t Find(const char* name, bool is_dynamic) const {return CheckIsValidCategoryIndex(FindImpl(name, is_dynamic));}constexpr size_t Find(const DynamicCategory&, bool) const {return kDynamicCategoryIndex;}constexpr bool ValidateCategories(size_t index = 0) const {return (index == category_count_)? true: IsValidCategoryName(categories_[index].name)? ValidateCategories(index + 1): false;}private:// TODO(skyostil): Make the compile-time routines nicer with C++14.constexpr size_t FindImpl(const char* name,bool is_dynamic,size_t index = 0) const {return is_dynamic ? kDynamicCategoryIndex: (index == category_count_)? kInvalidCategoryIndex: StringEq(categories_[index].name, name)? index: FindImpl(name, false, index + 1);}
可见全都是constexpr。
减少模板实例化数量:
这个技术来自这段代码的DecayEventNameType:
#define INTERNAL_TRACE_EVENT_ADD_SCOPED(category, name, ...) \PERFETTO_INTERNAL_SCOPED_TRACK_EVENT( \category, ::perfetto::internal::DecayEventNameType(name), ##__VA_ARGS__)// Convert all static strings of different length to StaticString to avoid
// unnecessary template instantiations.
inline ::perfetto::StaticString DecayEventNameType(const char* name) {return ::perfetto::StaticString{name};
}
代码解释如下:
-
函数体非常简单,仅包含一个返回语句。它创建了一个
perfetto::StaticString
类型的临时对象,并使用传入的name
参数初始化该对象。花括号{name}
是列表初始化的一种形式,在这里等同于使用传统的构造函数语法perfetto::StaticString(name)
。 -
inline
关键字表示这个函数应当被内联。内联函数的作用是告诉编译器在编译期间尽可能地将函数调用替换为函数体本身,这样可以消除函数调用的开销,但会增加编译后程序的大小。内联主要用于小型且频繁调用的函数。 -
注释说明了函数的目的:将不同长度的静态字符串转换为
StaticString
类型,以避免在模板实例化时产生不必要的开销。在模板编程中,如果模板函数或类针对每一个不同的字符串长度都实例化一个版本,将会导致代码膨胀。通过使用StaticString
类型统一接口,可以减少这种类型的模板实例化,从而减少编译后的代码量和可能的运行时开销。
这段代码通过减少模板实例化来提高程序的效率。
结语
默认情况下,Perfetto在 Chromium 中以进程内模式工作,仅记录 Chromium 进程发出的数据。在 Android 上(以及在 Linux 上,如果停用 Chromium 沙盒),跟踪可以在混合进程内+系统模式下工作,将特定于 Chrome 的跟踪事件与 Perfetto 系统事件相结合。
Perfetto给chromium开发者一个性能分析的利器。另外利用查看Trace的事件,也是分析Chromium工作原理和主要流程的利器!