Matrix-iOS 卡顿、内存监控 (一)

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;
            }
        }
    }
}

可以看出,方法中主要处理了两件事情,检测记录卡顿、根据退火算法控制时间间隔

  1. 创建的子线程通过 while 使其成为常驻线程,直到主动执行 stop 方法才会被销毁。
  2. 获取是否卡顿、卡顿类型以及针对不同卡顿原因进行不同的处理
  3. 其中,使用 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];
                }
  1. [self check]获取卡顿类型
  2. 如果没有卡顿,重置各种状态,包括控制退火算法的m_nIntervalTime
  3. 如果是主线程卡顿,会先检测子线程数量是否过多,按照微信团队的经验,线程数超出64个时会导致主线程卡顿,如果卡顿是由于线程多造成的,那么就没必 要通过获取主线程堆栈去找卡顿原因了(线程过多时 CPU 在切换线程上下文时,还会更新寄 存器,更新寄存器时需要寻址,而寻址的过程还会有较大的 CPU 消耗)
    否则,不是因为线程过多造成的卡顿,则更新最近最耗时的堆栈,并回到主线程写入文件记录
  4. 如果不是因为主线程卡顿(当单核 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. 如果没有卡顿 则睡眠时间为1秒 在上面检测卡顿的方法中[self needFilter]内部会使用斐波那契数列更新m_nIntervalTime
    if (bIsSame) {
        NSUInteger lastTimeInterval = m_nIntervalTime;
        m_nIntervalTime = m_nLastTimeInterval + m_nIntervalTime;
        m_nLastTimeInterval = lastTimeInterval;
        ...
    } else {
        m_nIntervalTime = 1;
        ...
    }
  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 的循环队列中。有一个游标不断的指向最近的堆栈。


image.png

微信的策略是每隔 50 毫秒获取一次主线程堆栈,保存最近 20 个主线程堆栈。这个会增加 3% 的 CPU 占用,内存占用可以忽略不计。

  1. 首先,堆栈信息通过 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 卡顿监控用如下特征找出最近最耗时堆栈:

  1. 以栈顶函数为特征,认为栈顶函数相同的即整个堆栈是相同的;
  2. 取堆栈的间隔是相同的,堆栈的重复次数近似作为堆栈的调用耗时,重复越多,耗时越多;
  3. 重复次数相同的堆栈可能很有多个,取最近的一个最耗时堆栈。
总结

根据代码分析,Matrix 卡顿监控主要通过常驻子线程来检测runloop卡顿以及CPU使用率,使用退火算法优化捕获卡顿的效率防止连续捕获相同的卡顿,并且通过保存最近的10次堆栈信息,获取获取最近最耗时堆栈。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容