Perfetto源码浅析
简介
Perfetto在需要进行性能优化,对软件进行性能分析这件事上会很有用。Perfetto在设计之初就作为Android 操作系统和 Chrome 浏览器的默认 tracing 系统
Perfetto的实现非常有趣,很多内容采用在编译期处理的方式实现,非常值得学习,下面一起简单分析一下
获取Perfetto SDK
Perfetto的仓库在git@github.com:google/perfetto.git,将仓库clone下来后切换到最新的tag,在sdk/目录下存在perfetto.cc perfetto.h两个文件,这就是Perfetto SDK的全部,一般我们直接将头文件和源码直接嵌入到项目中,你的工程只需要保证支持C++17标准即可
PERFETTO_DEFINE_CATEGORIES声明默认空间
#include <perfetto.h>
PERFETTO_DEFINE_CATEGORIES(perfetto::Category("sample_node")
.SetDescription("Events from sample_node"));
PERFETTO_TRACK_EVENT_STATIC_STORAGE();
以上的两个宏定义了一个名为perfetto的默认空间,当然这是Perfetto为了使用者方便定义的两个宏,在一些复杂一点的项目中,你也可以通过其它宏定义自己的命名空间,笔者会在后面进行介绍
空间在Perfetto中是一个比较关键的概念,你的每一个类别一定属于某一个空间中,例如这里的sample_node这个类别就属于默认的perfetto空间中,如果需要,当然可以在这个类别后面继续定义其它类别,但是值的注意的是,某一空间的所有类别都只能在同一个PERFETTO_DEFINE_CATEGORIES宏中声名
再回到这里来,这里的PERFETTO_DEFINE_CATEGORIES和PERFETTO_TRACK_EVENT_STATIC_STORAGE两个宏,前者负责声明,后者负责定义
我们先看PERFETTO_DEFINE_CATEGORIES,其展开后由两个宏组成
#define PERFETTO_DEFINE_CATEGORIES(...) \
PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE(perfetto, __VA_ARGS__); \
PERFETTO_USE_CATEGORIES_FROM_NAMESPACE(perfetto)
PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE
前者展开后如下,这里需要注意的是,展开后最外层的namespace perfetto,其名称是通过传入的perfetto决定的,这在后面自定义空间的时候会用到
namespace perfetto {
namespace perfetto_track_event {
namespace internal {
constexpr ::perfetto::Category kCategories[] = {
::perfetto::Category("sample_node")
.SetDescription("Events from sample_node")};
constexpr size_t kCategoryCount = sizeof(kCategories) / sizeof(kCategories[0]);
extern std::atomic<uint8_t> g_category_state_storage[kCategoryCount];
constexpr ::perfetto::internal::TrackEventCategoryRegistry
kConstExprCategoryRegistry(kCategoryCount, &kCategories[0], nullptr);
extern const ::perfetto::internal::TrackEventCategoryRegistry kCategoryRegistry;
static_assert(kConstExprCategoryRegistry.ValidateCategories(),
"Invalid category names found");
} // namespace internal
struct TrackEvent
: public ::perfetto::internal::TrackEventDataSource<
TrackEvent, &internal::kCategoryRegistry> {
virtual ~TrackEvent();
};
} // namespace perfetto_track_event
using perfetto_track_event::TrackEvent;
} // namespace perfetto
重点有如下
kCategories,类型为::perfetto::Category数组的全局变量,以上面为例,这里的数组长度为1,只有sample_node这一个类别kCategoryCount,表示数组kCategories的长度g_category_state_storage(只是extern,未定义),一个std::atomic数组,长度为kCategories,这很容易联想到这里的每个std::atomic和对应的::perfetto::Category有关系kConstExprCategoryRegistry,一个类型为::perfetto::internal::TrackEventCategoryRegistry的全局变量kCategoryRegistry(只是extern,未定义),一个类型为::perfetto::internal::TrackEventCategoryRegistry的全局变量- 定义为
TrackEvent类型的struct,它从::perfetto::internal::TrackEventDataSource继承,::perfetto::internal::TrackEventDataSource中存在一些静态成员函数,这在注册这个空间的时候会用到 using perfetto_track_event::TrackEvent;,这里将TrackEvent放到了perfetto空间下
总结,PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE宏会定义一些全局变量,同时导出一些全局变量,所以到这里我们就能很好的解释为什么不能多次调用PERFETTO_DEFINE_CATEGORIES的方式分散定义新类别了,因为这样会重复定义全局变量,从而导致无法通过编译
我们再来看展开后的另一个宏
PERFETTO_USE_CATEGORIES_FROM_NAMESPACE
namespace perfetto_track_event {
using ::perfetto::perfetto_track_event::TrackEvent;
namespace internal {
using ::perfetto::perfetto_track_event::internal::kCategoryRegistry;
using ::perfetto::perfetto_track_event::internal::kConstExprCategoryRegistry;
} // namespace internal
} // namespace perfetto_track_event
这里使用PERFETTO_USE_CATEGORIES_FROM_NAMESPACE宏做了一个桥街,让全局的::perfetto_track_event能看到::perfetto::perfetto_track_event里的东西,这很重要,因为TRACE_EVENT在采集事件时,默认使用全局的::perfetto_track_event中的东西,这里将::perfetto::perfetto_track_event::*中的各个全局变量桥接到全局的::perfetto_track_event下,那么TRACE_EVENT在上报事件时,含义就变成了默认使用::perfetto::perfetto_track_event::*中的全局变量了,如果在perfetto空间中找不到对应的类别,那么会出现编译期报错
PERFETTO_TRACK_EVENT_STATIC_STORAGE定义默认空间
这个宏就是将PERFETTO_DEFINE_CATEGORIES中,只声明未定义的部分全局变量进行定义,仅此而已
通常我们会将这个宏放到某一个cpp的源文件中
TRACE_EVENT埋点采集信息
TRACE_EVENT("sample_node", "RecordServer::RecordServer");
在代码中,最多的是按照这样的方式记录和采集事件。sample_node表示事件所属的类别,这需要在PERFETTO_DEFINE_CATEGORIES定义的空间下声明该类别,否则会出现编译期报错;后面紧跟的是事件名称,上报事件时还能携带一些参数
我们在看TRACE_EVENT内部做了什么
TRACE_EVENT
PERFETTO_INTERNAL_SCOPED_TRACK_EVENT
PERFETTO_INTERNAL_SCOPED_EVENT_FINALIZER
TRACE_EVENT_BEGIN
这大致是TRACE_EVENT的调用链,下面我们挑重要的介绍一下
PERFETTO_INTERNAL_SCOPED_TRACK_EVENT
这里比较有意思的是PERFETTO_INTERNAL_SCOPED_TRACK_EVENT宏,它的定义如下
#define PERFETTO_INTERNAL_SCOPED_TRACK_EVENT(category, name, ...) \
PERFETTO_INTERNAL_SCOPED_EVENT_FINALIZER(category) \
PERFETTO_UID(scoped_event) { \
[&]() { \
TRACE_EVENT_BEGIN(category, name, ##__VA_ARGS__); \
return 0; \
}() \
}
// PERFETTO_INTERNAL_SCOPED_EVENT_FINALIZER 宏的定义如下
#define PERFETTO_INTERNAL_SCOPED_EVENT_FINALIZER(category) \
struct PERFETTO_UID(ScopedEvent) { \
struct EventFinalizer { \
EventFinalizer(...) {} \
~EventFinalizer() { \
TRACE_EVENT_END(category); \
} \
\
EventFinalizer(const EventFinalizer&) = delete; \
inline EventFinalizer& operator=(const EventFinalizer&) = delete; \
\
EventFinalizer(EventFinalizer&&) = \
PERFETTO_INTERNAL_EVENT_FINALIZER_KEYWORD; \
EventFinalizer& operator=(EventFinalizer&&) = delete; \
} finalizer; \
}
我们知道,在用Perfetto UI查看采集的信息时,每个通过TRACE_EVENT采集到的事件都包括调用栈所占用的详细时间,这里时间的统计就是PERFETTO_INTERNAL_SCOPED_EVENT_FINALIZER中通过RAII的方式实现的
这里的PERFETTO_UID宏作用是将输入和行号进行拼接,获得一个新的名称,比如这里的struct PERFETTO_UID(ScopedEvent)如果是在126行,那么宏展开后将会得到struct ScopedEvent126
然后在声明的struct PERFETTO_UID(ScopedEvent)内部再声明一个内部类struct EventFinalizer,同时定义一个成员变量finalizer,所以PERFETTO_INTERNAL_SCOPED_TRACK_EVENT宏完全展开后如下
struct ScopedEvent100 {
struct EventFinalizer {
EventFinalizer(...) {}
~EventFinalizer() {
TRACE_EVENT_END("sample_node");
}
EventFinalizer(const EventFinalizer&) = delete;
EventFinalizer& operator=(const EventFinalizer&) = delete;
EventFinalizer(EventFinalizer&&) = delete_or_default;
EventFinalizer& operator=(EventFinalizer&&) = delete;
} finalizer;
} scoped_event100 {
[&]() {
TRACE_EVENT_BEGIN("sample_node", "foo");
return 0;
}()
};
总结下来就是,在构造scoped_event100时(100是行数,这样可以避免在一个函数中多次采集时报变量名重复),会首先调用传入的lambda,这个lambda的返回值会作为输入初始化成员变量finalizer,不过我们看到EventFinalizer的构造函函数入参是可变参数,但是它什么也不做,就是会忽略lambda的返回值,所以这里lambda的作用就是为了调用TRACE_EVENT_BEGIN
scoped_event100离开作用域析构时,同样会调用成员变量finalizer的析构函数,finalizer析构时则调用TRACE_EVENT_END,这样一来就采集到了栈帧的一些运行信息了,很巧妙的方式
所以,真正的采集是从TRACE_EVENT_BEGIN开始的,我们看看怎么会事
TRACE_EVENT_BEGIN
这个宏定最终调用PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD
#define PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD(method, category, name, ...) \
do { \
::perfetto::internal::ValidateEventNameType<decltype(name)>(); \
namespace tns = PERFETTO_TRACK_EVENT_NAMESPACE; \
/* Compute the category index outside the lambda to work around a */ \
/* GCC 7 bug */ \
PERFETTO_INTERNAL_STATIC_FOR_MSVC constexpr auto PERFETTO_UID( \
kCatIndex_ADD_TO_PERFETTO_DEFINE_CATEGORIES_IF_FAILS_) = \
PERFETTO_GET_CATEGORY_INDEX(category); \
if (::PERFETTO_TRACK_EVENT_NAMESPACE::internal::IsDynamicCategory( \
category)) { \
tns::TrackEvent::CallIfEnabled( \
[&](uint32_t instances) PERFETTO_NO_THREAD_SAFETY_ANALYSIS { \
tns::TrackEvent::method( \
instances, category, \
::perfetto::internal::DecayEventNameType(name), \
##__VA_ARGS__); \
}); \
} else { \
tns::TrackEvent::CallIfCategoryEnabled( \
PERFETTO_UID(kCatIndex_ADD_TO_PERFETTO_DEFINE_CATEGORIES_IF_FAILS_), \
[&](uint32_t instances) PERFETTO_NO_THREAD_SAFETY_ANALYSIS { \
tns::TrackEvent::method( \
instances, \
PERFETTO_UID( \
kCatIndex_ADD_TO_PERFETTO_DEFINE_CATEGORIES_IF_FAILS_), \
::perfetto::internal::DecayEventNameType(name), \
##__VA_ARGS__); \
}); \
} \
} while (false)
首先是PERFETTO_GET_CATEGORY_INDEX(category),即在编译期查找对应空间中是否有这个类别,如果找不到那么会在编译期报错
#define PERFETTO_GET_CATEGORY_INDEX(category) \
PERFETTO_TRACK_EVENT_NAMESPACE::internal::kConstExprCategoryRegistry.Find( \
category, \
::PERFETTO_TRACK_EVENT_NAMESPACE::internal::IsDynamicCategory(category))
使用PERFETTO_TRACK_EVENT_NAMESPACE::internal::kConstExprCategoryRegistry的FInd方法进行查找,那么这个kConstExprCategoryRegistry在哪儿呢?
其实就在最开始通过PERFETTO_DEFINE_CATEGORIES定义默认空间时,做了一个桥接using ::perfetto::perfetto_track_event::internal::kConstExprCategoryRegistry;,即将::perfetto::perfetto_track_event::internal::下的kConstExprCategoryRegistry桥接到了::perfetto_track_event::internal::空间下,而这里PERFETTO_TRACK_EVENT_NAMESPACE默认值就是perfetto_track_event
所以最后就是在::perfetto::perfetto_track_event::internal::kConstExprCategoryRegistry;中查找类别是否存在
IsDynamicCategory是一个模板函数,如果传入的是char*,那么表示这个Category是静态注册到空间中的;如果是一个::perfetto::DynamicCategory,那么表示这个Category是动态注册到空间中的,对于前者会进行静态查找,如果未找到会报错,后者则不会进行静态查找
后面就是两个分支,区分category是静态注册还是动态注册
静态注册的Category
静态注册时tns::TrackEvent::CallIfCategoryEnabled,这里同样是通过桥接默认调用到::perfetto::perfetto_track_event::TrackEvent::CallIfCategoryEnabled,CallIfCategoryEnabled会找到类别对应的std::atomic<uint8_t>,如果被置为1,那么表示需要采集,反之则不会通过回调去调用tns::TrackEvent::method进行采集
注意这里查找对应的std::atomic<uint8_t>所在的index都是编译期就找到了的,在运行期则直接读取对应的std::atomic<uint8_t>值判断是否需要进行采集,所以性能非常好
动态注册的Category
对于动态注册的Category,因为不可以在编译期找到对应的std::atomic<uint8_t>,所以不会在编译期进行check。它只会通过CallIfEnabled检查当前是否有空间被启用,如果没有则不会采集事件
定义不同空间
在此之前我们在默认空间perfetto下注册了一些类别,但是在多模块复杂工程中,如果只使用默认空间的话,会导致每个组件在定义自己的新类别时,都去改动同一个文件的同一个部分
可以通过PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE和PERFETTO_TRACK_EVENT_STATIC_STORAGE_IN_NAMESPACE两个宏自定义空间
前者只会在新的空间下声明全局变量,不会去做桥接,这样就不会影响默认行为;而后者和PERFETTO_TRACK_EVENT_STATIC_STORAGE不同点在与空间名是可以自定义的,仅此而已
借助这两个宏,在多模块下,每个模块可以自定义自己的空间,然后在自己的空间下注册自己的新类别,彼此之间解耦
那么还剩下一个问题,那就是TRACE_EVENT是从默认的perfetto空间下检查传入的类别的,那么自定义空间时要怎么让TRACE_EVENT去自定义的空间下查找类别呢?
#define MY_TRACE_EVENT(ns, category, name, ...) \
do { \
PERFETTO_USE_CATEGORIES_FROM_NAMESPACE_SCOPED(ns); \
TRACE_EVENT(category, name, ##__VA_ARGS__); \
} while (0)
可以像这样将TRACE_EVENT封装一下,这里的ns就是我们自定义的空间名称了,下面解释一下原理,其实很简单,我们看下PERFETTO_USE_CATEGORIES_FROM_NAMESPACE_SCOPED宏的定义
#define PERFETTO_USE_CATEGORIES_FROM_NAMESPACE_SCOPED(ns) \
namespace PERFETTO_TRACK_EVENT_NAMESPACE = ns::PERFETTO_TRACK_EVENT_NAMESPACE
一目了然了,原来就是在do while中,将PERFETTO_TRACK_EVENT_NAMESPACE桥接到我们自定义的空间下,这样一来,TRACE_EVENT就会去我们自定义的空间下查找类别了,很有意思
总结
本文详细介绍了Perfetto SDK中TRACE_EVENT宏的实现机制。通过PERFETTO_DEFINE_CATEGORIES声明默认命名空间,PERFETTO_TRACK_EVENT_STATIC_STORAGE定义静态存储,最终TRACE_EVENT宏利用PERFETTO_INTERNAL_SCOPED_TRACK_EVENT在作用域内自动记录事件。文章还展示了如何定义不同命名空间以及静态与动态注册Category的区别,为性能追踪提供了完整的实现参考
更多推荐


所有评论(0)