分类: 瞎折腾

  • Grok Build 在 WSL2 输入延迟 430ms:一次从 strace 到 9P 文件系统的排查实录

    xAI 的 Grok Build CLI(grok)在 WSL2 里打字有明显延迟(单键 100–400ms 回显),而原生 Windows、原生 Linux、以及同为 TUI 的 btop 都流畅。折腾一晚上,最终用 strace 抓到真凶:一个每帧都要跑的 ffmpeg PATH 全扫描,在 WSL 的 9P 文件系统上每次花掉 400ms。修复后按键回显从 avg 405ms 降到 5.6ms。


    现象

    环境输入是否延迟
    WSL2 + Windows Terminal❌ 卡
    WSL2 + WSLg(Zutty / WezTerm)❌ 卡
    原生 Windows(grok.exe)✅ 流畅
    原生 Linux✅ 流畅
    WSL2 里的 btop(同为 TUI)✅ 流畅

    “只有 WSL2 卡”这个特征很重要——它把嫌疑圈在 WSL 特有的层上,但也差点把我们带进沟里。

    排查过程(含走过的弯路)

    弯路 1:怀疑 Windows PE / interop 层

    一开始 file 命令看到的是 grok-0.2.114-linux-x86_64,但在 WSL 里 which grok 指向的符号链接被误判成了 Windows PE。WSL 里跑 Windows exe 确实要过 interop 翻译,但这个 grok 是原生 ELF,方向错了。

    弯路 2:怀疑 epoll 100ms 超时轮询

    strace 显示输入线程在 epoll_pwait(11, [], 3, 100) 里反复空转 100ms 超时,看起来像经典 bug:

    26380 epoll_pwait(11, [], 3, 100) = 0   ← 空等
    26380 epoll_pwait(11, [], 3, 100) = 0   ← 又空等
    26380 epoll_pwait(11, ..., 100) = 1     ← 终于收到
    26380 read(0, "4")                      ← 400ms 后才读到

    于是:

    1. LD_PRELOAD shim 劫持 epoll_pwait 把超时降到 5ms —— 无效(二进制是 static-pie,LD_PRELOAD 根本不加载);
    2. 直接改源码,把输入线程的 POLL_TIMEOUT 从 100ms 改成 1ms —— 依然无效。

    回头细看 strace 才发现被误导了:那些 100ms 空转发生在两次按键之间(空闲期),而按键本身的 epoll 事件是及时到达的:

    20.015758 epoll_pwait(11, <unfinished>
    20.068647 resumed [{EPOLLIN}] = 1     ← 事件立刻到,没等满 100ms
    20.068833 read(0, "7") = 1            ← 读取零延迟

    输入读取根本没问题。延迟在下游——按键读进来之后,回显迟迟没写回终端。

    教训:strace 输出是海量的,先找对”延迟到底发生在哪一段”再深挖,否则会被漂亮的巧合现象带偏。

    转折:一个能稳定复现的探针

    与其在真实终端里凭手感测,不如写一个 pty 探针:key-latency(挂在 xai-grok-pager-pty-harness 里),用 mock 后端起一个真实 session,然后逐键注入、测量每个字符出现在屏幕上的时间:

    key  3 ('c'): 534.1 ms
    key  4 ('d'): 425.1 ms
    key  5 ('e'): 461.0 ms
    ...
    session-prompt echo latency: avg 404.8 ms  max 534.1 ms

    稳定的 ~400ms,和手感一致。现在可以对它抓 strace 了。

    真凶:每帧一次 ffmpeg PATH 全扫描

    在探针运行中对 pager 抓 strace(strace -f -tt -T -o log <binary> 作为父进程,避免 ptrace 权限问题),看按键 k 之后、回显之前,渲染线程在干什么:

    12:57:33.181 read(0, "k") = 1                          ← 按键到达,零延迟
    12:57:33.186 openat("/dev/tty") / ioctl(TIOCGWINSZ)    ← 常规操作
    12:57:33.187 statx("/home/bzsc/.cargo/bin/ffmpeg")         ENOENT
    12:57:33.188 statx("/home/bzsc/.local/share/pnpm/bin/ffmpeg") ENOENT
    12:57:33.188 statx("/home/bzsc/.pyenv/shims/ffmpeg")        ENOENT
    ... 20+ 个 Linux PATH 目录 ...
    12:57:33.191 statx("/mnt/c/Windows/system32/ffmpeg")   ENOENT  ← 9P,0.7ms
    12:57:33.195 statx("/mnt/c/WINDOWS/system32/ffmpeg")   ENOENT  ← 9P,3.7ms
    12:57:33.203 statx("/mnt/c/Windows/ffmpeg")            ENOENT  ← 9P,2.9ms
    ... 十几个 /mnt/c、/mnt/d 目录,每个 1–6ms ...

    每次按键后的重绘,渲染线程都在做一次完整的 ffmpeg PATH 扫描。

    链路是这样的:

    • scrollback/state/mod.rsprepare_layout()每帧渲染前调用 ffmpeg_available();
    • ffmpeg_available() 实现是 which::which("ffmpeg"),即逐个目录 statx,负结果完全不缓存,下次调用重扫一遍;
    • WSL 会把 Windows 的 PATH 追加进 Linux 的 PATH。于是每次扫描除了 ~20 个 Linux 目录,还要遍历十几二十个 /mnt/c/.../mnt/d/...;
    • /mnt/c9P 协议访问 Windows 文件系统,一个 statx 要 1–6ms(内核态请求 → Windows 侧翻译 → 往返);
    • 整个 PATH 扫完正好 ~400ms —— 与体感延迟分毫不差。

    原生 Linux 上这段扫描只要几百微秒(全在本地 ext4),所以”只有 WSL2 卡”。而”空闲时偶尔卡、打字时卡得更明显”是因为:打字 → 每次按键都重绘 → 每次重绘都扫描。

    修复

    方案 A(推荐,给用户):在 WSL 里装 ffmpeg

    sudo apt install ffmpeg

    ffmpeg_available() 对命中结果是永久锁存的(FOUND latch),只要在 PATH 里找到一次,之后连扫描都不发生。这是最干净、零风险的解法,还顺便解锁了内联视频功能。

    方案 B(给上游,已改进源码):负结果限频 + 后台线程

    crates/codegen/xai-grok-pager/src/inline_media_ffmpeg.rsffmpeg_available():

    • 负结果限频:5 秒内最多做一次 PATH 扫描(之前是每帧一次);
    • 限频后的扫描放后台线程,渲染线程永不阻塞;
    • 首次探测仍同步,保证第一帧就有准确答案;中途装 ffmpeg 后,5 秒内恢复内联视频提示。

    验证

    同一个 key-latency 探针,修复后的 release 二进制:

    修复前:avg 404.8 ms  max 534.1 ms
    修复后:avg   5.6 ms  max   7.8 ms   (800 词滚动内容下)

    一些可以带走的东西

    1. 先量化,再定位。真实终端里”手感卡”没法 debug;一个能稳定复现、能给出数字的 pty 探针,把 4 小时的排查压缩到 20 分钟。
    2. 别被 strace 的表面现象带偏。epoll 100ms 空转看着像根因,但那是空闲期的正常休眠;按键事件的到达其实是及时的。先问”延迟发生在哪一段”,再问”为什么”。
    3. WSL 的 PATH 扫描很贵。9P 上每个 statx 1–6ms,”每帧查一次命令在不在 PATH 里”这种代码,在原生 Linux 上无感,在 WSL 上是灾难。凡是高频路径里的 which/PATH 探测,要么缓存、要么限频。
    4. 上游修复价值:这不只是 grok 的问题,任何在渲染循环里做 PATH 探测的 TUI(以及 WSL 用户)都会踩到。这个修复已经按”对原生 Linux 零影响”的方式设计(限频只在负结果时生效,命中即永久锁存)。

    环境:WSL2 (kernel 6.18.33.2-microsoft-standard-WSL2), grok build 0.2.114 (linux-x86_64), strace 6.x

  • GParted Live 从硬盘 GRUB 启动 — 分区调整记录

    GParted Live 从硬盘 GRUB 启动 — 分区调整记录

    日期:2026-07-19
    系统:openKylin 2.0 SP2 (nile),GRUB 2.12
    目的:/ 分区 (40G) 空闲较多,/data 分区 (4G) 不够用,从 / 划一部分给 /data


    磁盘分区布局

    nvme0n1  953.9G
    ├─p1  260M   EFI (SYSTEM_DRV)
    ├─p2   16M   MSR
    ├─p3  400G   Windows-SSD (NTFS)
    ├─p4  500G   Data (NTFS)
    ├─p5    2G   WINRE_DRV (NTFS)
    ├─p6    1G   KYLIN-BACKUP (ext4)  ← ISO 放这里
    ├─p7    4G   /data (ext4)         ← 需要扩容
    ├─p8   40G   / (ext4)             ← 需要缩容
    └─p9    5G   SWAP

    最终 GRUB 配置

    # /etc/grub.d/40_custom
    menuentry 'GParted Live (ISO Boot)' {
        set isofile=/gparted-live.iso
        search --no-floppy --fs-uuid --set=root 344bea2d-4807-497a-b3a1-9e481fc17367
        loopback loop $isofile
        linux (loop)/live/vmlinuz boot=live findiso=/gparted-live.iso components noswap i915.force_probe=7d55
        initrd (loop)/live/initrd.img
    }

    踩坑记录

    坑 1:NTFS 分区上的 ISO 无法启动

    • 现象Unable to find a medium containing a live file system
    • 原因:GRUB 有 ntfs.mod 能在 bootloader 阶段读取 NTFS,但内核接管后 initramfs 没有 ntfs-3g,无法挂载 NTFS 定位 ISO
    • 解决:ISO 必须放在 ext4 分区

    坑 2:ISO 不能放在要操作的分区

    • 原因:ISO 所在分区的 loopback 会使其处于使用状态,GParted 无法操作
    • 解决:放到不参与调整的 p6(KYLIN-BACKUP)

    坑 3:nomodeset 对 Intel 新 GPU 是反作用

    • GPU:Intel Meteor Lake-P Arc Graphics (8086:7d55)
    • 原因nomodeset 禁用 Kernel Mode Setting,Intel 新 GPU 必须开启 KMS 才能正常显示
    • 解决:去掉 nomodeset

    坑 4:i915 驱动不认识 Meteor Lake

    • 现象:系统能启动但 GUI 无法显示
    • 原因:GParted Live 的内核版本可能还未将 7d55 这个 PCI ID 加入 i915 的默认支持列表
    • 解决:添加 i915.force_probe=7d55 强制 i915 认领该 GPU

    操作步骤备忘

    # 1. 下载 ISO 到硬盘
    wget -O gparted-live-1.8.1-3-amd64.iso <url>
    
    # 2. 挂载 p6 并复制 ISO
    sudo mount /dev/nvme0n1p6 /mnt
    sudo cp gparted-live-1.8.1-3-amd64.iso /mnt/gparted-live.iso
    
    # 3. 配置 GRUB
    sudo vim /etc/grub.d/40_custom   # 添加上面的 menuentry
    sudo sed -i 's/GRUB_TIMEOUT=1/GRUB_TIMEOUT=5/' /etc/default/grub
    sudo update-grub
    
    # 4. 重启进入 GParted Live
    sudo reboot
    
    # 5. 在 GParted 中操作
    # - 缩小 p8 (/) 从左边界向右
    # - 扩展 p7 (/data) 右边界向右
    # - 应用操作

    相关 UUID

    分区UUID
    p6 (ISO 所在)344bea2d-4807-497a-b3a1-9e481fc17367
    p8 (/)bf7fae4e-9707-4839-85ac-2d1c07e96eef
    p4 (Data, NTFS)8AECA8EFECA8D6AB