Trace 因果投影:怎么读这棵树
目标锚定单主树 · 无损明细 · 证据索引。
树上每一行、每一个数,都能对回 trace 里的一段证据。
附加性能 trace 提问后,答案里常会出现一章「Trace 因果投影」——一棵纯文本的因果树。 这份文档教你逐要素把它读懂:主树怎么锚定、状态段各是什么、占比条怎么读(以及什么不能加)、 [E#] 怎么对回证据附录、需求与供给为什么分开呈现。页面里的所有树片段都取自真实分析输出,逐字未改。
这是什么
Trace 因果投影是 CODRAX 对「这个线程为什么慢」的结构化回答:以你关注的线程为根(⊚), 把它在分析窗内的等待,沿 trace 里真实记录的唤醒/依赖边逐层向上游展开成一棵主树, 每行带状态、时长、占窗比与证据编号。先看一小段真实输出(来自一次真机 trace 分析,逐字摘录,中间行以「……(截断)」略去):
⊚ com.baidu.tieba-59566 ‹用户关注线程› 满格=窗口114.940ms
……(截断)
├─下钻─ ☾ CookieMonsterCl-59843 · sleep… ████░░░░░░ 44.836ms 39% [E5(+6)]
│ · sleep(s_sleep) · 上下文·不参与根因排序
│ · 链上L1 · 6次(2.705~10.976ms)
│ └─唤醒─ ☾ NetworkService-60595 · sleep… ██░░░░░░░░ 25.558ms 22% [E6(+4)]
│ · sleep(s_sleep) · 上下文·不参与根因排序
│ · 链上L2 · 3次(6.620~10.769ms)
│ └─唤醒─ ⛓ ThreadPoolForeg-60555 █░░░░░░░░░ 4.269ms 4% [E7(+1)]
│ · D-state(d_sleep) · 链上L3
│ · 承自归因6.936ms · 实际6.936ms(超出发生段,窗内)
……(截断)来源:eval/results/real_trace_d4_demand_vs_supply-20260730-043227/run-1.out(逐字摘录)
读法只有一条主线:自上而下 = 从关注线程向上游追溯。上面这段的含义是——主线程在睡眠等待(下钻), 直接原因是 CookieMonsterCl 没来唤醒它;CookieMonsterCl 又在等 NetworkService;NetworkService 最终卡在 ThreadPoolForeg 的一段 D 状态(不可中断等待)上。三行缩进就是三层因果。
为什么不用火焰图 / 时间线也能读懂等待因果
火焰图和时间线擅长「谁在跑」
它们按 CPU 占用或时间轴铺开所有线程,回答「时间花在哪」。但卡顿问题里线程多数时间不在跑——在睡、在等 IO、在等别人唤醒。等待的因果关系(谁在等谁)在那两种视图里要靠人眼跨线程对齐时间戳去猜。
因果树直接画出「谁在等谁」
投影树把 trace 里真实的 sched_wakeup/阻塞记录整理成边(├─下钻─、└─唤醒─),一行一个节点、一行一份证据。等待链是画出来的,不是猜出来的;trace 里没有的边不会被画上(见边界与诚实性)。
树是等宽纯文本(```text 代码块),不是图片也不是图表库——终端、markdown 落盘、网页三个出口
消费同一份字节,复制粘贴到工单里也不走样。这是产品的一条长期设计裁定。
什么时候会看到它
前提是本轮附加了性能 trace(CLI --htrace / --atrace,或 REPL 里
/htrace / /atrace;支持 HiTrace / atrace / systrace / perfetto 文本格式),
并且问题被判定为 trace 诊断类。提问尽量给足三要素——进程/线程名(或 pid/tid)、时间窗、想要的结论形态——
与站点其它页面的建议一致。下面是三条真实问法(逐字,来自真机 trace 的分析记录):
❯ 只分析这份 trace,不分析代码。在 34579.472865s 到 34579.587805s 期间,com.baidu.tieba 59566 主线程的卡顿更像是 CPU 算力不够(供给侧问题),还是在等依赖/事件(需求侧问题)?请给出判断和 trace 内依据。来源:eval/results/real_trace_d4_demand_vs_supply-20260730-043227/run-1.out
❯ 只分析这份 trace,不分析代码。请分析 CompThread_0-2955 在 13762.791708s 到 13763.024898s 窗口内的等待情况:它有没有进入过不可中断等待(D 状态)?内核记录的等待对象是什么,一共发生了几次,总量有多大?来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out
❯ 只分析这份 trace,不分析代码。请分析 .ugc.aweme.lite-17267 在 13762.791708s 到 13763.024898s 窗口内的 CPU 供给与运行占用:它在窗口内实际运行了多久?窗口内四种线程状态各占多少?它运行的 CPU 频率有没有受到限制,证据是什么?来源:eval/results/real_trace_h4_supply_thermal_witness-20260730-043658/run-1.out
它在答案里的位置
模型撰写的结论段之后,会出现标题为「Trace 因果投影」的章节。以真实输出为准,其骨架大致是:
- 主根因行 + 分析窗与状态账:一句话点名已证链上单项最大可消除量,随后是窗长、关注线程四态分布、已归因/未归因覆盖句。
- 树读法(图例):本报告用到的每个记号、口径词都在这里给出定义——图例是承诺面,出场记号必有条目。
- ◎ 窗内可消除量总览与因果投影主树:两个等宽代码块,前者按修复方向分节导航,后者就是本页的主角。
- 关键指标表 / 下一步 / 指标快照 / 逐节点明细 / 证据索引:树上压缩掉的完整属性、每个 [E#] 的 trace 行号或时间区间,都在这些配套小节里,无损可查。
不是每个 trace 问题都带树:纯画像/计数类问题的答案可能没有这一章;证据不足以支撑因果行时,系统会用显式的「覆盖边界」披露代替(见边界与诚实性),而不是硬画一棵树。
要素①主树与锚定:先找 ⊚
树根永远是你在问题里点名的那个线程——⊚ 线程名 ‹用户关注线程›。树头同一行标注
满格=窗口时长(所有占比条的统一标尺),头部还有几行板级注记与「记号速览」:
⊚ CompThread_0-2955 ‹用户关注线程› 满格=窗口233.190ms
- 链上平铺席按显示值降序(仅平铺席;树位挂靠行按链结构)
- 记号速览: ⊚根 ☾sleep ⧖runnable ⚙running ⛓D/IO链上 ⧗D/IO非链 ⇅反转候选 ◦数据行 ◦中转 ✦语义
➊..➎该板TOP5 ◇邻近 ▒背景 ◈业务span (全释义见 树读法)来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out
树里的父子关系由边词表达,闭集只有几种,图例(树读法)里逐条定义。这是真实输出里的原文:
- 边:
- `├─下钻─` = 父行在等什么:该子行就是父行等待的直接原因。
- `└─唤醒─` = 该行唤醒其父行(父行的等待由该行结束;父行依赖该行)。
- `父节点未确认` = 该行在链上且引擎已给出层数(行注/明细作 链上L#(父节点未确认)),但本树内未找到可挂靠的父节点:不声称唤醒/下钻关系,不编造树位。
- `└─链上─`+行注`链上·深度未解析` = 链上项但树位深度未解析:不声称唤醒/下钻关系,不编造树位;该行 [E#] 经证据索引给出 trace 行号区间。
- `├─语义─`/`✦` = 该位置的语义 span(业务阶段,确定性优化族),非调度状态行。来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out(树读法节选)
常见误读:不要把缩进读成「调用栈」。这不是函数调用树,是等待因果树:子行是父行等待的原因或唤醒来源。也不要默认所有链上行都挂在树上——引擎只在有唤醒/下钻证据时才画边,其余链上项以 ├─链上─ 平铺,并明确标注「父节点未确认」或「链上·深度未解析」。
要素②状态段:☾ ⧖ ⚙ ⛓ 各是什么
每行开头的记号标出该行的主导调度状态。定义以每份报告自带的图例为准(逐字):
- 记号:
- 行内 sleep/runnable/running/iowait/D-state = 该行的主导调度状态。
- `☾/sleep` = 睡眠等待(等事件/等唤醒);睡眠是症状而非根因,根因看它的下钻/唤醒子行。
- `⧖/runnable` = 就绪等待(有资格运行但未获得 CPU)。
- `⚙/running` = 运行占用(正在 CPU 上执行)。
- `⛓/D-state·iowait` = 不可中断等待/IO阻塞(链上行)。来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out(树读法节选)
各状态怎么读、容易误读在哪:
| 记号 / 状态 | 含义 | 常见误读 |
|---|---|---|
☾ sleep | 睡眠等待(等事件/等唤醒)。 | 把大段 sleep 当根因。图例明说:睡眠是症状而非根因,要看它的下钻/唤醒子行是谁没来唤醒它。 |
⧖ runnable | 就绪等待:有资格运行但没拿到 CPU。 | runnable 很短却硬归因「CPU 不够」。runnable 长期堆积才指向调度供给问题。 |
⚙ running | 运行占用:正在 CPU 上执行。 | 以为 running 不会上根因榜。低频/小核上「跑得慢」的 running 会以折算口径入榜(见要素⑥)。 |
⛓ D-state·iowait | 不可中断等待 / IO 阻塞(链上行;非链上同族用 ⧗)。 | 把 D 状态一律当磁盘 IO。判词区分「IO阻塞」与「不可中断等待·非IO已证」两族——下面的例子里 D 状态等的是 GPU fence,iowait=0。 |
⇅ / ✦ / ◦ | 优先级反转候选 / 业务语义 span / 未识别形态的数据行。 | 各自的准确定义都在该报告的「树读法」里,出场记号必有图例条目。 |
一段真实的状态段行(关注线程自身的 sleep 与 D 状态,注意 D 状态行直接给出内核记录的等待对象):
│ ☾ 自身·sleep 78.630ms 8次(6.912~16.419ms) 该段处于等待唤醒 [E2(+7)]
│ ⛓ 自身·D-state(对端未解析) 36.757ms 4次(3.774~16.064ms) D-state [E3(+3)]
│ · 等待对象 dma_fence_default_w · 同段镜像·与家族行同源来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out
要素③占比条与百分数:怎么读、什么不能加
每行的时长条是定宽的:满格 = 树头标注的长度(通常就是分析窗全长),百分数是该行时长占分析窗的比例。
链上/邻近行用 █ 填充,背景行用 ▒,一眼可分。真正要记住的是两条纪律:
⚠ 纪律一:不同层的墙钟时长不可直接相加。
父行在等、子行也在等——两段等待在墙钟上互相包含、彼此重叠,逐层加总会把同一段物理时间计费多次。这是产品记录在案的裁定(覆盖率只做「第一层已归因 vs 窗长」的减法,从不做逐层求和),也是用户最容易犯的错。树头覆盖句自己就会披露这一点:
分析窗 13762.792~13763.025s,共 233.190ms。
- 关注线程等待(sleep/D-state/runnable) 116.963ms 中链上已归因 14.750ms(13%),未归因 102.213ms(87%)。
- 另有 86 条链上行未计入上句已归因数值(单项最大 74.915ms;墙钟不可加和,详见明细/树)。
- 各根因席位有效归因合计 332.701ms 超过窗长 233.190ms:席位间物理时间可重叠,不可直接相加。来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out(树头覆盖句,逐字)
注意最后一行:各席位有效归因合计 332.701ms 超过窗长 233.190ms——系统直接把「不可相加」的原因印在报告里 (席位间物理时间可重叠)。唯一被承诺可相加的形态是「同源二分」对席(见要素⑤)。
⚠ 纪律二:占窗 >100% 可以是合法的,但要看口径词。
跨线程累计、跨 CPU 多段累计、他线程口径的背景行,都可能合法超过窗口长度——此时时长条已封顶,数值旁必带口径注记(如 (跨线程累计,非墙钟)、超窗(他线程口径))。口径词永远与数值同行同视野。
▒ 背景压力
⧖ 调度压力(需求积压)·聚合 201.409ms(跨线程累计,非墙钟)
⤷ ·≈平均排队深度 10.1 [E18]
· 上下文·不参与根因排序
├─背景─ ◦ 其余 10 项(折叠) ▒▒▒▒▒▒▒▒▒▒ 63.361ms 317% [E19(+9)]
· 成员 hilogd.pst-474 · 其余 9 项见明细
· 10线程取最大(单项4.990~63.361ms) - 占窗>100% = 跨CPU/多段累计,可合法超过窗口长度(时长条已封顶);同一线程几乎相同的重复记录(差异≤3%)只计一次,明显不同的重叠段分段累计。来源:eval/results/real_trace_c2_dstate_iowait-20260730-043041/run-1.out(背景压力区段与「占窗>100%」口径条,逐字)
还有一类行根本没有条和百分数:⌗ 口径旁栏 行(计数当量、综合评分)。它们不是墙钟时长,
所以不画时长条、不标占窗比、不占根因排序序数——非墙钟数值不与墙钟同池比较。
要素④[E#]:从树行回到证据
每个持值行的行尾都有 [E#] 编号;E#(+N) 表示该行另合并了 N 条同类观测。
文末的「证据索引」按编号列出每条证据在 trace 里的行号或时间区间,以及一串审计字段
(tier / causality / rank / confidence / predicate 等,均为 trace_query 原文 token)。先看树上一行:
│ └─唤醒─ ☾ VSyncGenerator-2179 █░░░░░░░░░ 12.088ms 5% [E8]
│ · sleep(s_sleep) · 上下文·不参与根因排序
│ · 链上L3 · 发生段 13762.949876s~13762.962502s·与[E15]不相交(共2段,合计 26.048ms)
│ · 实际18.912ms(超出发生段,窗内)行尾是 [E8]。翻到同一份报告文末的证据索引,按编号找到它和它的邻居:
- **E6** — 定位: [13762.950~13762.965s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact,行 16709–19986
- **E7** — 定位: [13762.950~13762.964s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact,行 16706–19904
- **E8** — 定位: [13762.950~13762.963s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact,行 17494–19635
- **E9** — 定位: [13762.954~13762.959s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact,行 17690–19297
- **E10** — 定位: [13762.954~13762.958s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact,行 18565–19170
- **E11** — 定位: [13762.954~13762.958s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact,行 18126–19161来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out(证据索引节选)
对回的方法:
- 定位给出 trace 文件里的时间区间(或行号区间)——拿原始 trace 在这个区间内就能复核该行的每个数。坐标指向 trace 文件本身,不是源码行号。
- causality=on_wakeup_chain 说明该证据在唤醒链上;
rank=是根因排序榜位;origin=system_supplement表示这条观测来自成文前的确定性补采,不是模型即兴查询。 - 树、关键指标表、逐节点明细用的是同一套 E# 编号——三处互指,任何一行都能顺着编号走到证据。
要素⑤同段双源与合并行:一段时间只记一次账
同一段物理时间常被 trace 的多条通道观测到(根因排序看到它、唤醒链也看到它;裸状态视图和家族聚合也会重复)。 投影的约定是合并显示、互指留痕、数值不重复计入。报告图例的原文定义:
图例原文(节选):「同段镜像 = 同一物理段/同一物理时间在多条通道重复发布:同段同值的裸状态行已并入所指行(其 [E#] 并入行首括号)……两行数值不可相加」;「同段被根因排序与唤醒链两来源各发一行时已合并为一行,根因排序行的 E# 并入行尾 [E#+E#],数值不重复计入」。
真实例子:关注线程的 D 状态在树上有两行——裸状态视图行(标注「同段镜像·与家族行同源」)与佩戴 ➋ 徽章的家族聚合席。两行说的是同一批 D 状态段,账只记一次:
│ ⛓ 自身·D-state(对端未解析) 36.757ms 4次(3.774~16.064ms) D-state [E3(+3)]
│ · 等待对象 dma_fence_default_w · 同段镜像·与家族行同源
│ ➋ ⛓ 自身·D-state 合计36.757ms [E4]
│ · D状态候选·目标自身·墙钟席·置信中·修向 IO/内核/依赖·等待对象 dma_fence_default_w
│ · 有效归因 36.757ms = 合计(共4段,同线程)
│ · 成员 d_sleep cpu=3 合计16.064ms(5段)
│ · 成员 d_sleep cpu=1 合计10.424ms(3段)
│ · 成员 d_sleep cpu=2 合计6.495ms(2段)
│ · 其余 1 项见明细(成员共4,列3)
│ · 等待对象 dma_fence_default_w来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out
与之相对,「同源二分」是全树唯一被承诺可相加的席位对:一份全窗账按链上凭证拆成 ⛓ 锚定席 + ◇ 余段席,两席不相交、相加恰好还原全窗值,行内直接写出恒等式并互指对席:
├─链上─ ⧖ T7@ZeusThreadPo-61839 · runnable █░░░░░░░░░ 0.370ms <1% [E20]
│ · 调度压力候选·根因排序#9·置信中·链上·深度未解析·修向 调度供给·有效归因 0.370ms(全额)
│ · 同源二分:全窗0.445ms=本行锚定0.370ms+其余0.075ms(◇余段席)
│ · 唤醒锚定(宿主→目标,见图例)·最晚相关边 34579.496810s·凭证=直接裸边
│ · 同源二分对席:◇席=[E31],⛓席=本行;合计还原全窗账 0.445ms(规则见图例)来源:eval/results/real_trace_d4_demand_vs_supply-20260730-043227/run-1.out
要素⑥需求与供给:为什么分开呈现
「谁在等谁」(需求侧)和「CPU 给得够不够」(供给背景)是两类不同性质的证据,混在一起必然误导。 投影把它们放在不同位置、用不同口径:链上席回答等待因果;频点、温控、绑核这类供给证据以独立口径出现, 从不冒充一个等待席位。
供给证据长什么样
关注线程自身的 running 席是最典型的供给载体:running 原始时长按「全域最大核最高频」基准折算, 差值就是「供给折算缺口」——低频/小核导致的跑慢成分。温控证据(热限压)直接写在行内:
│ ➊ ⚙ 自身·running 74.915ms [E1]
│ · 算力供给候选·目标自身·墙钟席·置信高·修向 频率与热治理
│ · 有效归因 65.912ms = running(折算,按全域最大核最高频) 65.912ms
│ · running 原始 74.915ms → 计入 65.912ms(折算,按前述基准,按实测频点共动分簇折算)
│ · 供给折算缺口 65.912ms(运行频点非最高,按前述基准折算,下界,分簇口径同前)为主,running
│ 时间含降频/小核导致的跑慢成分;窗内该簇受热限压至 1.53GHz绑核/亲和限制则以「CPU亲和/cpuset限制」行出现在 ◇ 邻近区段,行内给出允许核、排除核与被排除的大核算力档——并明确声明它「无链上凭证,整席不入链上榜」:
⧖ logd.writer-9163 · CPU亲和/cpuset限制 ██░░░░░░░░ 48.519ms 21% [E39]
· 调度压力候选·邻近影响#9·置信中·修向 调度供给
· runnable · 无链上凭证(整席不入链上榜,见图例)
· CPU约束描述:允许核 0-1,3-11 · 排除全域观测核 2,12-13 · 绑核排除更大核档(允许核最高档 2.27GHz
< 全域最大核档 2.75GHz) · 判定依据 sched_switch_next_info以上两段来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out
三个区段的分工
链上(树区)
有 typed 因果凭证的席位:唤醒边、下钻关系、目标自身墙钟。只有这里的数值是「已证可消除量」。
◇ 邻近区段
与唤醒链时间相邻、但不在唤醒链上。是「条件可消除上界」——因果候选成立时至多好这么多,不进方向守恒。
▒ 背景压力
环境证据(跨线程/他线程口径),不计入链上归因,需结合链上证据解读。压力大不等于因果成立。
读法要点:看到「供给折算缺口」先看它相对等待量的大小——在下文的案例里,10.331ms 的频率缺口对上 84.358ms 的 sleep,系统与答案都明确判它是次要因素。供给背景解释「跑得慢」,解释不了「根本没在跑」。
要素⑦◎ 总览:按修复方向导航
主树之前还有一个 ◎ 窗内可消除量总览 代码块:把同尺(关注线程窗内墙钟 ms)的持值行按
修复方向(调度供给 / 锁与优先级 / IO·内核·依赖 / 频率与热治理 / 自身工作量……闭集词表)分节陈列,
每节头给出「最大可消」。它是导航索引:零序数、零徽章、只转录值与指针,榜位归属仍在主树。方向间收益不可相加。
◎ 窗内可消除量总览 · 尺=com.baidu.tieba-59566 窗内墙钟ms
⛓ 链上块先 · 节=修复方向(其他方向恒末,余按节内最大可消降序)· 节内值降序 · 方向间收益不可相加
▸ 锁与优先级 · 最大可消 23.994ms · 2席 · 成员区间重叠,合计不可直加
23.994ms ████████████ CookieMonsterCl-59843 · 优先级反转候选 ·构成 ·交集证明 [E8]
19.041ms ██████████░░ NetworkService-60595 · 优先级反转候选 ·构成 ·交集证明 [E15]
▸ IO/内核/依赖 · 最大可消 10.433ms · 2席 · 成员区间重叠,合计不可直加
10.433ms █████░░░░░░░ ThreadPoolForeg-60555 · IO阻塞·不可中断(原因未证) ·合计(共3段,同线程)
⤷ ·交集证明 [E16]
7.386ms ████░░░░░░░░ ThreadPoolForeg-60555 · IO阻塞 ·合计(共4段,同线程) ·交集证明 [E17(+2)]
▸ 频率与热治理 · 最大可消 10.331ms
10.331ms █████░░░░░░░ com.baidu.tieba-59566 · 低频运行 ·折算,按全域最高频 ·目标自身 [E2]
▸ 自身工作量 · 最大可消 0.285ms
0.285ms █░░░░░░░░░░░ T7@ZeusThreadPo-61839 · 类校验 ·唤醒锚定 [E24]
……(截断)来源:eval/results/real_trace_d4_demand_vs_supply-20260730-043227/run-1.out(◎ 总览节选)
注意每节头的诚实注记:「2席 · 成员区间重叠,合计不可直加」——两席的时间包络实测重叠时,系统只发「最大可消」, 拒绝给出会重复计费的小计。
一个完整案例走读
选一份 2026-07-30 的真机结果:com.baidu.tieba 主线程卡顿,问「是 CPU 算力不够(供给侧),还是在等依赖/事件(需求侧)」。 从提问到最终树逐段走一遍,看结论是怎么从树上读出来的。以下每段均为逐字摘录。
-
1
提问(三要素齐全:线程、时间窗、结论形态)
❯ 只分析这份 trace,不分析代码。在 34579.472865s 到 34579.587805s 期间,com.baidu.tieba 59566 主线程的卡顿更像是 CPU 算力不够(供给侧问题),还是在等依赖/事件(需求侧问题)?请给出判断和 trace 内依据。 -
2
答案先给判断,再给依据
结论段开门见山:需求侧问题。判据是自身四态分布——sleep 73% / runnable 仅 3%:
在 34579.472865s–34579.587805s(窗口约 114.94ms)期间,com.baidu.tieba 59566 主线程的卡顿是需求侧问题——主线程在等待依赖/事件,而不是 CPU 算力不够。 主线程自身状态分布:sleep 占 84.358ms(约 73%),runnable 仅 3.636ms(约 3%),running 约 26.946ms(约 23%)。runnable 时间极短,说明主线程并没有大量积压的待运行工作,不符合"想跑但没有 CPU 可用"的供给侧特征。 -
3
树头:分析窗、四态账、覆盖句
四态合计恰等于分析窗(这是唯一合法的加和);「已归因 14% / 未归因 86%」只做第一层减法;行尾直接声明「墙钟不可加和」:
分析窗 34579.473~34579.588s,共 114.940ms。 - 关注线程全窗四态: running 26.946ms(23%) + runnable 3.636ms(3%) + sleep 84.358ms(73%) + D-state 0.000ms(0%) = 114.940ms(四态合计=分析窗)。 - running 26.946ms: 供给折算影响 10.331ms 见 ➍ [E2](折算,不计入四态合计) · 自身执行(无确定性可优化工作) 26.946ms。 - 关注线程等待(sleep/D-state/runnable) 80.235ms(按自身状态视图行合计尺,与上方四态行不同尺,不可直接对账) 中链上已归因 11.103ms(14%),未归因 69.132ms(86%)。 - 另有 31 条链上行未计入上句已归因数值(单项最大 47.282ms;墙钟不可加和,详见明细/树)。 -
4
自身状态段:症状面
⊚ 下的自身行:sleep 76.599ms(12 次)标注「窗口内主要处于等待唤醒」——症状;➍ running 席携带供给折算缺口 10.331ms——供给侧的全部分量,量级摆在这里:
│ ☾ 自身·sleep 76.599ms 12次(2.584~11.103ms) 窗口内主要处于等待唤醒 [E1(+11)] │ ➍ ⚙ 自身·running 26.946ms [E2] │ · 算力供给候选·目标自身·墙钟席·置信高·修向 频率与热治理 │ · 有效归因 10.331ms = running(折算,按全域最高频) 10.331ms │ · running 原始 26.946ms → 计入 10.331ms(折算,按全域最高频,按频率比) │ · 供给折算缺口 10.331ms(运行频点非最高,按全域最高频折算,下界,按频率比)为主,running │ 时间含降频等导致的跑慢成分 │ ⧖ 自身·优先级反转·可运行等待 合计3.615ms [E3] │ · 优先级反转·可运行等待·目标自身·墙钟席·根因排序#8·置信中·修向 锁与优先级 │ · runnable · 有效归因 1.017ms │ ⧖ 自身·runnable 0.021ms [E4] │ · 调度压力候选·目标自身·墙钟席·根因排序#11·置信中·修向 调度供给·有效归因 0.021ms(全额) -
5
唤醒链:等待主因的因果路径
下钻三层:主线程 ← CookieMonsterCl ← NetworkService ← ThreadPoolForeg(D 状态)。这是「在等依赖」的直接证据链:
├─下钻─ ☾ CookieMonsterCl-59843 · sleep… ████░░░░░░ 44.836ms 39% [E5(+6)] │ · sleep(s_sleep) · 上下文·不参与根因排序 │ · 链上L1 · 6次(2.705~10.976ms) │ └─唤醒─ ☾ NetworkService-60595 · sleep… ██░░░░░░░░ 25.558ms 22% [E6(+4)] │ · sleep(s_sleep) · 上下文·不参与根因排序 │ · 链上L2 · 3次(6.620~10.769ms) │ └─唤醒─ ⛓ ThreadPoolForeg-60555 █░░░░░░░░░ 4.269ms 4% [E7(+1)] │ · D-state(d_sleep) · 链上L3 │ · 承自归因6.936ms · 实际6.936ms(超出发生段,窗内) ├─链上─ ◦ 其余 6 项(折叠) ████░░░░░░ 47.282ms 41% [E23(+5)] │ · 成员最大 CookieMonsterCl-59843(见榜位#1) · sleep 47.282ms · 其余 5 项见明细 │ · 6线程取最大(单项4.558~47.282ms) -
6
➊ 席位:主根因的量与凭证
CookieMonsterCl 的 runnable 席佩 ➊:有效归因 23.994ms,分解为 runnable(全额) + running(折算),并与构成段行互指、声明物理时间重叠不可相加:
├─链上─ ➊ ⇅ CookieMonsterCl-59843 · runnable ██░░░░░░░░ 23.994ms 21% [E8] │ · 优先级反转候选·置信高·链上L1(父节点未确认)·修向 锁与优先级 │ · 有效归因 23.994ms = runnable(全额) 23.748ms + running(折算) 0.246ms │ · runnable 原始 23.748ms → 计入 23.748ms(全额) │ · running 原始未发布 → 计入 0.246ms(折算,按全域最高频,运行频点非最高,按频率比) │ · 链上累计26.170ms · 构成段见[E9(+4)](本席数值已计入全部构成段,构成段行不另计) │ · 与[E9(+4)]同线程同状态族·物理时间重叠(不可相加)·账目关系(见图例):本行=按链上聚合归账 │ (根因排序席),[E9(+4)]=按本行发生段归账 │ · 本线程另有邻近席 [E27] -
7
◇ 邻近区段:调度背景
同一个 CookieMonsterCl 还有 26.738ms 的 runnable 落在邻近区段——无链上凭证,所以只作「条件可消上界」,不入链上榜:
◇ 邻近区段 ⧖ CookieMonsterCl-59843 · 优先级反转… ██░░░░░░░░ 26.738ms 23% [E27] · 优先级反转·可运行等待·邻近影响#3·置信中·修向 锁与优先级 · runnable · 无链上凭证(整席不入链上榜,见图例) · 本线程另有链上席 [E8] · 有效归因 1.847ms ……(截断) -
8
对回证据:E5→E8
证据索引里,链上各行的定位与审计字段(E8 即 ➊ 席,rank=1、causality=on_wakeup_chain):
- **E5** — 定位: [34579.476~34579.588s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78…,行 13431–15160 - **E6** — 定位: [34579.525~34579.588s]; 审计: tier=context_only · causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact · merged_count=3…,行 13437–15148 - **E7** — 定位: [34579.577~34579.588s]; 审计: causality=on_wakeup_chain · confidence=0.78 · origin=system_supplement · predicate=wakeup_causal_impact…,行 13447–15131 - **E8** — 定位: [34579.473~34579.572s]; 审计: tier=primary · causality=on_wakeup_chain · rank=1 · confidence=0.88…,行 2892–13060 -
9
供给侧交叉核对,收束结论
答案没有回避供给证据,而是给出量级对比后判为次要因素:
主线程 running 时段的频率折算缺口为 10.331ms(修向:频率与热治理)——这意味着如果运行在最高频点,理论上可节省约 10ms。但 10.331ms 相对于 84.358ms 的 sleep 占比极小(仅约 12%),不能解释主线程 73% 的 sleep 状态。频率缺口是次要因素,需求侧等待才是主因。至此三件事都从树上读出来了:等待主因(CookieMonsterCl 优先级反转,23.994ms)、唤醒链(主线程←CookieMonsterCl←NetworkService←ThreadPoolForeg D 状态)、调度背景(频率缺口 10.331ms,次要;◇/▒ 提供上界与环境证据)。
本节全部摘录来源:eval/results/real_trace_d4_demand_vs_supply-20260730-043227/run-1.out
边界与诚实性
这棵树的另一半价值在于它不画什么:没有唤醒证据就不画边,没有窗口数据就不造占比, 证据不足就显式披露。树上不声称的关系,就是证据里没有的关系——折叠行除外,它们带着计数指针指向无损明细,折叠不丢信息。
链没追上去时:⊘ 与平铺形
窗口里追不出唤醒路径时,树头直接换成 ⊘ 声明,席位改为平铺——不编一棵好看的树:
⊘ 唤醒链路径未解析 满格=窗口20.000ms
- 本板成因: 仅单簇有频点采样——「按频率比」行同此因,板级一次声明,行内不再复读
- 链上平铺席按显示值降序(仅平铺席;树位挂靠行按链结构)(计数当量行恒末)
- 记号速览: ⧖runnable ⚙running ⛓D/IO链上 ⧗D/IO非链 ⌗口径旁栏 ◦数据行 ➊..➎该板TOP5 ⊘链止 ◇邻近 ▒背景
◈业务span ⤷折行续行 (全释义见 树读法)来源:eval/results/real_trace_c2_dstate_iowait-20260730-043041/run-1.out
树位存疑时:「父节点未确认」
引擎知道某行在链上、也知道层数,但在本树里找不到可挂靠的父节点时,它平铺该行并如实标注——不声称唤醒/下钻关系,不编造树位:
├─链上─ ☾ RSUniRenderThre-2188 · sleep… █░░░░░░░░░ 15.960ms 7% [E12]
│ · sleep(s_sleep) · 上下文·不参与根因排序
│ · 链上L1(父节点未确认) · 发生段 13762.881585s~13762.898004s·与[E6]不相交(共2段,合计 30.265ms)
│ · 实际32.321ms(超出发生段,窗内)来源:eval/results/real_trace_h2_dstate_dma_fence_triform-20260730-043627/run-1.out
证据不足时:显式的覆盖边界披露
每份报告末尾都可能出现「Trace 因果投影覆盖边界」披露块,写明哪些结果被压缩、哪些枚举不完整、哪些结论只能当下界。这是一段真实的披露原文:
> **Trace 因果投影覆盖边界** 本报告已获得 trace_query 的结构化执行记录,但没有产出有数据支撑的 root_cause/wakeup_chain/semantic 行,因此未生成分层因果表。 结构化原因: trace_query 结果已压缩;event_search 结果达到条数上限。 这不是“没有背景影响”的结论;只表示当前证据没有给出可审计的因果/背景统计,可追问一次根因/窗口/交互统计分析(root_cause_rank、window_stats 或 interaction_stats)补齐。 目标窗内状态账: com.baidu.tieba-59566 窗2.992ms — 主导状态=sleep 2.978ms(99.5%),running=0.000ms,runnable=0.014ms,sleep=2.978ms(其中 IO等待 0.000ms),d_state=0.000ms,io_wait=0.000ms;等待型自身状态是症状面,不作为可消除影响参与根因排序席位。证据权限: frame_causality=unproven,frame_evidence_status=absent;未获得可绑定到目标的 frame/deadline 证据或 typed causal row,调度、IO、频率观察只能描述窗口背景,不能证明具体丢帧因果。枚举权限: enumeration_status=incomplete,compacted_views=critical_blocking_calls,event_search,root_cause_rank,boundaries=critical_blocking_calls/candidates:emitted=20,total=21|event_search/events:emitted=40,total=59|root_cause_rank/candidates:emitted=12,total=26;达到上限或分页只返回的行只能作为样本或下界,不能支撑“全部/仅有/总计/共N/最大/最小”结论。来源:eval/results/real_trace_f1_exclude_no_code-20260730-043441/run-1.out
模型侧的未覆盖项同样落在显式的「补充说明」里:
- 本回答生成前,系统建议的部分补充定位/钻取步骤未执行;结论以已收集的证据为准,未覆盖的部分请按未验证对待。
- trace 中部分线程的睡眠等待未定位到上游唤醒者;相关结论基于已收集的证据,未定位的唤醒来源请按未验证对待。来源:eval/results/real_trace_c2_dstate_iowait-20260730-043041/run-1.out
一句话总结:读这棵树时可以放心做的推断,是「行上写了什么、证据就有什么」;不能做的推断,是「行上没写的也大概如此」。缺口在哪,报告自己会说。
树是投影,证据是本体;
每一行都能对回 trace,这才是它可信的原因。
怎么附加 trace、转换二进制格式,见使用手册 §3.2 附加性能 trace;投影与 trace 子系统的完整机制设计,见仓库内 docs/architecture.md。