Matrix-iOS 卡頓監(jiān)控
Matrix-iOS 內(nèi)存監(jiān)控
一、卡頓檢測
Matrix-iOS 在addMonitorThread方法中創(chuàng)建了一個子線程用來監(jiān)控卡頓,子線程會執(zhí)行threadProc方法
- (void)threadProc
{
g_matrix_block_monitor_dumping_thread_id = pthread_mach_thread_np(pthread_self());
if (m_firstSleepTime) {
sleep(m_firstSleepTime);
m_firstSleepTime = 0;
}
if (g_filterSameStack) {
m_stackHandler = [[WCFilterStackHandler alloc] init];
}
while (YES) {
@autoreleasepool {
if (g_bMonitor) {
// 檢查是否卡頓,以及卡頓原因
...
// 針對不同卡頓原因進行不同的處理
...
}
// 時間間隔處理,檢測時間間隔正常情況是1秒,間隔時間會受檢測線程退火算法影響,按照斐波那契數(shù)列遞增,直到?jīng)]有卡頓時恢復1秒
for (int nCnt = 0; nCnt < m_nIntervalTime && !m_bStop; nCnt++) {
usleep(...)
}
// 控制線程退出的條件
if (m_bStop) {
break;
}
}
}
}
可以看出,方法中主要處理了兩件事情,檢測記錄卡頓、根據(jù)退火算法控制時間間隔
- 創(chuàng)建的子線程通過 while 使其成為常駐線程,直到主動執(zhí)行 stop 方法才會被銷毀。
- 獲取是否卡頓、卡頓類型以及針對不同卡頓原因進行不同的處理
- 其中,使用 usleep 方法進行時間間隔操作, g_CheckPeriodTime就是正常情況的時間間隔的值,退火算法影響的是 m_nIntervalTime,遞增后檢測卡頓的時間間隔就會不斷變長。直到判定卡頓已結(jié)束,m_nIntervalTime 的 值會恢復成1。
卡頓處理實現(xiàn):
EDumpType dumpType = [self check]; // 檢測卡頓
if (m_bStop) { // 用來控制是否退出卡頓檢測
break;
}
BM_SAFE_CALL_SELECTOR_NO_RETURN(_delegate,
@selector(onBlockMonitor:enterNextCheckWithDumpType:),
onBlockMonitor:self enterNextCheckWithDumpType:dumpType);
if (dumpType != EDumpType_Unlag) {
if (EDumpType_BackgroundMainThreadBlock == dumpType ||
EDumpType_MainThreadBlock == dumpType) { // 前/后臺 主線程卡頓
if (g_CurrentThreadCount > 64) { // 線程數(shù)量超過64個 認為線程過多造成卡頓 不記錄主線程堆棧
dumpType = EDumpType_BlockThreadTooMuch;
[self dumpFileWithType:dumpType];
} else {
EFilterType filterType = [self needFilter]; // 過濾堆棧信息,判斷是否有重復堆棧信息等... 避免重復記錄
if (filterType == EFilterType_None) { // 沒有相同堆棧信息
if (g_MainThreadHandle) {
// 獲得最近最耗時的堆棧
g_PointMainThreadArray = [m_pointMainThreadHandler getPointStackCursor];
m_potenHandledLagFile = [self dumpFileWithType:dumpType];
} else {
m_potenHandledLagFile = [self dumpFileWithType:dumpType];
}
} else {
...
}
}
} else {
m_potenHandledLagFile = [self dumpFileWithType:dumpType];
}
} else {
[self resetStatus];
}
-
[self check]獲取卡頓類型 - 如果沒有卡頓,重置各種狀態(tài),包括控制退火算法的m_nIntervalTime
- 如果是主線程卡頓,會先檢測子線程數(shù)量是否過多,按照微信團隊的經(jīng)驗,線程數(shù)超出64個時會導致主線程卡頓,如果卡頓是由于線程多造成的,那么就沒必 要通過獲取主線程堆棧去找卡頓原因了(線程過多時 CPU 在切換線程上下文時,還會更新寄 存器,更新寄存器時需要尋址,而尋址的過程還會有較大的 CPU 消耗)
否則,不是因為線程過多造成的卡頓,則更新最近最耗時的堆棧,并回到主線程寫入文件記錄 - 如果不是因為主線程卡頓(當單核 CPU 使用率超過 80%,就判定 CPU 占用過高。CPU 使用率過高,可能導 致 App 卡頓。) 記錄日志文件
時間間隔控制實現(xiàn):
for (int nCnt = 0; nCnt < m_nIntervalTime && !m_bStop; nCnt++) {
if (g_MainThreadHandle && g_bMonitor) {
// intervalCount = 20
int intervalCount = g_CheckPeriodTime / g_PerStackInterval;
if (intervalCount <= 0) {
usleep(g_CheckPeriodTime);
} else {
// intervalCount = 20 g_PerStackInterval = 50毫秒 忽略其他方法執(zhí)行時間 總睡眠時間為1秒
for (int index = 0; index < intervalCount && !m_bStop; index++) {
usleep(g_PerStackInterval);
size_t stackBytes = sizeof(uintptr_t) * g_StackMaxCount;
uintptr_t *stackArray = (uintptr_t *) malloc(stackBytes);
if (stackArray == NULL) {
continue;
}
__block size_t nSum = 0;
memset(stackArray, 0, stackBytes);
[WCGetMainThreadUtil getCurrentMainThreadStack:^(NSUInteger pc) {
stackArray[nSum] = (uintptr_t) pc;
nSum++;
}
withMaxEntries:g_StackMaxCount
withThreadCount:g_CurrentThreadCount];
[m_pointMainThreadHandler addThreadStack:stackArray andStackCount:nSum];
}
}
} else {
usleep(g_CheckPeriodTime);
}
}
g_CheckPeriodTime == 1秒 g_PerStackInterval == 50毫秒
- 如果沒有卡頓 則睡眠時間為1秒 在上面檢測卡頓的方法中
[self needFilter]內(nèi)部會使用斐波那契數(shù)列更新m_nIntervalTime
if (bIsSame) {
NSUInteger lastTimeInterval = m_nIntervalTime;
m_nIntervalTime = m_nLastTimeInterval + m_nIntervalTime;
m_nLastTimeInterval = lastTimeInterval;
...
} else {
m_nIntervalTime = 1;
...
}
- 這里還會每50毫秒獲取一次主線程堆棧信息以及所有線程數(shù)量(這個會增加 3% 的 CPU 占用,內(nèi)存占用可以忽略不計)
卡頓檢測實現(xiàn):
主線程卡頓檢測包含 runloop 卡頓檢測,以及 cpu使用率檢測
runloop 卡頓檢測 :
BOOL tmp_g_bRun = g_bRun; // runloop是否在運行中 kCFRunLoopBeforeWaiting、kCFRunLoopExit runloop退出或者休眠時 g_bRun = NO
struct timeval tmp_g_tvRun = g_tvRun; // runloop最后觸發(fā)Observer的時間
struct timeval tvCur;
gettimeofday(&tvCur, NULL); // 當前時間
unsigned long long diff = [WCBlockMonitorMgr diffTime:&tmp_g_tvRun endTime:&tvCur]; // 時間差
#if !TARGET_OS_OSX // ios
struct timeval tmp_g_tvSuspend = g_tvSuspend; // 程序掛起時間
if (__timercmp(&tmp_g_tvSuspend, &tmp_g_tvRun, >)) { // 對比運行時間 運行后暫停 直接返回未卡頓
MatrixInfo(@"suspend after run, filter");
return EDumpType_Unlag;
}
#endif
m_blockDiffTime = 0;
// runloop運行中 && 運行時間存在 && 運行時間 < 當前時間 && 運行時間超過閾值 g_RunLoopTimeOut = 2秒
if (tmp_g_bRun && tmp_g_tvRun.tv_sec && tmp_g_tvRun.tv_usec && __timercmp(&tmp_g_tvRun, &tvCur, <) && diff > g_RunLoopTimeOut) {
m_blockDiffTime = tvCur.tv_sec - tmp_g_tvRun.tv_sec;
#if !TARGET_OS_OSX
MatrixInfo(@"check run loop time out %u %ld bRun %d runloopActivity %lu block diff time %llu",
g_RunLoopTimeOut, (long) m_currentState, g_bRun, g_runLoopActivity, diff);
if (g_bBackgroundLaunch) { // Background Fetch 直接返回未卡頓
MatrixInfo(@"background launch, filter");
return EDumpType_Unlag;
}
if (m_currentState == UIApplicationStateBackground) { // 如果當前處于后臺狀態(tài)
if (g_enterBackground.tv_sec != 0 || g_enterBackground.tv_usec != 0) { // 判斷處于后臺的時間
unsigned long long enterBackgroundTime = [WCBlockMonitorMgr diffTime:&g_enterBackground endTime:&tvCur];
if (__timercmp(&g_enterBackground, &tvCur, <) && (enterBackgroundTime > APP_SHOULD_SUSPEND)) {
MatrixInfo(@"may mistake block %lld", enterBackgroundTime);
return EDumpType_Unlag; // 處于后臺的時間超過3分鐘(iOS7以后后臺只有3分鐘處理任務時間,可多次申請最多10分鐘)
}
}
return EDumpType_BackgroundMainThreadBlock; // 返回后臺主線程卡頓
}
#endif
return EDumpType_MainThreadBlock; // 返回主線程卡頓
}
runloop 檢測卡頓,網(wǎng)絡上有很多類似的實現(xiàn),大同小異。創(chuàng)建Observe監(jiān)聽主線程runloop狀態(tài),并在進入每個狀態(tài)的時候記錄時間,然后由于子線程時間控制默認為1秒,也就是每1秒檢測一下,判斷最后記錄的時間與當前時間的差值,超過閾值2秒就認為是卡頓。 這里還增加了是否是后臺狀態(tài)的判斷。
cpu使用率檢測:
float cpuUsage = [WCCPUHandler getCurrentCpuUsage];
if ([_monitorConfigHandler getShouldPrintCPUUsage] && cpuUsage > 40.0f) {
MatrixInfo(@"mb[%f]", cpuUsage);;
}
if (cpuUsage > 100.0f) {
MatrixInfo(@"check cpu over usage 100.0f, %f", cpuUsage);
}
if (m_bTrackCPU) {
unsigned long long checkPeriod = [WCBlockMonitorMgr diffTime:&g_lastCheckTime endTime:&tvCur];
gettimeofday(&g_lastCheckTime, NULL);
if ([m_cpuHandler cultivateCpuUsage:cpuUsage periodTime:(float)checkPeriod / 1000000]) {
MatrixInfo(@"exceed cpu average usage");
BM_SAFE_CALL_SELECTOR_NO_RETURN(_delegate, @selector(onBlockMonitorIntervalCPUTooHigh:), onBlockMonitorIntervalCPUTooHigh:self)
if ([_monitorConfigHandler getShouldGetCPUIntervalHighLog]) {
return EDumpType_CPUIntervalHigh;
}
}
if (cpuUsage > g_CPUUsagePercent) {
MatrixInfo(@"check cpu over usage dump %f", cpuUsage);
BM_SAFE_CALL_SELECTOR_NO_RETURN(_delegate, @selector(onBlockMonitorCurrentCPUTooHigh:), onBlockMonitorCurrentCPUTooHigh:self)
if ([_monitorConfigHandler getShouldGetCPUHighLog]) {
return EDumpType_CPUBlock;
}
}
}
[WCBlockMonitorMgr diffTime:&g_lastCheckTime endTime:&tvCur] 遍歷所有線程,通過thread_info獲取線程信息,將所有線程CPU使用率累加。根據(jù)CPU使用率計算是否超過閾值。
主線程堆棧過濾: Matrix-iOS 卡頓監(jiān)控里面有詳細介紹
為了解決檢測到卡頓時,獲取的堆棧信息有延遲的問題,Matrix 卡頓監(jiān)控通過主線程耗時堆棧提取來解決這個問題。
卡頓監(jiān)控定時獲取主線程堆棧,并將堆棧保存到內(nèi)存的一個循環(huán)隊列中。如下圖,每間隔時間 t 獲得一個堆棧,然后將堆棧保存到一個最大個數(shù)為 3 的循環(huán)隊列中。有一個游標不斷的指向最近的堆棧。

微信的策略是每隔 50 毫秒獲取一次主線程堆棧,保存最近 20 個主線程堆棧。這個會增加 3% 的 CPU 占用,內(nèi)存占用可以忽略不計。
- 首先,堆棧信息通過
WCMainThreadHandler存儲
static uintptr_t **g_mainThreadStackCycleArray; // 堆棧二維數(shù)組,相當于保存N個堆棧列表
static size_t *g_mainThreadStackCount; // 對應`g_mainThreadStackCycleArray`每個堆棧列表層級數(shù)
static uint64_t g_tailPoint; // 當前指向的Index 由于是循環(huán)數(shù)組 利用模運算 重復指向0 -> N 循環(huán)利用空間
static size_t *g_topStackAddressRepeatArray; // 對應`g_mainThreadStackCycleArray`每個堆棧列表重復次數(shù) 默認0次
文檔中介紹,會保存最近20個主線程堆棧,但是代碼中m_cycleArrayCount = 10,也就是上面的三個數(shù)組長度都是10。并且單個堆棧信息層級最大為100
在 needFilter中通過以上存儲信息,過濾重復的堆棧信息
- (EFilterType)needFilter
{
BOOL bIsSame = NO;
static std::vector<NSUInteger> vecCallStack(300);
__block NSUInteger nSum = 0;
__block NSUInteger stackFeat = 0; // use the top stack address;
// 獲取當前主線程堆棧信息
if (g_MainThreadHandle) {
nSum = [m_pointMainThreadHandler getLastMainThreadStackCount];
uintptr_t *stack = [m_pointMainThreadHandler getLastMainThreadStack];
if (stack) {
for (size_t i = 0; i < nSum; i++) {
vecCallStack[i] = stack[i];
stackFeat += stack[i];
}
stackFeat = kssymbolicate_symboladdress(stack[0]);
} else {
nSum = 0;
}
} else {
[WCGetMainThreadUtil getCurrentMainThreadStack:^(NSUInteger pc) {
if (nSum < WXGBackTraceMaxEntries) {
vecCallStack[nSum] = pc;
stackFeat += pc;
}
if (nSum == 0) {
stackFeat = kssymbolicate_symboladdress(pc);
}
nSum++;
}];
}
// 堆棧層級太少 直接返回
if (nSum <= 1) {
MatrixInfo(@"filter meaningless stack");
return EFilterType_Meaningless;
}
// 判斷堆棧是否與之前最后記錄的一樣
if (nSum == m_lastMainThreadStackCount) {
NSUInteger index = 0;
for (index = 0; index < nSum; index++) {
if (vecCallStack[index] != m_vecLastMainThreadCallStack[index]) {
break;
}
}
if (index == nSum) {
bIsSame = YES;
}
}
if (bIsSame) {
// 如果堆棧記錄與之前一樣 則使用退火算法,修改檢測時間間隔 返回 EFilterType_Annealing
NSUInteger lastTimeInterval = m_nIntervalTime;
m_nIntervalTime = m_nLastTimeInterval + m_nIntervalTime;
m_nLastTimeInterval = lastTimeInterval;
MatrixInfo(@"call stack same timeinterval = %lu", (unsigned long) m_nIntervalTime);
return EFilterType_Annealing;
} else {
m_nIntervalTime = 1;
m_nLastTimeInterval = 1;
// 如果不一樣 更新記錄的最后一次調(diào)用棧
//update last call stack
m_vecLastMainThreadCallStack.clear();
m_lastMainThreadStackCount = 0;
for (NSUInteger index = 0; index < nSum; index++) {
m_vecLastMainThreadCallStack.push_back(vecCallStack[index]);
m_lastMainThreadStackCount++;
}
// 根據(jù)棧頂?shù)刂? 判斷一天捕獲次數(shù)是否超過閾值
if (g_filterSameStack) {
NSUInteger repeatCnt = [m_stackHandler addStackFeat:stackFeat];
if (repeatCnt > g_triggerdFilterSameCnt) {
MatrixInfo(@"call stack appear too much today, repeat conut:[%u]",(uint32_t) repeatCnt);
return EFilterType_TrigerByTooMuch;
}
}
MatrixInfo(@"call stack diff");
return EFilterType_None;
}
}
Matrix 卡頓監(jiān)控用如下特征找出最近最耗時堆棧:
- 以棧頂函數(shù)為特征,認為棧頂函數(shù)相同的即整個堆棧是相同的;
- 取堆棧的間隔是相同的,堆棧的重復次數(shù)近似作為堆棧的調(diào)用耗時,重復越多,耗時越多;
- 重復次數(shù)相同的堆??赡芎苡卸鄠€,取最近的一個最耗時堆棧。
總結(jié)
根據(jù)代碼分析,Matrix 卡頓監(jiān)控主要通過常駐子線程來檢測runloop卡頓以及CPU使用率,使用退火算法優(yōu)化捕獲卡頓的效率防止連續(xù)捕獲相同的卡頓,并且通過保存最近的10次堆棧信息,獲取獲取最近最耗時堆棧。