對於任何技術棧,都會有一個繞不過去的坎,那就是性能優化,而對於如何進行性能優化,最重要的前提就是需要知道具體的耗時分佈,要知道耗時分佈,就得打點(時間戳),一般的性能打點都是一些散點,比較凌亂,而本文要講的 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 工具打開即可分析,非常直觀,能啟到事半功倍的效果。
關注「淘系技術」微信公眾號,一個有溫度有內容的技術社區~