btrace 3.0 重磅新增 iOS 支持!免插樁原理大揭秘!
重磅更新
btrace 是由字節(jié)跳動(dòng)抖音基礎(chǔ)技術(shù)團(tuán)隊(duì)自主研發(fā)的面向移動(dòng)端的性能數(shù)據(jù)采集工具,它能夠高效的助力移動(dòng)端應(yīng)用采集性能 Trace 數(shù)據(jù),深入剖析代碼的運(yùn)行狀況,進(jìn)而輔助優(yōu)化提升移動(dòng)端應(yīng)用的性能體驗(yàn),提升業(yè)務(wù)價(jià)值。此次更新,我們重磅推出 btrace 3.0 版本,提出了業(yè)界首創(chuàng)的同步抓棧的 Trace 采集方案,實(shí)現(xiàn)了高性能的 Trace 采集。此外,新版本在支持 Android 系統(tǒng)的基礎(chǔ)上,新增了對(duì) iOS 系統(tǒng)的全面支持。
歡迎訪問(wèn) https://github.com/bytedance/btrace 進(jìn)一步了解。 此外,后文我們還對(duì) btrace 3.0 誕生的背景、建設(shè)思路和實(shí)現(xiàn)細(xì)節(jié)進(jìn)行了深入介紹,歡迎進(jìn)一步閱讀。
背景說(shuō)明
自 btrace 2.0 正式發(fā)布以來(lái),已近兩年時(shí)間。在此期間,我們收到了大量用戶反饋,經(jīng)總結(jié),主要問(wèn)題如下:
1. 接入維護(hù)成本較高:接入、使用及維護(hù)的成本均偏高,對(duì)用戶的使用體驗(yàn)產(chǎn)生了影響。接入的插件配置較為復(fù)雜,編譯期插樁致使構(gòu)建耗時(shí)增加,接入后字節(jié)碼插樁異常會(huì)導(dǎo)致無(wú)法正常編譯,且問(wèn)題原因難以排查。
2. 系統(tǒng)方法信息缺失:編譯期的字節(jié)碼插樁方案僅能對(duì)打包至 apk 內(nèi)的方法生效,無(wú)法對(duì) android framework 等系統(tǒng)方法生效,導(dǎo)致所采集的 Trace 信息不夠豐富,影響后續(xù)的性能分析。
除 android 端團(tuán)隊(duì)針對(duì) 2.0 版本給出的反饋外,隨著行業(yè)內(nèi)雙端合作愈發(fā)緊密,業(yè)界對(duì)于 iOS 相關(guān)能力的需求也十分迫切。然而,蘋果官方所提供的 Trace 方案 Time profiler 存在以下局限:
1. 使用成本高:Time profiler 的界面比較復(fù)雜,配套的說(shuō)明文檔數(shù)量較少,使用 Time profiler 定位問(wèn)題時(shí)需要消耗很多精力。
2. 應(yīng)用靈活性低:Time profiler 工具是一個(gè)黑盒,出現(xiàn)問(wèn)題時(shí)無(wú)法排查,且無(wú)法自定義數(shù)據(jù)維度和數(shù)據(jù)展示方式。
對(duì)此,為了持續(xù)提升用戶使用體驗(yàn)、增強(qiáng) Trace 信息豐富度、進(jìn)一步降低性能損耗,以及對(duì) iOS 端能力的支持,我們開(kāi)啟了全新的 Trace 方案的探索。
思路介紹
實(shí)際上不難看出,btrace 目前存在的問(wèn)題主要是由其使用的編譯期字節(jié)碼插樁方案引起的,因此為了解決目前存在的問(wèn)題,我們重點(diǎn)探索了編譯期插樁之外的 Trace 采集方案。
目前業(yè)界主流的 Trace 采集方案分為兩種:代碼插樁方案和采樣抓棧方案。那么采樣抓棧方案是否就是更合適的方案呢,我們對(duì)兩種方案的優(yōu)缺點(diǎn)做了簡(jiǎn)單的總結(jié):
從上述對(duì)比可以看出,兩種方案各有優(yōu)劣。采樣抓棧方案可以追蹤到系統(tǒng)方法的執(zhí)行、可以動(dòng)態(tài)開(kāi)啟或關(guān)閉、接入和維護(hù)成本也相對(duì)較低,但是它在 Trace 精度和性能上存在明顯的不足,核心原因在于采樣抓棧采用的是定期的異步抓棧流程。首先是每次抓棧都需要經(jīng)歷掛起線程、回溯方法棧、恢復(fù)線程三個(gè)階段,這三個(gè)階段都有著明顯的性能損耗。其次后臺(tái)線程的定時(shí)任務(wù)由于線程調(diào)度的原因,無(wú)法做到精準(zhǔn)的調(diào)度,以及線程掛起時(shí)機(jī)的不確定性,導(dǎo)致抓棧的間隔至少都設(shè)置在 10 毫秒以上,trace 的精度無(wú)法保證。
既然兩種方案各有優(yōu)劣,我們能否取長(zhǎng)補(bǔ)短,將兩種方案的優(yōu)勢(shì)融合呢?答案是肯定的,將異步抓棧改成直接在目標(biāo)線程進(jìn)行同步抓棧來(lái)免去線程掛起和恢復(fù)帶來(lái)的性能損耗,再通過(guò)動(dòng)態(tài)插樁提供的插樁點(diǎn)作為同步抓棧的驅(qū)動(dòng)時(shí)機(jī),最終就形成了 btrace 3.0 所采用的動(dòng)態(tài)插樁和同步抓棧結(jié)合的 Trace 采集新方案。
新方案將如何保證 Trace 的精度呢?這對(duì)應(yīng)著動(dòng)態(tài)插樁的插樁點(diǎn)選取策略,也即尋找同步抓棧的最佳時(shí)機(jī)。實(shí)際上只要保證插樁點(diǎn)在線程運(yùn)行時(shí)能夠被高頻地執(zhí)行到,我們就可以通過(guò)高頻地同步抓棧來(lái)保證 Trace 的精度。另外還需要注意的是,插樁點(diǎn)的選取不僅要保證能夠被高頻地執(zhí)行到,同時(shí)也要盡可能的分布在“葉子節(jié)點(diǎn)”所處的方法上。如下圖所示,如果“葉子節(jié)點(diǎn)”方法上沒(méi)有插樁點(diǎn),那么最終生成的 Trace 就會(huì)存在信息丟失。如何實(shí)現(xiàn)快速同步抓棧、如何進(jìn)行動(dòng)態(tài)插樁以及具體選取哪些插樁點(diǎn)將在方案明細(xì)中介紹。
值得一提的是,同步抓棧方案除了免去線程掛起和恢復(fù)的性能損耗以外,還可以在抓棧時(shí)記錄當(dāng)前線程的更多上下文信息,更進(jìn)一步地結(jié)合了插樁與抓棧的雙重優(yōu)勢(shì),這方面也將在下文的方案中進(jìn)行闡述。
然而,同步抓棧高度依賴樁點(diǎn)。若遇到極端情況,如方法邏輯本身不存在合適的樁點(diǎn),或者線程被阻塞,便無(wú)法采集到相應(yīng)的 Trace 信息。針對(duì)此問(wèn)題,可通過(guò)異步抓棧進(jìn)一步提高 Trace 的豐富度。特別是 iOS 系統(tǒng),其自身具備極高的異步抓棧性能,適合在同步抓棧的基礎(chǔ)上疊加異步抓棧功能,以進(jìn)一步提升 Trace 的豐富度。
方案明細(xì)
接下來(lái),我們將對(duì)雙端的技術(shù)細(xì)節(jié)展開(kāi)深入探討。鑒于系統(tǒng)差異,雙端的實(shí)現(xiàn)原理存在區(qū)別,以下將分別進(jìn)行介紹。
Android
Android 端的 Trace 采集方案主要分為同步抓棧和動(dòng)態(tài)插樁兩部分,其中同步抓棧部分由于已經(jīng)免去了線程掛起和恢復(fù)流程,所以只需要聚焦于如何實(shí)現(xiàn)快速的方法抓棧即可。
快速抓棧
Android Framework 本身提供的抓棧方式 Thread.getStackTrace 會(huì)在抓棧的同時(shí)解析方法符號(hào),解析符號(hào)是比較耗時(shí)的操作。針對(duì) Trace 采集需要高頻率抓棧的場(chǎng)景,每次抓棧都解析方法符號(hào)顯然是比較浪費(fèi)性能的選擇,尤其是多次抓棧里很可能會(huì)存在很多重復(fù)的方法符號(hào)解析。為了優(yōu)化抓棧性能,我們選擇在抓棧時(shí)僅保存方法的指針信息,待抓棧完成后,進(jìn)行 Trace 數(shù)據(jù)上報(bào)時(shí)對(duì)指針去重后進(jìn)行批量符號(hào)化,這樣可以最大化節(jié)省解析符號(hào)的成本。
具體該如何實(shí)現(xiàn)快速抓棧且僅保存方法指針信息呢?在 Android 中 Java 方法的棧回溯主要依賴 ART 虛擬機(jī)內(nèi)部的 StackVisitor 類來(lái)實(shí)現(xiàn)的,大部分的快速抓棧方案都是圍繞著創(chuàng)建 StackVisitor 的實(shí)現(xiàn)類對(duì)象并調(diào)用其 WalkStack() 方法進(jìn)行回溯來(lái)實(shí)現(xiàn)的,我們也是使用這種方案。只有少數(shù)方案如 Facebook 的 profilo 是不依賴系統(tǒng) StackVisitor 類自己實(shí)現(xiàn)的棧回溯,但是這種方案的兼容性較差且有較高的維護(hù)成本,目前隨著 Android 版本的不斷更新,profilo 官方已無(wú)力再對(duì)新版本進(jìn)行更新適配了。
不過(guò)在使用系統(tǒng)的 StackVisitor 類進(jìn)行棧回溯時(shí)我們也做了一些額外的版本適配方案的優(yōu)化。具體來(lái)說(shuō)是構(gòu)造 StackVisitor 對(duì)象的方案優(yōu)化,我們不假定 StackVisitor 類內(nèi)成員變量的內(nèi)存布局,而是定義了一個(gè)內(nèi)存足夠的 mSpaceHolder 字段來(lái)容納 StackVisitor 對(duì)象的所有成員變量。
class StackVisitor {... [[maybe_unused]] virtual bool VisitFrame(); // preserve for real StackVisitor's fields space [[maybe_unused]] char mSpaceHolder[2048]; ...};
class StackVisitor {
...
[[maybe_unused]] virtual bool VisitFrame();
// preserve for real StackVisitor's fields space
[[maybe_unused]] char mSpaceHolder[2048];
...
};
然后交由 ART 提供的構(gòu)造函數(shù)來(lái)妥善初始化 StackVisitor 對(duì)象,自動(dòng)將預(yù)留的 mSpaceHolder 初始化成預(yù)期的數(shù)據(jù),省去了對(duì)每個(gè)版本的對(duì)象內(nèi)存布局適配工作。同時(shí)再將 StackVisitor 對(duì)象的虛函數(shù)表替換,最后實(shí)現(xiàn)類似繼承自 StackVisitor 的效果。
bool StackVisitor::innerVisitOnce(JavaStack &stack, void *thread, uint64_t *outTime, uint64_t *outCpuTime) { StackVisitor visitor(stack); void *vptr = *reinterpret_cast<void **>(&visitor); // art::Context::Create() auto *context = sCreateContextCall(); // art::StackVisitor::StackVisitor(art::Thread*, art::Context*, art::StackVisitor::StackWalkKind, bool) sConstructCall(reinterpret_cast<void *>(&visitor), thread, context, StackWalkKind::kIncludeInlinedFrames, false); *reinterpret_cast<void **>(&visitor) = vptr; // void art::StackVisitor::WalkStack<(art::StackVisitor::CountTransitions)0>(bool) visitor.walk();}
bool StackVisitor::innerVisitOnce(JavaStack &stack, void *thread, uint64_t *outTime,
uint64_t *outCpuTime) {
StackVisitor visitor(stack);
void *vptr = *reinterpret_cast<void **>(&visitor);
// art::Context::Create()
auto *context = sCreateContextCall();
// art::StackVisitor::StackVisitor(art::Thread*, art::Context*, art::StackVisitor::StackWalkKind, bool)
sConstructCall(reinterpret_cast<void *>(&visitor), thread, context, StackWalkKind::
kIncludeInlinedFrames
, false);
*reinterpret_cast<void **>(&visitor) = vptr;
// void art::StackVisitor::WalkStack<(art::StackVisitor::CountTransitions)0>(bool)
visitor.walk();
}
最后當(dāng)調(diào)用 StackVisitor.walk 后,相關(guān)回調(diào)都將分發(fā)到我們自己的 VisitFrame,這樣只需要再調(diào)用相關(guān)函數(shù)進(jìn)行堆棧數(shù)據(jù)讀取即可。
[[maybe_unused]] bool StackVisitor::VisitFrame() { // art::StackVisitor::GetMethod() const auto *method = sGetMethodCall(reinterpret_cast<void *>(this)); mStack.mStackMethods[mCurIndex] = uint64_t(method); mCurIndex++; return true;}
[[maybe_unused]] bool StackVisitor::VisitFrame() {
// art::StackVisitor::GetMethod() const
auto *method = sGetMethodCall(reinterpret_cast<void *>(this));
mStack.mStackMethods[mCurIndex] = uint64_t(method);
mCurIndex++;
return true;
}
這種方案在性能和兼容性方面能同時(shí)得到保障,維護(hù)成本也低。
動(dòng)態(tài)插樁
現(xiàn)在可以實(shí)現(xiàn)快速抓棧了,那么應(yīng)該在什么時(shí)候抓棧呢?這就輪到動(dòng)態(tài)插樁出場(chǎng)了,所謂的動(dòng)態(tài)插樁是利用運(yùn)行時(shí)的 Hook 工具對(duì)系統(tǒng)內(nèi)部的方法進(jìn)行 Hook 并插入同步抓棧的邏輯,目前主要使用到的 Hook 工具為 ShadowHook。
按照前文思路分析,對(duì)于動(dòng)態(tài)插樁重點(diǎn)是高頻且盡可能分布在“葉子節(jié)點(diǎn)”方法上。而在 Android 應(yīng)用中 Java 對(duì)象的創(chuàng)建是虛擬機(jī)運(yùn)行過(guò)程中除方法調(diào)用外最高頻的操作,并且對(duì)象創(chuàng)建時(shí)的內(nèi)存分配不會(huì)調(diào)用任何業(yè)務(wù)邏輯,是整個(gè)方法執(zhí)行的末端。于是,第一個(gè)理想的抓棧時(shí)機(jī)即是 Java 對(duì)象創(chuàng)建時(shí)的內(nèi)存分配。
Java 對(duì)象創(chuàng)建監(jiān)控的核心是向虛擬機(jī)注冊(cè)對(duì)象創(chuàng)建的監(jiān)聽(tīng)器,這個(gè)能力虛擬機(jī)的 Heap 類已經(jīng)提供有注冊(cè)接口,但是通過(guò)該接口注冊(cè)首先會(huì)暫停所有 Java 線程,這會(huì)存在很高的 ANR 風(fēng)險(xiǎn),為此我們借鑒了公司內(nèi)部開(kāi)發(fā)的 Java 對(duì)象創(chuàng)建方案,實(shí)現(xiàn)了實(shí)時(shí)的 Java 對(duì)象創(chuàng)建監(jiān)控能力,具體實(shí)現(xiàn)原理請(qǐng)移步查看倉(cāng)庫(kù)源碼,這里不作詳細(xì)介紹。
注冊(cè)完 AllocationListener 后將在每次對(duì)象分配時(shí)收到回調(diào):
class MyAllocationListener : AllocationListener { ... void ObjectAllocated(void *self, void **obj, size_t byte_count) override { // TODO 這里抓棧 }};
class MyAllocationListener : AllocationListener {
...
void ObjectAllocated(void *self, void **obj, size_t byte_count) override {
// TODO 這里抓棧
}
};
因?yàn)閷?duì)象分配十分高頻,如果每次都進(jìn)行抓棧會(huì)有很大的性能損耗。一方面大批量的抓棧開(kāi)銷累計(jì)會(huì)有很大的性能成本,另一方面如此存儲(chǔ)大規(guī)模的抓棧數(shù)據(jù)也是棘手的問(wèn)題。
為控制抓棧數(shù)量以減少性能損耗,首先可考慮的方法是控頻:通過(guò)對(duì)比連續(xù)兩次內(nèi)存回調(diào)的間隔時(shí)間,僅當(dāng)該時(shí)間間隔大于閾值時(shí)才再次進(jìn)行抓棧操作。
thread_local uint64_t lastNano = 0;bool SamplingCollector::request(SamplingType type, void *self, bool force, bool captureAtEnd, uint64_t beginNano, uint64_t beginCpuNano, std::function<void(SamplingRecord&)> fn) { auto currentNano = rheatrace::current_time_nanos(); if (force || currentNano - lastNano > threadCaptureInterval) { lastNano = currentNano; ... if (StackVisitor::visitOnce(r.mStack, self)) { collector->write(r); return true; } } return false;}
thread_local uint64_t lastNano = 0;
bool SamplingCollector::request(SamplingType type, void *self, bool force, bool captureAtEnd, uint64_t beginNano, uint64_t beginCpuNano, std::function<void(SamplingRecord&)> fn) {
auto currentNano = rheatrace::current_time_nanos();
if (force || currentNano - lastNano > threadCaptureInterval) {
lastNano = currentNano;
...
if (StackVisitor::visitOnce(r.mStack, self)) {
collector->write(r);
return true;
}
}
return false;
}
除了內(nèi)存分配以外,還可以通過(guò)豐富其他抓棧的時(shí)機(jī)來(lái)提升兩次抓棧的時(shí)間密度,提升數(shù)據(jù)的效果。比如 JNI 方法調(diào)用、獲取鎖、Object.wait、Unsafe.park 等節(jié)點(diǎn)。這些葉子節(jié)點(diǎn)可以主要分為兩大類:高頻執(zhí)行與阻塞執(zhí)行。
高頻執(zhí)行是很好的進(jìn)行主動(dòng)抓棧的時(shí)間點(diǎn),記錄下當(dāng)前執(zhí)行的堆棧即可,比如前面介紹的內(nèi)存分配、JNI 調(diào)用等。
阻塞執(zhí)行即可能處于阻塞的狀態(tài)等待滿足條件后繼續(xù)執(zhí)行,對(duì)于此類節(jié)點(diǎn),除了對(duì)應(yīng)的執(zhí)行堆棧外,還預(yù)期記錄當(dāng)前方法阻塞的耗時(shí)。可以在方法即將執(zhí)行時(shí)記錄開(kāi)始執(zhí)行時(shí)間,在方法結(jié)束時(shí)進(jìn)行抓棧并記錄結(jié)束時(shí)間。
這里以獲取鎖的的場(chǎng)景為例,獲取鎖最終會(huì)走到 Native 層的 MonitorEnter,可以通過(guò) shadowhook 來(lái)代理該函數(shù)的執(zhí)行:
void Monitor_Lock(void* monitor, void* threadSelf) {SHADOWHOOK_STACK_SCOPE(); rheatrace::ScopeSampling a(rheatrace::stack::SamplingType::kMonitor, threadSelf);SHADOWHOOK_CALL_PREV(Monitor_Lock, monitor, threadSelf);}class ScopeSampling {private: uint64_t beginNano_; uint64_t beginCpuNano_;public: ScopeSampling(SamplingType type, void *self = nullptr, bool force = false) : type_(type), self_(self), force_(force) { beginNano_ = rheatrace::current_time_nanos(); beginCpuNano_ = rheatrace::thread_cpu_time_nanos(); } ~ScopeSampling() { SamplingCollector::request(type_, self_, force_, true, beginNano_, beginCpuNano_); }};
void Monitor_Lock(void* monitor, void* threadSelf) {
SHADOWHOOK_STACK_SCOPE();
rheatrace::ScopeSampling a(rheatrace::stack::SamplingType::
kMonitor, threadSelf);
SHADOWHOOK_CALL_PREV
(Monitor_Lock, monitor, threadSelf);
}
class ScopeSampling {
private:
uint64_t beginNano_;
uint64_t beginCpuNano_;
public:
ScopeSampling(SamplingType type, void *self = nullptr, bool force = false) : type_(type), self_(self), force_(force) {
beginNano_ = rheatrace::current_time_nanos();
beginCpuNano_ = rheatrace::thread_cpu_time_nanos();
}
~ScopeSampling() {
SamplingCollector::request(type_, self_, force_, true, beginNano_, beginCpuNano_);
}
};
通過(guò)封裝的 ScopeSampling 對(duì)象,可以在 Monitor_Lock 函數(shù)執(zhí)行時(shí)記錄方法的開(kāi)始時(shí)間,待方法結(jié)束時(shí)記錄結(jié)束時(shí)間的同時(shí)并進(jìn)行抓棧。這樣整個(gè)鎖沖突的堆棧以及獲取鎖的耗時(shí)都會(huì)被完整的記錄下來(lái)。
除了鎖沖突以外,像 Object.wait、Unsafe.park、GC 等阻塞類型的耗時(shí),都可以通過(guò)這樣的方法同時(shí)記錄執(zhí)行堆棧與耗時(shí)信息。
至此 Android 端的核心原理基本完成介紹,歡迎移步 https://github.com/bytedance/btrace 進(jìn)一步了解。
iOS
下面來(lái)看 iOS 的原理,正如前文所述,iOS 具備高性能的異步抓棧方案,因此 iOS 端采用同步與異步結(jié)合采樣的 Trace 采集方案:
- 同步抓棧:選定一批方法進(jìn)行 hook,當(dāng)這些方法被執(zhí)行時(shí),同步采集當(dāng)前線程的 Trace 數(shù)據(jù)。
- 異步抓棧:當(dāng)線程超過(guò)一定時(shí)間未被采集數(shù)據(jù)時(shí),由獨(dú)立的采樣線程暫停線程,異步采集 Trace 數(shù)據(jù)然后恢復(fù)線程,以確保數(shù)據(jù)的時(shí)間連續(xù)性。
同步采集
同步采集模式在性能和數(shù)據(jù)豐富度方面都有一定優(yōu)勢(shì)。在同步采集模式下,我們遇到并解決了一些有挑戰(zhàn)的問(wèn)題:
- 選擇同步采集點(diǎn)
- 減少存儲(chǔ)占用
- 多線程數(shù)據(jù)寫入性能
選擇同步采集點(diǎn)
可能的選取方法有:
- 依靠 RD 同學(xué)的經(jīng)驗(yàn),但是存在不可持續(xù)性。
- 通過(guò)代碼插樁,但是需要重新編譯 app,其次無(wú)法獲取到系統(tǒng)庫(kù)方法,容易存在遺漏。
我們推薦檢查 iOS app 高頻系統(tǒng)調(diào)用,因?yàn)橄到y(tǒng)調(diào)用足夠底層且對(duì)于 app 來(lái)說(shuō)必不可少。可以在模擬器上運(yùn)行 app,然后使用如下命令查看 app 運(yùn)行時(shí)的系統(tǒng)調(diào)用使用情況:
# 關(guān)閉sip# 終端登錄root賬號(hào)dtruss -c -p pid # -c表示統(tǒng)計(jì)系統(tǒng)調(diào)用的次數(shù);
# 關(guān)閉sip
# 終端登錄root賬號(hào)
dtruss -c -p pid # -c表示統(tǒng)計(jì)系統(tǒng)調(diào)用的次數(shù);
通過(guò)實(shí)際分析可知, iOS app 高頻系統(tǒng)調(diào)用可以大致分為這幾類:內(nèi)存分配、I/O、鎖、時(shí)間戳讀取,如下所示:
名稱 次數(shù)ulock_wake 15346 # unfair lock, unlockulock_wait2 15283 # unfair lock, lockpread 10758 # iopsynch_cvwait 10360 # pthread條件變量,等待read 9963 # iofcntl 8403 # iomprotect 8247 # memorymmap 8225 # memorygettimeofday 7531 # 時(shí)間戳psynch_cvsignal 6862 # pthread條件變量,發(fā)送信號(hào)writev 6048 # ioread_nocancel 4892 # iofstat64 4817 # iopwrite 3646 # iowrite_nocancel 3446 # ioclose 2850 # iogetsockopt 2818 # 網(wǎng)絡(luò)stat64 2811 # iopselect 2457 # io多路復(fù)用psynch_mutexwait 1923 # mutex, lockpsynch_mutexdrop 1918 # mutex, unlockpsynch_cvbroad 1826 # pthread條件變量,發(fā)送廣播信號(hào)
名稱 次數(shù)
ulock_wake 15346 # unfair lock, unlock
ulock_wait2 15283 # unfair lock, lock
pread 10758 # io
psynch_cvwait 10360 # pthread條件變量,等待
read 9963 # io
fcntl 8403 # io
mprotect 8247 # memory
mmap 8225 # memory
gettimeofday 7531 # 時(shí)間戳
psynch_cvsignal 6862 # pthread條件變量,發(fā)送信號(hào)
writev 6048 # io
read_nocancel 4892 # io
fstat64 4817 # io
pwrite 3646 # io
write_nocancel 3446 # io
close 2850 # io
getsockopt 2818 # 網(wǎng)絡(luò)
stat64 2811 # io
pselect 2457 # io多路復(fù)用
psynch_mutexwait 1923 # mutex, lock
psynch_mutexdrop 1918 # mutex, unlock
psynch_cvbroad 1826 # pthread條件變量,發(fā)送廣播信號(hào)
減小存儲(chǔ)占用
所采集的 Trace 數(shù)據(jù)中占用存儲(chǔ)空間(內(nèi)存和磁盤)最大的就是調(diào)用棧數(shù)據(jù)。調(diào)用棧具有時(shí)間和空間相似性,我們可以利用這些相似性來(lái)大幅壓縮調(diào)用棧所占用的空間。
空間相似性:可以觀察到,調(diào)用棧中越靠近上層的方法越容易是相同的,比如主線程的調(diào)用棧都以 main 方法開(kāi)始。因此,我們可以將不同調(diào)用棧中相同層級(jí)的同名方法只存儲(chǔ)一份來(lái)節(jié)省存儲(chǔ)空間。我們?cè)O(shè)計(jì)了 CallstackTable 結(jié)構(gòu)來(lái)存儲(chǔ)調(diào)用棧:
class CallstackTable{public: struct Node { uint64_t parent; uint64_t address; };
struct NodeHash { size_t operator()(const Node* node) const { size_t h = std::hash<uint64_t>{}(node->parent); h ^= std::hash<uint64_t>{}(node->address); return h; } }; struct NodeEqual { bool operator()(const Node* node1, const Node* node2) const noexcept { bool result = (node1->parent == node2->parent) && (node1->address == node2->address); return result; } }; using CallStackSet = hash_set<Node *, NodeHash, NodeEqual>;private: CallStackSet stack_set_;};
class CallstackTable
{
public:
struct Node
{
uint64_t parent;
uint64_t address;
};
struct NodeHash {
size_t operator()(const Node* node) const {
size_t h = std::hash<uint64_t>{}(node->parent);
h ^= std::hash<uint64_t>{}(node->address);
return h;
}
};
struct NodeEqual
{
bool operator()(const Node* node1, const Node* node2) const noexcept
{
bool result = (node1->parent == node2->parent) && (node1->address == node2->address);
return result;
}
};
using CallStackSet = hash_set<Node *, NodeHash, NodeEqual>;
private:
CallStackSet stack_set_;
};
以下圖為例介紹如何高效存儲(chǔ)調(diào)用棧。
- 樣本 1 的
A
方法沒(méi)有父方法,因此存儲(chǔ)為 Node(0, A),并記錄 Node 的地址為 NodeA - 樣本 1 的
B
方法的父方法為A
,因此存儲(chǔ)為 Node(NodeA, B),并記錄 Node 的地址為 NodeB - 樣本 1 的
C
方法的父方法為B
,因此存儲(chǔ)為 Node(NodeB, C),并記錄 Node 的地址為 NodeC - 樣本 2 的
A
方法對(duì)應(yīng)的 Node 為 Node(0, A),已經(jīng)存儲(chǔ)過(guò),不再重復(fù)存儲(chǔ) - 樣本 2 的
B
方法和C
方法同理不再重復(fù)存儲(chǔ) - 樣本 3 的
A
方法不再重復(fù)存儲(chǔ) - 樣本 3 的
E
方法存儲(chǔ)為 Node(NodeA, E) - 樣本 3 的
C
方法存儲(chǔ)為 Node(NodeE, C)
時(shí)間相似性:App 中很多方法的執(zhí)行時(shí)間都遠(yuǎn)大于我們的采集間隔,因此連續(xù)一段時(shí)間內(nèi)采集到的調(diào)用棧很有可能是相同的。我們可以把相鄰相同調(diào)用棧的記錄進(jìn)行合并,只存儲(chǔ)開(kāi)始和結(jié)束兩條記錄,就可以極大程度上減少存儲(chǔ)空間占用。
以上圖為例進(jìn)行說(shuō)明,如果以 1ms 的間隔進(jìn)行數(shù)據(jù)采集,而 A->B->C->D 棧執(zhí)行了 100ms,那么就會(huì)采集到 100 條記錄,由于這 100 條記錄對(duì)應(yīng)的是相同的棧,我們可以只記錄開(kāi)始和結(jié)束兩條記錄,從而大幅壓縮存儲(chǔ)占用。
多線程數(shù)據(jù)寫入
同步采集方案中,存在多線程同時(shí)寫入數(shù)據(jù)到 buffer 的情況,必須對(duì)多線程寫入進(jìn)行處理,保證數(shù)據(jù)不異常。
一般通過(guò)加鎖來(lái)保證數(shù)據(jù)安全,但是性能比較差。而 CAS 操作可以顯著優(yōu)于加鎖,但是 lock-free 不等于 wait-free,也會(huì)有偶發(fā)性性能問(wèn)題。還可以使用 Thread Local Buffer 的方案,其最大的優(yōu)勢(shì)是完全避免線程間沖突性能最優(yōu),但是容易造成內(nèi)存浪費(fèi),需要仔細(xì)考慮內(nèi)存復(fù)用機(jī)制。我們摒棄了以上傳統(tǒng)方案,而是將單個(gè) buffer 拆分為多個(gè) sub buffer,將線程的每次寫入操作根據(jù)線程 ID 和寫入大小分配到各個(gè) sub buffer,類似哈希機(jī)制來(lái)提升寫入的并發(fā)度。
異步采集
異步抓棧方案相對(duì)成熟,Apple 官方的 Time Profiler 以及開(kāi)源的 ETTrace 等項(xiàng)目均采用了該方案。抖音 iOS btrace 初版同樣運(yùn)用了異步抓棧方案。異步抓棧方案不可避免地需要先暫停線程執(zhí)行,接著采集數(shù)據(jù),最后恢復(fù)線程執(zhí)行。此過(guò)程易出現(xiàn)死鎖與性能問(wèn)題,我們也對(duì)此進(jìn)行了針對(duì)性處理。
防止死鎖
當(dāng)訪問(wèn)某些系統(tǒng)方法時(shí),采集線程與被采集線程可能同時(shí)持有相同的鎖。例如,在同時(shí)調(diào)用 malloc
進(jìn)行內(nèi)存分配時(shí),可能會(huì)觸發(fā) malloc
內(nèi)部的鎖。當(dāng)代碼以如下方式被觸發(fā)時(shí),將會(huì)導(dǎo)致死鎖:
- 被采樣線程調(diào)用
malloc
并首先獲取鎖; - 被采樣線程被采樣線程暫停執(zhí)行;
- 采樣線程調(diào)用
malloc
并嘗試獲取鎖,但是永遠(yuǎn)等不到鎖釋放。
最終,采樣線程會(huì)陷入永久性的等待鎖狀態(tài),被采樣線程則會(huì)發(fā)生永久性暫停。
針對(duì)此類問(wèn)題也沒(méi)有特別好的處理辦法,目前是通過(guò)一些約定禁止在采樣線程在掛起被采樣線程期間調(diào)用危險(xiǎn) api,類似于信號(hào)安全函數(shù),具體已知的函數(shù)如下:
- ObjC 方法,因?yàn)?OC 方法動(dòng)態(tài)派發(fā)時(shí)可能會(huì)持有內(nèi)部鎖
- 打印文本(printf 家族,NSlog 等)
- 堆內(nèi)存分配(malloc 等)
- pthread 部分 api
性能優(yōu)化
過(guò)濾非活躍線程:抖音這種大型 App 在運(yùn)行過(guò)程中會(huì)創(chuàng)建數(shù)量非常多的線程,如果每次采樣都采集所有線程的數(shù)據(jù),性能開(kāi)銷是無(wú)法接受的。同時(shí),也沒(méi)有必要每次都采集所有線程的數(shù)據(jù),因?yàn)榻^大多數(shù)線程在大部分時(shí)間里都在休眠,只有當(dāng)事件發(fā)生時(shí)線程才會(huì)喚醒并處理執(zhí)行任務(wù),因此可以只采集活躍狀態(tài)的線程的數(shù)據(jù),同時(shí),對(duì)長(zhǎng)時(shí)間未被采集數(shù)據(jù)的線程強(qiáng)制采集一次數(shù)據(jù)。
bool active(uint64_t thread_id) { mach_msg_type_number_t thread_info_count = THREAD_BASIC_INFO_COUNT; kern_return_t res = thread_info(thread_id, THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(info), &thread_info_count); if (unlikely((res != KERN_SUCCESS))) { return false; } return (info.run_state == TH_STATE_RUNNING && (info.flags & TH_FLAGS_IDLE) == 0);}
bool active(uint64_t thread_id)
{
mach_msg_type_number_t thread_info_count = THREAD_BASIC_INFO_COUNT;
kern_return_t res = thread_info(thread_id, THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(info), &thread_info_count);
if (unlikely((res != KERN_SUCCESS)))
{
return false;
}
return (info.run_state == TH_STATE_RUNNING && (info.flags & TH_FLAGS_IDLE) == 0);
}
高效棧回溯:異步回溯線程的調(diào)用棧時(shí),為了防止讀取到非法指針中的數(shù)據(jù)導(dǎo)致 app 崩潰,通常會(huì)使用系統(tǒng)庫(kù)提供的 api vm_read_overwrite
來(lái)讀取數(shù)據(jù),使用該 api 讀取到非法指針的數(shù)據(jù)時(shí)會(huì)得到一個(gè)錯(cuò)誤標(biāo)識(shí)而不會(huì)導(dǎo)致 app 崩潰。雖然vm_read_overwrite
已經(jīng)足夠高效(耗時(shí)在微秒級(jí)別),但其耗時(shí)相比于直接讀指針的耗時(shí)仍然高了數(shù)十倍。而且 app 的調(diào)用棧通常都有數(shù)十層,因此vm_read_overwrite
的高耗時(shí)問(wèn)題會(huì)被放大。我們?cè)诨厮菡{(diào)用棧之前已經(jīng)將線程暫停了,理論上線程的調(diào)用棧不會(huì)發(fā)生變化,所有的指針都應(yīng)該是合法的,然而經(jīng)過(guò)實(shí)際驗(yàn)證,直接讀指針確實(shí)會(huì)讀取到非法指針從而造成 app 崩潰。通過(guò)閱讀暫停線程的 api thread_suspend
的說(shuō)明文檔,以及分析崩潰日志,我們發(fā)現(xiàn)在一些情況下線程會(huì)繼續(xù)運(yùn)行(如線程正在執(zhí)行退出時(shí)的清理動(dòng)作)。
The thread_suspend function increments the suspend count for target_thread and prevents the thread from executing any more user-level instructions.
In this context, a user-level instruction can be either a machine instruction executed in user mode or a system trap instruction, including a page fault. If a thread is currently executing within a system trap, the kernel code may continue to execute until it reaches the systemreturn code or it may suspend within the kernel code. In either case, the system trap returns when the thread resumes.
The thread_suspend function increments the suspend count for target_thread and prevents the thread from executing any more user-level instructions.
In this context, a user-level instruction can be either a machine instruction executed in user mode or a system trap instruction, including a page fault. If a thread is currently executing within a system trap, the kernel code may continue to execute until it reaches the systemreturn code or it may suspend within the kernel code. In either case, the system trap returns when the thread resumes.
The thread_suspend function increments the suspend count for target_thread and prevents the thread from executing any more user-level instructions.
In this context, a user-level instruction can be either a machine instruction executed in user mode or a system trap instruction, including a page fault. If a thread is currently executing within a system trap, the kernel code may continue to execute until it reaches the systemreturn code or it may suspend within the kernel code. In either case, the system trap returns when the thread resumes.
最終,我們使用如下措施來(lái)盡可能使用直接讀指針的方式以提升?;厮莸男阅埽?/span>
- 不采集正在退出的線程的數(shù)據(jù)
- 暫停線程后,再次確認(rèn)線程運(yùn)行狀態(tài),若線程已經(jīng)暫停直接讀指針進(jìn)行棧回溯
- 否則兜底使用系統(tǒng) api
vm_read_overwrite
保證安全異步回溯調(diào)用棧
Trace 生成
在介紹完雙端的技術(shù)實(shí)現(xiàn)細(xì)節(jié)之后,接下來(lái)我們將關(guān)注 Trace 可視化部分。
在 Trace 可視化方面,雙端都依舊選擇了基于 perfetto 進(jìn)行數(shù)據(jù)展示。具體邏輯與 Android 官方提供的 Debug.startMethodTracingSampling 的實(shí)現(xiàn)方案相似,此處以 Android 為例進(jìn)行簡(jiǎn)要介紹,iOS 的實(shí)現(xiàn)情況大體一致。
基本思路是對(duì)比連續(xù)抓取的棧信息之間的差異,找出從棧頂?shù)綏5椎牡谝粋€(gè)不同的函數(shù)。將前序堆棧中該不同的函數(shù)出棧,把后序堆棧中該不同的函數(shù)入棧,入棧和出棧的時(shí)間間隔即為該函數(shù)的執(zhí)行耗時(shí)。以下是代碼示例:
// 生成一個(gè)虛擬的 Root 節(jié)點(diǎn),待完成解析后,Root 的子樹(shù)便構(gòu)成了 Trace 的森林CallNode root = CallNode.makeRoot();Stack<CallNode> stack = new Stack<>();stack.push(root);...for (int i = 0; i < stackList.size(); i++) { StackItem curStackItem = stackList.get(i); nanoTime = curStackItem.nanoTime; // 第一個(gè)堆棧全部入棧 if (i == 0) { for (String name : curStackItem.stackTrace) { stack.push(new CallNode(curStackItem.tid, name, nanoTime, stack.peek())); } } else { // 當(dāng)前堆棧與前一個(gè)堆棧對(duì)比,自頂向下,找到第一個(gè)不同的函數(shù) StackItem preStackItem = stackList.get(i - 1); int preIndex = 0; int curIndex = 0; while (preIndex < preStackItem.size() && curIndex < curStackItem.size()) { if (preStackItem.getPtr(preIndex) != curStackItem.getPtr(curIndex)) { break; } preIndex++; curIndex++; } // 前一個(gè)堆棧中不同的函數(shù)全部出棧 for (; preIndex < preStackItem.size(); preIndex++) { stack.pop().end(nanoTime); } // 當(dāng)前堆棧中不同的函數(shù)全部入棧 for (; curIndex < curStackItem.size(); curIndex++) { String name = curStackItem.get(curIndex); stack.push(new CallNode(curStackItem.tid, name, nanoTime, stack.peek())); } }}// 遺留在棧中的函數(shù)全部出棧while (!stack.isEmpty()) { stack.pop().end(nanoTime);}
// 生成一個(gè)虛擬的 Root 節(jié)點(diǎn),待完成解析后,Root 的子樹(shù)便構(gòu)成了 Trace 的森林
CallNode root = CallNode.makeRoot();
Stack<CallNode> stack = new Stack<>();
stack.push(root);
...
for (int i = 0; i < stackList.size(); i++) {
StackItem curStackItem = stackList.get(i);
nanoTime = curStackItem.nanoTime;
// 第一個(gè)堆棧全部入棧
if (i == 0) {
for (String name : curStackItem.stackTrace) {
stack.push(new CallNode(curStackItem.tid, name, nanoTime, stack.peek()));
}
} else {
// 當(dāng)前堆棧與前一個(gè)堆棧對(duì)比,自頂向下,找到第一個(gè)不同的函數(shù)
StackItem preStackItem = stackList.get(i - 1);
int preIndex = 0;
int curIndex = 0;
while (preIndex < preStackItem.size() && curIndex < curStackItem.size()) {
if (preStackItem.getPtr(preIndex) != curStackItem.getPtr(curIndex)) {
break;
}
preIndex++;
curIndex++;
}
// 前一個(gè)堆棧中不同的函數(shù)全部出棧
for (; preIndex < preStackItem.size(); preIndex++) {
stack.pop().end(nanoTime);
}
// 當(dāng)前堆棧中不同的函數(shù)全部入棧
for (; curIndex < curStackItem.size(); curIndex++) {
String name = curStackItem.get(curIndex);
stack.push(new CallNode(curStackItem.tid, name, nanoTime, stack.peek()));
}
}
}
// 遺留在棧中的函數(shù)全部出棧
while (!stack.isEmpty()) {
stack.pop().end(nanoTime);
}
敏銳的讀者或許已經(jīng)察覺(jué)到,由于采用的是采樣抓棧方式,因此可能出現(xiàn)多次抓棧時(shí)堆棧完全相同,但這些情況可能并非源自同一次代碼執(zhí)行的情形。以下圖為例:
有 3 次不同的執(zhí)行堆棧,但是由于采樣的原因,僅 1 和 3 被采樣抓棧。按照上面的規(guī)則生成 Trace,那么 B 和 C 的方法耗時(shí)都會(huì)被放大,包含了方法 D 的耗時(shí)。
很遺憾,對(duì)于此類情況無(wú)法徹底解決掉,但是可以針對(duì)特定的問(wèn)題進(jìn)行規(guī)避。比如針對(duì)消息執(zhí)行的場(chǎng)景,可以設(shè)計(jì)一個(gè)消息 ID 用于標(biāo)記消息,每次執(zhí)行 nativePollOnce 后消息 ID 自增,在每次抓棧時(shí)同時(shí)記錄消息 ID。這樣就算多次抓棧結(jié)果一致,但是只要消息 ID 不一樣,依然可以識(shí)別出來(lái)從而在解析 Trace 時(shí)結(jié)束方法。
最后我們?cè)倏聪聰?shù)據(jù)效果,下面是 btrace demo 在啟動(dòng)階段的 Trace 數(shù)據(jù),可以看到豐富度和細(xì)節(jié)上相比于 2.0 有了明顯的提升。
Android 效果圖
iOS 效果圖
耗時(shí)歸因數(shù)據(jù)
除基本的方法執(zhí)行 Trace 外,不僅要了解方法的耗時(shí)情況,還需明確方法產(chǎn)生耗時(shí)的原因。為此,我們需要進(jìn)一步剖析方法的耗時(shí)原因,深入分析 WallTime 的具體去向,例如在 CPU 執(zhí)行上花費(fèi)了多少時(shí)間、有多少時(shí)間處于阻塞狀態(tài)等信息。得益于整體方案的設(shè)計(jì),我們?cè)诿看芜M(jìn)行棧抓取時(shí)采集包含 WallTime 在內(nèi)的額外信息,從而能夠輕松實(shí)現(xiàn)函數(shù)級(jí)的數(shù)據(jù)統(tǒng)計(jì)。 這部分?jǐn)?shù)據(jù)采集的原理在雙端是類似的,篇幅有限,這里僅以 Android 為例展開(kāi)介紹。
CPUTime
CPUTime 是最基礎(chǔ)的耗時(shí)歸因數(shù)據(jù),可以直觀體現(xiàn)當(dāng)前函數(shù)是在執(zhí)行 CPU 密集型任務(wù),還是在等待資源。
實(shí)現(xiàn)方式很簡(jiǎn)單,就是在每次抓棧時(shí)通過(guò)下面的方式獲取下當(dāng)前線程的 CPUTime:
static uint64_t thread_cpu_time_nanos() { struct timespec t; clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t); return t.tv_sec * 1000000000LL + t.tv_nsec;}
static uint64_t thread_cpu_time_nanos() {
struct timespec t;
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t);
return t.tv_sec * 1000000000LL + t.tv_nsec;
}
Android 效果圖
iOS 效果圖
對(duì)象分配次數(shù)與大小
我們已借助對(duì)象分配的接口進(jìn)行棧捕獲,但并非每次對(duì)象分配時(shí)都會(huì)進(jìn)行棧捕獲,這會(huì)致使直接通過(guò)該接口采集的數(shù)據(jù)為抽樣結(jié)果,無(wú)法真實(shí)反映對(duì)象分配所需的內(nèi)存大小。實(shí)際上,我們只需在對(duì)象分配接口中做好數(shù)據(jù)統(tǒng)計(jì),隨后在每次進(jìn)行棧捕獲時(shí)(無(wú)論由何種斷點(diǎn)觸發(fā)),記錄當(dāng)前的線程級(jí)對(duì)象分配情況,與獲取線程的 CPU 時(shí)間的操作方式相同即可。
thread_local rheatrace::JavaObjectStat::ObjectStat stats;
void rheatrace::JavaObjectStat::onObjectAllocated(size_t b) { stats.objects++; stats.bytes += b;}
thread_local rheatrace::JavaObjectStat::ObjectStat stats;
void rheatrace::JavaObjectStat::onObjectAllocated(size_t b) {
stats.objects++;
stats.bytes += b;
}
Android 效果圖
iOS 效果圖
缺頁(yè)次數(shù)、上下文切換次數(shù)
和 CPUTime 類似,可以通過(guò) getrusage 來(lái)讀取線程級(jí)別的缺頁(yè)次數(shù)、上下文切換次數(shù)的信息。
struct rusage ru;if (getrusage(RUSAGE_THREAD, &ru) == 0) { r.mMajFlt = ru.ru_majflt; r.mNvCsw = ru.ru_nvcsw; r.mNivCsw = ru.ru_nivcsw;}
struct rusage ru;
if (getrusage(RUSAGE_THREAD, &ru) == 0) {
r.mMajFlt = ru.ru_majflt;
r.mNvCsw = ru.ru_nvcsw;
r.mNivCsw = ru.ru_nivcsw;
}
線程阻塞歸因
以主線程為例,線程阻塞歸因是監(jiān)控到主線程阻塞的時(shí)長(zhǎng),以及對(duì)應(yīng)的喚醒線程。目前阻塞歸因已經(jīng)包含 synchronized 鎖、Object.wait、Unsafe.park 3 中原因?qū)е碌木€程阻塞。通過(guò) hook 對(duì)應(yīng)的函數(shù)(hook 方案可以參考過(guò)往文章:重要升級(jí)!btrace 2.0 技術(shù)原理大揭秘)來(lái)記錄主線程的阻塞時(shí)長(zhǎng),同時(shí) hook 釋放鎖等操作,如果釋放的鎖是當(dāng)前主線程正在等待的鎖,那么就在釋放鎖時(shí)強(qiáng)制抓棧,且記錄下當(dāng)前釋放的目標(biāo)線程的 ID 用來(lái)關(guān)聯(lián)阻塞與釋放的關(guān)系?;驹砜梢詤⒖枷旅妫?/span>
static void *currentMainMonitor = nullptr;static uint64_t currentMainNano = 0;
void *Monitor_MonitorEnter(void *self, void *obj, bool trylock) {SHADOWHOOK_STACK_SCOPE(); if (rheatrace::isMainThread()) { rheatrace::ScopeSampling a(rheatrace::SamplingType::kMonitor, self); currentMainMonitor = obj; // 記錄當(dāng)前阻塞的鎖 currentMainNano = a.beginNano_; void *result = SHADOWHOOK_CALL_PREV(Monitor_MonitorEnter, self, obj, trylock); currentMainMonitor = nullptr; // 鎖已經(jīng)拿到,這里重置 return result; } ...}
bool Monitor_MonitorExit(void *self, void *obj) {SHADOWHOOK_STACK_SCOPE(); if (!rheatrace::isMainThread()) { if (currentMainMonitor == obj) { // 當(dāng)前釋放的鎖正式主線程等待的鎖 rheatrace::SamplingCollector::request(rheatrace::SamplingType::kUnlock, self, true, true, currentMainNano); // 強(qiáng)制抓棧,并通過(guò) currentMainNano 和主線程建立聯(lián)系A(chǔ)LOGX("Monitor_MonitorExit wakeup main lock %ld", currentMainNano); } } return SHADOWHOOK_CALL_PREV(Monitor_MonitorExit, self, obj);}
static void *currentMainMonitor = nullptr;
static uint64_t currentMainNano = 0;
void *Monitor_MonitorEnter(void *self, void *obj, bool trylock) {
SHADOWHOOK_STACK_SCOPE();
if (rheatrace::isMainThread()) {
rheatrace::ScopeSampling a(rheatrace::SamplingType::kMonitor, self);
currentMainMonitor = obj; // 記錄當(dāng)前阻塞的鎖
currentMainNano = a.beginNano_;
void *result = SHADOWHOOK_CALL_PREV(Monitor_MonitorEnter, self, obj, trylock);
currentMainMonitor = nullptr; // 鎖已經(jīng)拿到,這里重置
return result;
}
...
}
bool Monitor_MonitorExit(void *self, void *obj) {
SHADOWHOOK_STACK_SCOPE();
if (!rheatrace::isMainThread()) {
if (currentMainMonitor == obj) { // 當(dāng)前釋放的鎖正式主線程等待的鎖
rheatrace::SamplingCollector::request(rheatrace::SamplingType::kUnlock, self, true, true, currentMainNano); // 強(qiáng)制抓棧,并通過(guò) currentMainNano 和主線程建立聯(lián)系
ALOGX("Monitor_MonitorExit wakeup main lock %ld", currentMainNano);
}
}
return SHADOWHOOK_CALL_PREV(Monitor_MonitorExit, self, obj);
}
1.主線程等鎖,提示 wakeupby: 30657
2.輕松定位到 30657 線程相關(guān)代碼:
總結(jié)展望
以上主要闡述了 btrace 3.0 采用了將動(dòng)態(tài)插樁與同步抓棧相結(jié)合的新型 Trace 方案。該新型方案在使用體驗(yàn)和靈活性方面對(duì) btrace 進(jìn)行了一定程度的優(yōu)化。后續(xù),我們將持續(xù)對(duì) Trace 能力、拓展 Trace 采集場(chǎng)景以及生態(tài)建設(shè)進(jìn)行迭代與優(yōu)化,具體內(nèi)容如下:
- Trace 能力:在 Android 系統(tǒng)中支持 Native 層的 C/C++ Trace;在雙端均提供 GPU 等渲染層面的 Trace 信息。
- 使用場(chǎng)景:提供線上場(chǎng)景的 Trace 采集能力接入與使用方案,以助力發(fā)現(xiàn)并解決線上性能問(wèn)題。
- 生態(tài)建設(shè):圍繞 btrace 工具構(gòu)建自動(dòng)性能診斷能力。
- 提升性能和穩(wěn)定性:工具的性能和穩(wěn)定性優(yōu)化工作永無(wú)止境,仍需進(jìn)一步追求極致。
- 多端能力:在 Android、iOS 的基礎(chǔ)上,新增鴻蒙系統(tǒng)的支持,同時(shí)增加 Web 等跨平臺(tái)的 Trace 能力。
最后,歡迎大家移步 https://github.com/bytedance/btrace ,以進(jìn)一步了解 btrace 3.0 所帶來(lái)的全新體驗(yàn)。