对于任何技术栈,都会有一个绕不过去的坎,那就是性能优化,而对于如何进行性能优化,最重要的前提就是需要知道具体的耗时分布,要知道耗时分布,就得打点(时间戳),一般的性能打点都是一些散点,比较凌乱,而本文要讲的 Tracing 则是性能打点的一种非常优雅的实现,它以瀑布流的形式呈现,非常直观,还有一个更直观的名字叫 火焰图
Tracing 顾名思义 —— 追踪每段耗时分布。
背景
上面这张图是 Flutter Engine 初始化过程中的一部分流程,非常直观的反应了执行流程中每个阶段的耗时分布。
Tracing 是 Chrome 开发者工具中强大的性能分析工具之一,它能记录 Chrome 所有进程间的各种活动。例如能记录每个进程中每个线程里 C++ 或者 JavaScript 方法的调用栈/耗时,不仅仅如此,还能看到视图 Layer 之间的层级关系,相关文档介绍 The Trace Event Profiling Tool (about:tracing)。
本文会专注在 Flutter Engine 中 Tracing 原理与实践,会分为原理篇与实践篇,原理篇会涉及到具体实现,实践篇主要包括如何使用、分析、定制。
⚠️:Flutter 中用 Timeline 这个词代替了 Tracing,Flutter Devtool 也提供了 Timeline 工具(展示的就是 Tracing 结构的信息)。这两个词是一个对等的概念,下文提到的 Timeline 可以和 Tracing 对等。
原理篇
整个 Timeline 的过程主要包括初始化 Timeline 与记录 Tracing 信息两个部分。
▐ 初始化 Timeline
初始化 Timeline 包括四个过程:注册 Flag、设置 Flag、TimelineStream 初始化、Timeline 初始化。
注册 Flag
Flutter 中会注册非常多的 Flag 用于各种功能标记,对于 Timeline/Tracing 功能就是 timeline_streams 标实,具体如下:
/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc
// 执行宏定义
DEFINE_FLAG(charp,
timeline_streams,
NULL,
"Comma separated list of timeline streams to record. "
"Valid values: all, API, Compiler, CompilerVerbose, Dart, "
"Debugger, Embedder, GC, Isolate, and VM.");
// 展开后:
charp FLAG_timeline_streams = Flags::Register_charp(&FLAG_timeline_streams, 'timeline_streams', NULL, "Comma separated list of timeline streams to record. "
"Valid values: all, API, Compiler, CompilerVerbose, Dart, "
"Debugger, Embedder, GC, Isolate, and VM.");
其中 charp 为 typedef const char* charp;
真正执行的函数如下:
/path/to/engine/src/third_party/dart/runtime/vm/flags.cc
const char* Flags::Register_charp(charp* addr,
const char* name,
const char* default_value,
const char* comment) {
ASSERT(Lookup(name) == NULL);
Flag* flag = new Flag(name, comment, addr, Flag::kString);
AddFlag(flag);
return default_value;
}
其中 addr_ 是一个 union 成员,初始值为当前注册函数的默认值为 NULL,即 FLAG_timeline_streams 初始值为 NULL。
注册 Flag 的过程就是定义了 FLAG_timeline_streams 标记。
设置 Flag
在 Flutter Engine 初始化的过程中,可以进行 DartVm 参数的透传,例如 —trace-startup,这个参数就可以记录启动时 Tracing 信息,会由如下方法进行设置:
path/to/engine/src/flutter/runtime/dart_vm.cc
char* flags_error = Dart_SetVMFlags(args.size(), args.data());
最终调用方法:
/path/to/engine/src/third_party/dart/runtime/vm/flags.cc
char* Flags::ProcessCommandLineFlags(int number_of_vm_flags,
const char** vm_flags) {
...
while ((i < number_of_vm_flags) &&
IsValidFlag(vm_flags[i], kPrefix, kPrefixLen)) {
const char* option = vm_flags[i] + kPrefixLen;
Parse(option);
i++;
}
...
}
这里主要会进行 Flag 的有效性验证,关键步骤为 Parse 方法中的 SetFlagFromString
bool Flags::SetFlagFromString(Flag* flag, const char* argument) {
ASSERT(!flag->IsUnrecognized());
switch (flag->type_) {
...
case Flag::kString: {
*flag->charp_ptr_ = argument == NULL ? NULL : strdup(argument);
break;
}
....
}
flag->changed_ = true;
return true;
}
会针对不同 Flag Type 设置不同变量,而这些变量是一个 union 结构体,如下:
union {
void* addr_;
bool* bool_ptr_;
int* int_ptr_;
uint64_t* uint64_ptr_;
charp* charp_ptr_;
FlagHandler flag_handler_;
OptionHandler option_handler_;
}
根据 union 的特性,针对不同的 Flag Type,会得到不同值类型,可见之前定义的 FLAG_timeline_streams 值最终就会设置成透传的值。例如 —trace_startup 对应的值为 Compiler,Dart,Debugger,Embedder,GC,Isolate,VM。
设置 Flag 的过程就是具体设置了之前定义的 FLAG_timeline_streams 值。
TimelineStream 初始化
在 FLAG_timeline_streams 中非常多的类型值,每种都定义了不同的 Stream,初始化过程包括三个步骤:Declare Stream(申明)、Get Stream(获取)、Define Stream(定义)。
✎ Declare Stream
/path/to/engine/src/third_party/dart/runtime/vm/timeline.h
// stream 申明
#define TIMELINE_STREAM_DECLARE(name, fuchsia_name) \
static TimelineStream stream_##name##_;
TIMELINE_STREAM_LIST(TIMELINE_STREAM_DECLARE)
#undef TIMELINE_STREAM_DECLARE
// 展开后
static TimelineStream stream_API_;
static TimelineStream stream_Compiler_;
static TimelineStream stream_Dart_;
static TimelineStream stream_Embedder_;
....
Flutter Engine 中的 Timeline 信息为 stream_Embedder_,其它的 Timeline 也包括 Dart 层、API 层等等,本文主要会关注在 stream_Embedder_。
✎ Get Stream
/path/to/engine/src/third_party/dart/runtime/vm/timeline.h
// 获取 Stream
#define TIMELINE_STREAM_ACCESSOR(name, fuchsia_name) \
static TimelineStream* Get##name##Stream() { return &stream_##name##_; }
TIMELINE_STREAM_LIST(TIMELINE_STREAM_ACCESSOR)
#undef TIMELINE_STREAM_ACCESSOR
// 展开后
static TimelineStream* GetAPIStream() { return &stream_API_; }
static TimelineStream* GetDartStream() { return &stream_Dart_; }
static TimelineStream* GetEmbedderStream() { return &stream_Embedder_; }
...
设置了相应的静态获取方法。
Define Stream
/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc
#define TIMELINE_STREAM_DEFINE(name, fuchsia_name) \
TimelineStream Timeline::stream_##name##_(#name, fuchsia_name, false);
TIMELINE_STREAM_LIST(TIMELINE_STREAM_DEFINE)
#undef TIMELINE_STREAM_DEFINE
// 展开后
TimelineStream Timeline::stream_API_("API", "dart:api", false);
TimelineStream Timeline::stream_Dart_("Dart", "dart:dart", false);
TimelineStream Timeline::stream_Embedder_("Embedder", "dart:embedder", false);
...
Timeline 初始化
void Timeline::Init() {
ASSERT(recorder_ == NULL);
recorder_ = CreateTimelineRecorder();
ASSERT(recorder_ != NULL);
enabled_streams_ = GetEnabledByDefaultTimelineStreams();
// Global overrides.
#define TIMELINE_STREAM_FLAG_DEFAULT(name, fuchsia_name) \
stream_##name##_.set_enabled(HasStream(enabled_streams_, #name));
TIMELINE_STREAM_LIST(TIMELINE_STREAM_FLAG_DEFAULT)
#undef TIMELINE_STREAM_FLAG_DEFAULT
}
1、通过 CreateTimelineRecorder 创建 TimelineEventRecorder,如果需要获取启动 Tracing 信息会创建 TimelineEventEndlessRecorder,会记录无上限的 Trace 信息。
2、设置刚才创建的一系列 TimelineStream 实例的 set_enable 函数,后续在进行 Timeline 记录的时候都会查询是否 enable。
▐ 记录 Timeline 信息
上一部分主要讲了 Timeline 初始化准备的各种信息变量,这部分主要会讲记录 Tracing 信息的过程。
记录 Tracing 信息有非常多的调用方法,包括记录同步事件(TRACE_EVENT)、异步事件(TRACE_EVENT_ASYNC)、事件流(TRACE_FLOW_)。以下讲同步事件的调用过程,其他事件整个流程基本类似。
同步事件包括 TRACE_EVENT0 、TRACE_EVENT1、TRACE_EVENT2 等,以 TRACE_EVENT0 调用为例:
{
TRACE_EVENT0("flutter", "Shell::CreateWithSnapshots");
}
// 展开后
::fml::tracing::TraceEvent0("flutter", "Shell::CreateWithSnapshots");
::fml::tracing::ScopedInstantEnd __trace_end___LINE__("Shell::CreateWithSnapshots");
主要包括两个部分:
- 记录阶段 TraceEvent0,记录当前信息
- 标记结束 ScopedInstantEnd ,一般在作用域析构时调用
TraceEvent0
TraceEvent0 最终会调用如下方法:
path/to/engine/src/third_party/dart/runtime/vm/dart_api_impl.cc
DART_EXPORT void Dart_TimelineEvent(const char* label,
int64_t timestamp0,
int64_t timestamp1_or_async_id,
Dart_Timeline_Event_Type type,
intptr_t argument_count,
const char** argument_names,
const char** argument_values) {
...
TimelineStream* stream = Timeline::GetEmbedderStream();
ASSERT(stream != NULL);
TimelineEvent* event = stream->StartEvent();
...
switch (type) {
case Dart_Timeline_Event_Begin:
event->Begin(label, timestamp0);
break;
case Dart_Timeline_Event_End:
event->End(label, timestamp0);
break;
...
}
...
event->Complete();
}
整个过程主要包括四个阶段:
- TimelineStream::StartEvent:生成 TimelineEvent,其中Timeline::GetEmbedderStream() 即为初始化阶段的 stream_Embedder_。
- TimelineEvent::Begin/End:记录起始、结束的时间等信息
- TimelineEvent::Complete:完成当前记录
- TimelineEventBlock::Finish:上报记录的信息
✎ TimelineStream::StartEvent
stream->StartEvent() 最终会调用如下方法产生 TimelineEvent:
/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc
TimelineEvent* TimelineEventRecorder::ThreadBlockStartEvent() {
// Grab the current thread.
OSThread* thread = OSThread::Current();
ASSERT(thread != NULL);
Mutex* thread_block_lock = thread->timeline_block_lock();
...
thread_block_lock->Lock(); // 会一直持有,直到调用 CompleteEvent()
...
TimelineEventBlock* thread_block = thread->timeline_block();
if ((thread_block != NULL) && thread_block->IsFull()) {
MutexLocker ml(&lock_);
// Thread has a block and it is full:
// 1) Mark it as finished.
thread_block->Finish();
// 2) Allocate a new block.
thread_block = GetNewBlockLocked();
thread->set_timeline_block(thread_block);
} else if (thread_block == NULL) {
MutexLocker ml(&lock_);
// Thread has no block. Attempt to allocate one.
thread_block = GetNewBlockLocked();
thread->set_timeline_block(thread_block);
}
if (thread_block != NULL) {
// NOTE: We are exiting this function with the thread's block lock held.
ASSERT(!thread_block->IsFull());
TimelineEvent* event = thread_block->StartEvent();
return event;
}
....
thread_block_lock->Unlock();
return NULL;
}
1、首先会调用线程锁,一直持有本次记录过程,直到调用 CompleteEvent()。
2、如果没有 TimelineEventBlock ,则首先会创建一个,并记录在当前线程中。
3、如果 TimelineEventBlock 满了,会先 Finish (见下文分析),再创建一个新的,并记录。
4、最后都会在 TimelineEventBlock 中创建一个新的 TimelineEvent,每个 TimelineEventBlock 创建的 TimelineEvent 会有数量限制,最多为 64 个。
⚠️:如果为 TimelineEventEndlessRecorder,则会无限创建 TimelineEventBlock,否则会有数量限制。
✎ TimelineEvent::Begin/End
/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc
void TimelineEvent::Begin(const char* label,
int64_t micros,
int64_t thread_micros) {
Init(kBegin, label);
set_timestamp0(micros);
set_thread_timestamp0(thread_micros);
}
这些阶段主要是记录具体的信息,包括:
1、Init: 记录事件标签名,事情类型(kBegin,kEnd),End 一般会在作用域析构时调用(下面会分析)。
2、micros: 记录系统启动后运行的时间戳。
3、thread_micros: 记录该线程CPU运行的时间戳。
✎ TimelineEvent::Complete
最终调用方法如下:
/path/to/engine/src/third_party/dart/runtime/vm/timeline.cc
void TimelineEventRecorder::ThreadBlockCompleteEvent(TimelineEvent* event) {
...
// Grab the current thread.
OSThread* thread = OSThread::Current();
ASSERT(thread != NULL);
// Unlock the thread's block lock.
Mutex* thread_block_lock = thread->timeline_block_lock();
...
thread_block_lock->Unlock();
}
一次记录结束后会调用 Complete 方法,并最终会释放一开始 Lock 的同步锁。
✎ TimelineEventBlock::Finish
在 TimelineStream::StartEvent 中创建的TimelineEventBlock 提到,默认最多是 64 个,满了之后会调用 Finsih 方法。
void TimelineEventBlock::Finish() {
...
in_use_ = false;
#ifndef PRODUCT
if (Service::timeline_stream.enabled()) {
ServiceEvent service_event(NULL, ServiceEvent::kTimelineEvents);
service_event.set_timeline_event_block(this);
Service::HandleEvent(&service_event);
}
#endif
}
最终会将事件信息发送给 ServiceIsolate 来处理,关于 ServiceIsolate 简单可以理解为后端服务,是由 Dart VM 初始化的时候创建的, DevTool 显示的信息(包括 Tracing 信息)都会和 ServiceIsolate 通信获取。
ScopedInstantEnd
class ScopedInstantEnd {
public:
ScopedInstantEnd(const char* str) : label_(str) {}
~ScopedInstantEnd() { TraceEventEnd(label_); }
private:
const char* label_;
FML_DISALLOW_COPY_AND_ASSIGN(ScopedInstantEnd);
};
可以看到析构函数中会调用 TraceEventEnd 方法,也就是说离开了作用域就会调用 TraceEventEnd 方法,而 TraceEventEnd 方法最终调用的就是 TimelineEvent::End 阶段进行信息记录。
以上就是整体的 Tracing 信息的路由过程,实现上使用了大量的宏,宏在开发阶段还是方便实现,不过对于阅读源码来说会有一定的障碍,不能直观的进行代码搜索查找。
实践篇
主要介绍 Timeline 的使用、启动性能分析、有用的 Debug 参数介绍、以及添加自定义 Tracing 节点。
▐ Timeline 使用
Timeline 的使用在官方文档中已经有详细的说明,Using the Timeline view - Flutter 直接看文档即可。
▐ 启动性能分析
Timeline 工具仅仅只能分析 Flutter 页面启动之后的运行时情况,整个 Flutter 的启动过程完全是无法分析的,而启动/初始化过程也是比较关键的一环。
对于启动性能分析,官方文档描述甚少,目前只发现了这一处,Measuring app startup time - Flutter。
启动性能分析包括三个步骤:添加启动性能参数、获取 Tracing 信息、分析。
添加启动参数
只有添加了特定的参数后才能获取启动时 Tracing 信息。
✎ Flutter App 场景
flutter run --trace-startup --profile
主要是通过 flutter cli 命令行参数运行 Flutter App,最终会在当前目录下生成 build/start_up_info.json 文件。
可惜的是这个文件只产出了四个关键的 Timestamp,远远达不到能够分析的地步,跟进 Flutter Tools 源码后,关键源码如下:
path/to/flutter/packages/flutter_tools/lib/src/tracing.dart
/// Download the startup trace information from the given observatory client and
/// store it to build/start_up_info.json.
Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async {
final Tracing tracing = Tracing(observatory);
final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
awaitFirstFrame: awaitFirstFrame,
);
......
final Map<String, dynamic> traceInfo = <String, dynamic>{
'engineEnterTimestampMicros': engineEnterTimestampMicros,
};
......
traceInfo['timeToFrameworkInitMicros'] = timeToFrameworkInitMicros;
......
traceInfo['timeToFirstFrameRasterizedMicros'] = firstFrameRasterizedTimestampMicros - engineEnterTimestampMicros;
......
traceInfo['timeToFirstFrameMicros'] = timeToFirstFrameMicros;
......
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameBuiltTimestampMicros - frameworkInitTimestampMicros;
......
traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
}
可以看到关键的四个 Timestamp 被保存在 Map 进行输出到文件,最关键的一点是整个 timeline 数据其实都已经拿到了,于是可以进行如下改造:
/// Download the startup trace information from the given observatory client and
/// store it to build/start_up_info.json.
Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async {
final Tracing tracing = Tracing(observatory);
final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
awaitFirstFrame: awaitFirstFrame,
);
......
// 原来的 start_up_info.json 生成
traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
......
// 新增 start_up_trace_events.json 生成
final String traceEventsFilePath = globals.fs.path.join(getBuildDirectory(), 'start_up_trace_events.json');
final File traceEventsFile = globals.fs.file(traceEventsFilePath);
final List<Map<String, dynamic>> events =
List<Map<String, dynamic>>.from((timeline['traceEvents'] as List<dynamic>).cast<Map<String, dynamic>>());
traceEventsFile.writeAsStringSync(toPrettyJson(events));
}
改造后会在当前目录下生成 build/start_up_trace_events.json 文件,并通过 chrome://tracing 打开查看。有一个注意点,在改动 flutter tools 代码后,需要重新生成 flutter command ,具体可以看文档。The flutter tool · flutter/flutter Wiki · GitHub
上面这个场景对于整个 Flutter App 来讲是完全可以进行启动性能分析了,但是对于 Add to App 的场景还是无法满足,因为这种场景无法通过 flutter cli 来进行参数透传。
**✎ Add To App 场景
**
对于这种场景,需要通过 Platform 层去透传参数。
Android
Android 侧参数透传方法如下:
path/to/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
public FlutterEngine(
@NonNull Context context,
@NonNull FlutterLoader flutterLoader,
@NonNull FlutterJNI flutterJNI,
@NonNull PlatformViewsController platformViewsController,
@Nullable String[] dartVmArgs,
boolean automaticallyRegisterPlugins) {
......
}
通过实例化 FlutterEngine 时的构造参数 dartVmArgs 中添加 --trace-startup 即可。
new FlutterEngine(mPlatform.getApplication().getApplicationContext(),
FlutterLoader.getInstance(),new FlutterJNI(),new String[]{"--trace-startup"},true);
iOS
iOS 侧通过源码查看,对应的 FlutterEngine.mm 的构造参数中是没有对应的 dartVmArgs 参数透传。真正参数转换的地方如下:
path/to/engine/src/flutter/shell/platform/darwin/common/command_line.mm
fml::CommandLine CommandLineFromNSProcessInfo() {
std::vector<std::string> args_vector;
for (NSString* arg in [NSProcessInfo processInfo].arguments) {
args_vector.emplace_back(arg.UTF8String);
}
return fml::CommandLineFromIterators(args_vector.begin(), args_vector.end());
}
通过 [NSProcessInfo processInfo].arguments 拿的命令行参数,无法通过自定义加入参数实现,对于从 XCode 启动 App 的可以通过编辑 schema 添加参数实现,示例如下:
但是绝大多数情况下,不会通过 XCode 来启动 App,因此还是需要通过修改 Engine 代码来实现参数传递。对此提了 PR 来支持 dartVm 参数的透传。
path/to/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm
- (instancetype)initWithDartVmArgs:(nullable NSArray<NSString*>*)args {
return [self initWithPrecompiledDartBundle:nil dartVmArgs:args];
}
初始化 FlutterEngine.mm 中可以通过如下方式初始化:
_dartProject = [[FlutterDartProject alloc] initWithPrecompiledDartBundle:dartBundle dartVmArgs:@[@"--trace-startup"]];
_engine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:_dartProject allowHeadlessExecution:YES];
Android Systrace
对于 Android 设备来讲,还可以用 Android 独有的 Systrace 来看,不需要改任何 Flutter 相关的参数。
相关参考文档:
Understanding Systrace | Android Open Source Project
Overview of system tracing | Android Developers
获取 Tracing 文件
添加了启动参数之后,需要有工具进行查看,Flutter 默认提供的 DevTool 默认就能进行查看,按如下步骤:
拿到启动后的 Observatory 地址。
通过 flutter attach --debug-uri=observatory_url attach 到对应的服务,会生成一个 debugger/profiler 地址。
打开 debugger/profiler 地址后就是 Fluuter 默认的 DevTool 工具,点击 timeline 按钮即可打开 Tracing 内容。
分析 Tracing 文件
关于 Tracing 工具的使用可以查看相关 Chrome 文档, The Trace Event Profiling Tool (about:tracing)。
展示的信息比较直观,对于启动性能分析,能非常直观的看到各个部分的耗时情况,下图是 Flutter 启动时 iOS 上的各个耗时阶段的大致分布,图的左边,可以看到各个阶段执行对应的线程。
▐ Debug 参数
上面介绍了如何获取 Tracing 的方法,生成的 Tracing 耗时分布主要包括各个阶段的耗时,但是还并不是包含所有的阶段,介绍两个有用的 Debug 参数,其他相关参数参考文档 [Debug flags: performance - Flutter
](链接地址https://flutter.dev/docs/testing/code-debugging?spm=ata.13261165.0.0.32ca24d41mrBFF#debug-flags-performance)
debugProfilePaintsEnabled
path/to/flutter/packages/flutter/lib/src/rendering/debug.dart
bool debugProfilePaintsEnabled = false;
这个参数会在渲染 Paint 阶段,显示所有 Paint 时节点的遍历情况,可以根据这些信息查看是否有无用的节点 Paint
debugProfileBuildsEnabled
path/to/flutter/packages/flutter/lib/src/widgets/debug.dart
bool debugProfileBuildsEnabled = false;
这个参数会在 Widget Build 阶段,显示所有 Widget 节点 Build 时的遍历情况,可以根据这些信息查看是否有无用的节点 Build。
上图把 build、paint 阶段的过程全都显示出来了,有了这些信息后,还需要结合自身的业务逻辑分析 Widget Build/Paint 是否合理,是否执行了无用的操作,然后进行优化。
自定义 Tracing 节点
对于默认没有打点的地方,如果自己需要查看其耗时,则可以自行进行打点。例如需要查看创建 IOSContext 的耗时,则可以进行如下打点:
std::unique_ptr<IOSContext> IOSContext::Create(IOSRenderingAPI rendering_api) {
TRACE_EVENT0("flutter", "IOSContext::Create");
......
FML_CHECK(false);
return nullptr;
}
最终会反应在 Tracing 上,如下图:
后记
本文主要分析了 Tracing 在 Flutter 上的实现以及一些实践,Tracing 是 Chrome 实现的一种标准格式,任何技术栈的性能分析都可以生成这种标准格式,然后利用现成的 Chrome DevTool 工具打开即可分析,非常直观,能启到事半功倍的效果。
关注「淘系技术」微信公众号,一个有温度有内容的技术社区~