Matrix-iOS 卡頓、內(nèi)存監(jiān)控 (一)

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ù)退火算法控制時間間隔

  1. 創(chuàng)建的子線程通過 while 使其成為常駐線程,直到主動執(zhí)行 stop 方法才會被銷毀。
  2. 獲取是否卡頓、卡頓類型以及針對不同卡頓原因進行不同的處理
  3. 其中,使用 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];
                }
  1. [self check]獲取卡頓類型
  2. 如果沒有卡頓,重置各種狀態(tài),包括控制退火算法的m_nIntervalTime
  3. 如果是主線程卡頓,會先檢測子線程數(shù)量是否過多,按照微信團隊的經(jīng)驗,線程數(shù)超出64個時會導致主線程卡頓,如果卡頓是由于線程多造成的,那么就沒必 要通過獲取主線程堆棧去找卡頓原因了(線程過多時 CPU 在切換線程上下文時,還會更新寄 存器,更新寄存器時需要尋址,而尋址的過程還會有較大的 CPU 消耗)
    否則,不是因為線程過多造成的卡頓,則更新最近最耗時的堆棧,并回到主線程寫入文件記錄
  4. 如果不是因為主線程卡頓(當單核 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. 如果沒有卡頓 則睡眠時間為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;
        ...
    }
  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)隊列中。有一個游標不斷的指向最近的堆棧。


image.png

微信的策略是每隔 50 毫秒獲取一次主線程堆棧,保存最近 20 個主線程堆棧。這個會增加 3% 的 CPU 占用,內(nèi)存占用可以忽略不計。

  1. 首先,堆棧信息通過 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)控用如下特征找出最近最耗時堆棧:

  1. 以棧頂函數(shù)為特征,認為棧頂函數(shù)相同的即整個堆棧是相同的;
  2. 取堆棧的間隔是相同的,堆棧的重復次數(shù)近似作為堆棧的調(diào)用耗時,重復越多,耗時越多;
  3. 重復次數(shù)相同的堆??赡芎苡卸鄠€,取最近的一個最耗時堆棧。
總結(jié)

根據(jù)代碼分析,Matrix 卡頓監(jiān)控主要通過常駐子線程來檢測runloop卡頓以及CPU使用率,使用退火算法優(yōu)化捕獲卡頓的效率防止連續(xù)捕獲相同的卡頓,并且通過保存最近的10次堆棧信息,獲取獲取最近最耗時堆棧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容