Matrix获取卡顿堆栈 (Point Stack)

4 阅读12分钟

matrix 是腾讯微信团队开源的一套移动端性能监控与分析框架,核心目标是帮助开发者定位、解决移动端(iOS/Android)应用的性能问题,是微信内部大规模验证过的成熟工具,本文通过阅读源码,详细介绍了针对卡顿日志获取的核心原理。

Matrix 通过周期性采集主线程堆栈并保存在循环数组中,在检测到卡顿时,使用 Point Stack 算法找出最有可能导致卡顿的堆栈。

核心思想

时间流逝
   ↓
每 50ms 采集一次主线程堆栈
   ↓
保存到循环数组(固定大小,如 20 个)
   ↓
检测到卡顿时
   ↓
分析循环数组,找出 Point Stack(最可能导致卡顿的堆栈)
   ↓
生成卡顿报告

设计目标

目标实现方式
及时性每 50ms 采集一次,不错过卡顿过程
完整性保存一个周期内的所有堆栈(通常 20 个)
准确性通过算法找出真正导致卡顿的堆栈
高效性固定大小循环数组,避免内存膨胀
低开销CPU 占用 < 3%,不影响用户体验

核心时间参数

三个关键参数

// 1. RunLoop 超时阈值(卡顿判定阈值)
static useconds_t g_RunLoopTimeOut = 2000000;  // 2000ms = 2秒
// 作用:超过此时间判定为卡顿

// 2. 检查周期(单次采集周期)
static useconds_t g_CheckPeriodTime = 1000000;  // 1000ms = 1秒
// 作用:一轮堆栈采集的总时间,通常为超时阈值的一半

// 3. 堆栈采集间隔
static useconds_t g_PerStackInterval = 50000;  // 50ms
// 作用:每次堆栈采集之间的时间间隔

参数关系

┌────────────────────────────────────────────────┐
│ g_RunLoopTimeOut (2秒) - 卡顿判定阈值          │
└────────────────────────────────────────────────┘
                    │
                    ├─ 一半
                    ↓
┌────────────────────────────────────────────────┐
│ g_CheckPeriodTime (1秒) - 检查周期             │
└────────────────────────────────────────────────┘
                    │
                    ├─ 除以
                    ↓
┌────────────────────────────────────────────────┐
│ g_PerStackInterval (50ms) - 堆栈间隔           │
└────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────┐
│ g_MainThreadCount = 1000ms / 50ms = 20         │
│ (循环数组大小 = 一个周期内采集的堆栈数量)      │
└────────────────────────────────────────────────┘

时间轴示意

时间线(以2秒卡顿为例):

T=0ms      开始监控
|
T=50ms     采集第1个堆栈 ← S0
|
T=100ms    采集第2个堆栈 ← S1
|
T=150ms    采集第3个堆栈 ← S2
|
...
|
T=950ms    采集第19个堆栈 ← S18
|
T=1000ms   采集第20个堆栈 ← S19  ← 完成一轮采集
|          ↓
|          检查是否卡顿(检查RunLoop执行时间)
|          如果未卡顿,进入下一轮采集
|
T=1050ms   采集第21个堆栈 ← S20(覆盖S0)
|
...
|
T=2000ms   检查发现 RunLoop 执行超过 2秒
|          ↓
|          触发卡顿检测
|          ↓
|          分析循环数组中的 20 个堆栈
|          ↓
|          找出 Point Stack(最可能导致卡顿的堆栈)
|          ↓
|          生成卡顿报告

堆栈获取流程

整体流程图

┌──────────────────────────────────────────┐
│ 监控线程主循环                            │
│ while (YES) {                            │
│   check();            // 检测卡顿         │recordCurrentStack(); // 采集堆栈       │
│ }                                        │
└──────────────────────────────────────────┘
                    ↓
┌──────────────────────────────────────────┐
│ recordCurrentStack() 方法                │
│                                          │
│ 外层循环:遍历检查周期                    │
│   nTotalCnt = m_nIntervalTime /         │
│               g_CheckPeriodTime         │
│   通常 = 1000ms / 1000ms = 1次          │
│                                          │
│   内层循环:在一个周期内多次采集          │
│     intervalCount = g_CheckPeriodTime / │
│                     g_PerStackInterval  │
│     通常 = 1000ms / 50ms = 20次         │
│                                          │
│     每次循环:                            │
│       1. usleep(50ms)  // 等待          │2. 获取主线程堆栈                   │
│       3. 添加到循环数组                   │
└──────────────────────────────────────────┘

详细步骤

步骤 1:初始化
// 在 WCBlockMonitorMgr 的 start 方法中
- (void)start {
    // 计算循环数组大小
    g_MainThreadCount = g_CheckPeriodTime / g_PerStackInterval;
    // 例如:1000ms / 50ms = 20
    
    // 创建主线程堆栈处理器
    m_pointMainThreadHandler = [[WCMainThreadHandler alloc] 
                                 initWithCycleArrayCount:g_MainThreadCount];
    
    // g_MainThreadCount = 20
    // 意味着循环数组可以保存 20 个堆栈
}
步骤 2:周期性采集
- (void)recordCurrentStack {
    // ================================================================
    // 外层循环:决定执行几个检查周期
    // ================================================================
    // 正常情况:m_nIntervalTime = 1000ms
    // 退火情况:m_nIntervalTime = 2000ms, 3000ms, 5000ms...
    unsigned long nTotalCnt = m_nIntervalTime / g_CheckPeriodTime;
    
    for (int nCnt = 0; nCnt < nTotalCnt && !m_bStop; nCnt++) {
        // 记录本轮开始时间(用于检测系统挂起)
        gettimeofday(&m_recordStackTime, NULL);
        
        if (g_MainThreadHandle) {
            // ========================================================
            // 内层循环:在一个检查周期内多次采集
            // ========================================================
            // intervalCount = 1000ms / 50ms = 20
            int intervalCount = g_CheckPeriodTime / g_PerStackInterval;
            
            for (int index = 0; index < intervalCount && !m_bStop; index++) {
                // 1️⃣ 等待 50ms
                usleep(g_PerStackInterval);  // 50000 微秒 = 50ms
                
                // 2️⃣ 分配内存
                size_t stackBytes = sizeof(uintptr_t) * g_StackMaxCount;
                uintptr_t *stackArray = (uintptr_t *)malloc(stackBytes);
                if (stackArray == NULL) {
                    continue;  // 内存分配失败,跳过本次
                }
                
                // 3️⃣ 初始化
                __block size_t nSum = 0;
                memset(stackArray, 0, stackBytes);
                
                // 4️⃣ 获取主线程堆栈
                [WCGetMainThreadUtil
                 getCurrentMainThreadStack:^(NSUInteger pc) {
                     stackArray[nSum] = (uintptr_t)pc;  // 保存程序计数器
                     nSum++;
                 }
                 withMaxEntries:g_StackMaxCount        // 最大100个栈帧
                 withThreadCount:g_CurrentThreadCount];
                
                // 5️⃣ 添加到循环数组
                [m_pointMainThreadHandler addThreadStack:stackArray 
                                           andStackCount:nSum];
                // 注意:stackArray 的所有权转移给 m_pointMainThreadHandler
            }
        }
        
        // ============================================================
        // 检测是否被系统挂起
        // ============================================================
        struct timeval tvCur;
        gettimeofday(&tvCur, NULL);
        unsigned long long diff = [WCBlockMonitorMgr diffTime:&m_recordStackTime 
                                                      endTime:&tvCur];
        
        if (diff > DETECTION_THREAD_JUDGE_SUSPEND_THRESHOLD) {
            // 实际消耗时间 > 10秒,说明被挂起了
            gettimeofday(&g_tvRun, NULL);  // 更新时间,避免误报
            MatrixInfo(@"挂起后运行,差值 %llu", diff);
            return;
        }
    }
}
步骤 3:获取主线程堆栈(底层实现)
// WCGetMainThreadUtil 内部使用 backtrace
+ (void)getCurrentMainThreadStack:(StackCallback)callback 
                   withMaxEntries:(size_t)maxEntries
                  withThreadCount:(NSUInteger)threadCount {
    // 1. 获取主线程
    thread_t mainThread = pthread_mach_thread_np(pthread_main_thread_np());
    
    // 2. 暂停主线程(非常短暂,微秒级)
    thread_suspend(mainThread);
    
    // 3. 获取线程状态
    _STRUCT_MCONTEXT machineContext;
    mach_msg_type_number_t state_count = THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(mainThread,
                                        THREAD_STATE,
                                        (thread_state_t)&machineContext.__ss,
                                        &state_count);
    
    // 4. 回溯堆栈
    if (kr == KERN_SUCCESS) {
        uintptr_t backtraceBuffer[maxEntries];
        size_t backtraceLength = ksbt_backtraceLength(&machineContext);
        
        // 遍历堆栈帧
        for (size_t i = 0; i < backtraceLength && i < maxEntries; i++) {
            uintptr_t pc = ksbt_framePointer(&machineContext, i);
            callback(pc);  // 回调传递每个栈帧的地址
        }
    }
    
    // 5. 恢复主线程
    thread_resume(mainThread);
}

循环数组存储机制

数据结构设计

@interface WCMainThreadHandler {
    // ================================================================
    // 循环数组配置
    // ================================================================
    int m_cycleArrayCount;  // 数组大小,例如 20
    
    // ================================================================
    // 循环数组(核心存储结构)
    // ================================================================
    uintptr_t **m_mainThreadStackCycleArray;  // 二维数组
    // 第一维:堆栈索引 [0, 19]
    // 第二维:堆栈地址数组 uintptr_t[]
    
    size_t *m_mainThreadStackCount;  // 每个堆栈的深度
    // 例如:[50, 48, 52, ..., 45]
    
    uint64_t m_tailPoint;  // 尾指针,指向下一个写入位置
    
    // ================================================================
    // 分析数据(用于 Point Stack 算法)
    // ================================================================
    size_t *m_topStackAddressRepeatArray;  // 栈顶地址连续重复次数
    // 例如:[0, 1, 2, 0, 1, 0, ...]
    
    int *m_mainThreadStackRepeatCountArray;  // Point Stack 地址总重复次数
    // 动态分配,在找到 Point Stack 后创建
}

循环数组可视化

初始化状态(m_cycleArrayCount = 5):

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │NULL │NULL │NULL │NULL │NULL │
       └─────┴─────┴─────┴─────┴─────┘
          ↑
     m_tailPoint = 0


添加第 1 个堆栈(S0):

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │ S0  │NULL │NULL │NULL │NULL │
       └─────┴─────┴─────┴─────┴─────┘
               ↑
          m_tailPoint = 1


添加第 2-5 个堆栈:

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │ S0  │ S1  │ S2  │ S3  │ S4  │
       └─────┴─────┴─────┴─────┴─────┘
          ↑
     m_tailPoint = 0 (回绕)


添加第 6 个堆栈(S5,覆盖 S0):

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │ S5  │ S1  │ S2  │ S3  │ S4  │
       └─────┴─────┴─────┴─────┴─────┘
               ↑
          m_tailPoint = 1

时间顺序:S1 → S2 → S3 → S4 → S5(最新)

添加堆栈的实现

- (void)addThreadStack:(uintptr_t *)stackArray 
         andStackCount:(size_t)stackCount {
    if (stackArray == NULL) {
        return;
    }
    
    pthread_mutex_lock(&m_threadLock);
    
    // ================================================================
    // 1. 将堆栈写入循环数组
    // ================================================================
    
    // 如果当前位置已有堆栈,先释放旧的
    if (m_mainThreadStackCycleArray[m_tailPoint] != NULL) {
        free(m_mainThreadStackCycleArray[m_tailPoint]);
    }
    
    // 保存新堆栈
    m_mainThreadStackCycleArray[m_tailPoint] = stackArray;
    m_mainThreadStackCount[m_tailPoint] = stackCount;
    
    // ================================================================
    // 2. 统计栈顶地址连续重复次数(核心!)
    // ================================================================
    
    // 计算上一个位置的索引
    uint64_t lastTailPoint = (m_tailPoint + m_cycleArrayCount - 1) % m_cycleArrayCount;
    
    // 获取上一个堆栈的栈顶地址
    uintptr_t lastTopStack = 0;
    if (m_mainThreadStackCycleArray[lastTailPoint] != NULL) {
        lastTopStack = m_mainThreadStackCycleArray[lastTailPoint][0];
    }
    
    // 获取当前堆栈的栈顶地址
    uintptr_t currentTopStackAddr = stackArray[0];
    
    // 比较栈顶地址
    if (lastTopStack == currentTopStackAddr) {
        // 栈顶地址相同,累加重复次数
        size_t lastRepeatCount = m_topStackAddressRepeatArray[lastTailPoint];
        m_topStackAddressRepeatArray[m_tailPoint] = lastRepeatCount + 1;
    } else {
        // 栈顶地址不同,重置重复次数
        m_topStackAddressRepeatArray[m_tailPoint] = 0;
    }
    
    // ================================================================
    // 3. 移动尾指针
    // ================================================================
    
    m_tailPoint = (m_tailPoint + 1) % m_cycleArrayCount;
    
    pthread_mutex_unlock(&m_threadLock);
}

栈顶地址重复次数统计示例

假设连续采集到以下堆栈(简化为栈顶地址):

时间: T0   T50  T100 T150 T200 T250 T300 T350 T400
索引:  0    1    2    3    4    5    6    7    8
堆栈: S0   S1   S2   S3   S4   S5   S6   S7   S8
栈顶: A    B    C    C    C    C    C    D    D

m_topStackAddressRepeatArray 的值:
     [0,   0,   0,   1,   2,   3,   4,   0,   1]
      ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
      A    B    C   CCCCD   D重
     首次  首次 首次  复2345  首次  复2

分析:
- S6(索引6)的栈顶地址 C 连续重复了 4 次(从 S2S6- 说明主线程在函数 C 上停留了 5 × 50ms = 250ms
- S6 就是最有可能导致卡顿的堆栈(Point Stack

什么是 Point Stack?

Point Stack(关键堆栈) 是指在一个检查周期内,最有可能导致卡顿的主线程堆栈

一、核心数据结构

1. 循环数组配置

变量名类型说明
m_cycleArrayCountint循环数组大小(例如:20)
m_tailPointuint64_t循环数组尾指针,指向下一个写入位置
pthread_mutex_tm_threadLock线程锁,保护循环数组的并发访问

循环数组原理:

数组大小 = 检查周期 / 堆栈间隔
例如:1000ms / 50ms = 20

索引:    0    1    2    3    4    ...   19
       ┌────┬────┬────┬────┬────┬ ─ ─ ┬────┐
堆栈:   │ S0 │ S1 │ S2 │    │    │     │    │
       └────┴────┴────┴─▲──┴────┴ ─ ─ ┴────┘
                        │
                   m_tailPoint

当数组满时,从头开始覆盖(FIFO)

2. 堆栈存储数组(二维数组)

变量名类型维度说明
m_mainThreadStackCycleArrayuintptr_t **[cycleArrayCount][stackDepth]堆栈地址二维数组
m_mainThreadStackCountsize_t *[cycleArrayCount]每个堆栈的深度数组

数据结构示意:

m_mainThreadStackCycleArray:
  [0] → [0x1000, 0x2000, 0x3000, ...]  // 第0个堆栈,深度=3
  [1] → [0x1000, 0x2000, 0x3000, ...]  // 第1个堆栈,深度=3
  [2] → [0x1000, 0x2000, 0x4000, ...]  // 第2个堆栈,深度=3
  ...
  [19] → NULL                          // 尚未写入

m_mainThreadStackCount:
  [0] = 3   // 第0个堆栈深度
  [1] = 3   // 第1个堆栈深度
  [2] = 3   // 第2个堆栈深度
  ...
  [19] = 0  // 尚未写入

3. 栈顶重复次数数组

变量名类型说明
m_topStackAddressRepeatArraysize_t *每个堆栈的栈顶地址连续重复次数

用途: 找出 Point Stack(栈顶重复次数最多的堆栈)

数据示例:

假设连续采集的堆栈栈顶地址:
索引:    0      1      2      3      4
栈顶:    A      A      A      B      B

m_topStackAddressRepeatArray:
       [0]    [1]    [2]    [3]    [4]
       0      1      2      0      1

解释:
- 索引0: 第一次出现A,重复0- 索引1: 第二次出现A(与前一个相同),重复1- 索引2: 第三次出现A(与前一个相同),重复2- 索引3: 出现B(改变了),重复0- 索引4: 第二次出现B(与前一个相同),重复1次

结果:索引2的重复次数最多(2次),所以索引2Point Stack

4. Point Stack地址重复次数数组

变量名类型说明
m_mainThreadStackRepeatCountArrayint *Point Stack中每个地址的总重复次数(动态分配)

用途: 统计 Point Stack 中每个地址在所有堆栈中的总出现次数,识别热点函数

数据示例:

假设有4个堆栈,Point Stack是索引2Stack 0:        Stack 1:        Stack 2(Point):  Stack 3:
0x1000          0x1000          0x1000          0x1000
0x2000          0x2000          0x2000          0x2000
0x3000          0x3000          0x3000          0x4000
0x4000          0x5000          0x6000

Point Stack (索引2) 的地址:
  [0] = 0x1000
  [1] = 0x2000
  [2] = 0x3000

统计结果 m_mainThreadStackRepeatCountArray:
  [0] = 4   // 0x1000 在4个堆栈中都出现
  [1] = 4   // 0x2000 在4个堆栈中都出现
  [2] = 3   // 0x3000 在3个堆栈中出现

符号化后:
  [0] main           (4次) ← 所有堆栈都有,基础函数
  [1] viewDidLoad    (4次) ← 所有堆栈都有,入口函数
  [2] heavyWork      (3次) ← 75%的时间在这里,瓶颈!⚠️

image.png

算法流程

总体流程图

开始
  ↓
1. 查找最大重复次数
  ↓
2. 按时间顺序找出第一个等于最大值的堆栈索引
  ↓
3. 复制 Point Stack
  ↓
4. 计算 Point Stack 中每个地址的总重复次数
  ↓
5. 创建 KSStackCursor 并返回
  ↓
结束

步骤 1:查找最大重复次数

目的: 找出 m_topStackAddressRepeatArray 中的最大值。

size_t maxValue = 0;
BOOL trueStack = NO;

// 第一次遍历:只找最大值(不记录索引)
for (int i = 0; i < m_cycleArrayCount; i++) {
    size_t currentValue = m_topStackAddressRepeatArray[i];
    if (currentValue >= maxValue) {
        maxValue = currentValue;
        trueStack = YES;
    }
}

if (!trueStack) {
    return NULL;  // 没有有效堆栈
}

步骤 2:找出 Point Stack 的索引

目的: 按时间顺序(从新到旧)找第一个重复次数等于 maxValue 的堆栈。

size_t currentIndex = (m_tailPoint + m_cycleArrayCount - 1) % m_cycleArrayCount;

// 第二次遍历:按时间顺序(从新到旧)
for (int i = 0; i < m_cycleArrayCount; i++) {
    // 计算真实索引
    int trueIndex = (m_tailPoint + m_cycleArrayCount - i - 1) % m_cycleArrayCount;
    
    // 找到第一个等于最大值的
    if (m_topStackAddressRepeatArray[trueIndex] == maxValue) {
        currentIndex = trueIndex;
        break;  // 找到最新的,立即停止
    }
}

索引计算公式:

trueIndex = (m_tailPoint + m_cycleArrayCount - i - 1) % m_cycleArrayCount

参数说明:
- m_tailPoint: 下一个要写入的位置
- i: 遍历变量(0 = 最新,1 = 次新,...)
- m_cycleArrayCount: 数组大小(如20)

例子:
假设 m_tailPoint = 1, m_cycleArrayCount = 5

i=0: trueIndex = (1+5-0-1) % 5 = 0  → 最新堆栈
i=1: trueIndex = (1+5-1-1) % 5 = 4  → 次新堆栈
i=2: trueIndex = (1+5-2-1) % 5 = 3  → 第三新堆栈
i=3: trueIndex = (1+5-3-1) % 5 = 2  → 第四新堆栈
i=4: trueIndex = (1+5-4-1) % 5 = 1  → 最旧堆栈(空)

步骤 3:复制 Point Stack

size_t stackCount = m_mainThreadStackCount[currentIndex];
size_t pointThreadSize = sizeof(uintptr_t) * stackCount;
uintptr_t *pointThreadStack = (uintptr_t *)malloc(pointThreadSize);

// 复制堆栈地址
for (size_t idx = 0; idx < stackCount; idx++) {
    pointThreadStack[idx] = m_mainThreadStackCycleArray[currentIndex][idx];
}

步骤 4:计算地址总重复次数

三层循环统计:

// 分配重复次数数组
m_mainThreadStackRepeatCountArray = (int *)malloc(stackCount * sizeof(int));
memset(m_mainThreadStackRepeatCountArray, 0, stackCount * sizeof(int));

// 外层循环:遍历 Point Stack 的每个地址
for (size_t i = 0; i < stackCount; i++) {
    uintptr_t targetAddress = m_mainThreadStackCycleArray[currentIndex][i];
    
    // 中层循环:遍历循环数组中的每个堆栈
    for (int innerIndex = 0; innerIndex < m_cycleArrayCount; innerIndex++) {
        size_t innerStackCount = m_mainThreadStackCount[innerIndex];
        
        // 内层循环:遍历当前堆栈的每个地址
        for (size_t idx = 0; idx < innerStackCount; idx++) {
            // 比较是否匹配
            if (targetAddress == m_mainThreadStackCycleArray[innerIndex][idx]) {
                m_mainThreadStackRepeatCountArray[i] += 1;
            }
        }
    }
}

算法分析:

  • 时间复杂度:O(n × m × k)
    • n = Point Stack 深度(通常 < 100)
    • m = 循环数组大小(通常 20)
    • k = 平均堆栈深度(通常 < 50)
  • 实际数据量很小,性能可接受

步骤 5:创建 KSStackCursor

KSStackCursor *pointCursor = (KSStackCursor *)malloc(sizeof(KSStackCursor));
kssc_initWithBacktrace(pointCursor, pointThreadStack, (int)stackCount, 0);
return pointCursor;

作用: 将原始堆栈数组包装成 KSCrash 能使用的标准格式。


至于堆栈的获取,可以参考我的另一篇文章ARM64 调用栈回溯原理