Matrix-iOS 卡顿监控
Matrix-iOS 内存监控
一、卡顿检测
Matrix-iOS 在addMonitorThread
方法中创建了一个子线程用来监控卡顿,子线程会执行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秒,间隔时间会受检测线程退火算法影响,按照斐波那契数列递增,直到没有卡顿时恢复1秒
for (int nCnt = 0; nCnt < m_nIntervalTime && !m_bStop; nCnt++) {
usleep(...)
}
// 控制线程退出的条件
if (m_bStop) {
break;
}
}
}
}
可以看出,方法中主要处理了两件事情,检测记录卡顿、根据退火算法控制时间间隔
- 创建的子线程通过 while 使其成为常驻线程,直到主动执行 stop 方法才会被销毁。
- 获取是否卡顿、卡顿类型以及针对不同卡顿原因进行不同的处理
- 其中,使用 usleep 方法进行时间间隔操作, g_CheckPeriodTime就是正常情况的时间间隔的值,退火算法影响的是 m_nIntervalTime,递增后检测卡顿的时间间隔就会不断变长。直到判定卡顿已结束,m_nIntervalTime 的 值会恢复成1。
卡顿处理实现:
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) { // 线程数量超过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]
获取卡顿类型 - 如果没有卡顿,重置各种状态,包括控制退火算法的m_nIntervalTime
- 如果是主线程卡顿,会先检测子线程数量是否过多,按照微信团队的经验,线程数超出64个时会导致主线程卡顿,如果卡顿是由于线程多造成的,那么就没必 要通过获取主线程堆栈去找卡顿原因了(线程过多时 CPU 在切换线程上下文时,还会更新寄 存器,更新寄存器时需要寻址,而寻址的过程还会有较大的 CPU 消耗)
否则,不是因为线程过多造成的卡顿,则更新最近最耗时的堆栈,并回到主线程写入文件记录 - 如果不是因为主线程卡顿(当单核 CPU 使用率超过 80%,就判定 CPU 占用过高。CPU 使用率过高,可能导 致 App 卡顿。) 记录日志文件
时间间隔控制实现:
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毫秒 忽略其他方法执行时间 总睡眠时间为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]
内部会使用斐波那契数列更新m_nIntervalTime
if (bIsSame) {
NSUInteger lastTimeInterval = m_nIntervalTime;
m_nIntervalTime = m_nLastTimeInterval + m_nIntervalTime;
m_nLastTimeInterval = lastTimeInterval;
...
} else {
m_nIntervalTime = 1;
...
}
- 这里还会每50毫秒获取一次主线程堆栈信息以及所有线程数量(这个会增加 3% 的 CPU 占用,内存占用可以忽略不计)
卡顿检测实现:
主线程卡顿检测包含 runloop 卡顿检测,以及 cpu使用率检测
runloop 卡顿检测 :
BOOL tmp_g_bRun = g_bRun; // runloop是否在运行中 kCFRunLoopBeforeWaiting、kCFRunLoopExit runloop退出或者休眠时 g_bRun = NO
struct timeval tmp_g_tvRun = g_tvRun; // runloop最后触发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) { // 如果当前处于后台状态
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 检测卡顿,网络上有很多类似的实现,大同小异。创建Observe监听主线程runloop状态,并在进入每个状态的时候记录时间,然后由于子线程时间控制默认为1秒,也就是每1秒检测一下,判断最后记录的时间与当前时间的差值,超过阈值2秒就认为是卡顿。 这里还增加了是否是后台状态的判断。
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使用率累加。根据CPU使用率计算是否超过阈值。
主线程堆栈过滤: Matrix-iOS 卡顿监控里面有详细介绍
为了解决检测到卡顿时,获取的堆栈信息有延迟的问题,Matrix 卡顿监控通过主线程耗时堆栈提取来解决这个问题。
卡顿监控定时获取主线程堆栈,并将堆栈保存到内存的一个循环队列中。如下图,每间隔时间 t 获得一个堆栈,然后将堆栈保存到一个最大个数为 3 的循环队列中。有一个游标不断的指向最近的堆栈。
微信的策略是每隔 50 毫秒获取一次主线程堆栈,保存最近 20 个主线程堆栈。这个会增加 3% 的 CPU 占用,内存占用可以忽略不计。
- 首先,堆栈信息通过
WCMainThreadHandler
存储
static uintptr_t **g_mainThreadStackCycleArray; // 堆栈二维数组,相当于保存N个堆栈列表
static size_t *g_mainThreadStackCount; // 对应`g_mainThreadStackCycleArray`每个堆栈列表层级数
static uint64_t g_tailPoint; // 当前指向的Index 由于是循环数组 利用模运算 重复指向0 -> N 循环利用空间
static size_t *g_topStackAddressRepeatArray; // 对应`g_mainThreadStackCycleArray`每个堆栈列表重复次数 默认0次
文档中介绍,会保存最近20个主线程堆栈,但是代码中m_cycleArrayCount = 10
,也就是上面的三个数组长度都是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;
// 如果不一样 更新记录的最后一次调用栈
//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++;
}
// 根据栈顶地址 判断一天捕获次数是否超过阈值
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 卡顿监控用如下特征找出最近最耗时堆栈:
- 以栈顶函数为特征,认为栈顶函数相同的即整个堆栈是相同的;
- 取堆栈的间隔是相同的,堆栈的重复次数近似作为堆栈的调用耗时,重复越多,耗时越多;
- 重复次数相同的堆栈可能很有多个,取最近的一个最耗时堆栈。
总结
根据代码分析,Matrix 卡顿监控主要通过常驻子线程来检测runloop卡顿以及CPU使用率,使用退火算法优化捕获卡顿的效率防止连续捕获相同的卡顿,并且通过保存最近的10次堆栈信息,获取获取最近最耗时堆栈。