前言:一次长视频导出卡死,我怎样用 DFX Skills 把问题缩到文件复制
从 THREAD_BLOCK_6S 到有界异步 I/O:证据、修复与复测
视频导出最让人误判的一类问题,是编码已经做了很多工作,界面却在保存阶段不再响应。看起来像媒体引擎卡住,日志顶部也可能停在系统内存函数里;如果顺着最显眼的那一行往下猜,很容易越查越远。
这次案例的故障类型是 AppFreeze,记录中的原因是 THREAD_BLOCK_6S。它不是 JS Crash,也不能因为栈里出现 Native 函数就改按 CppCrash 分析。DFX Skills 对我的帮助,是把原始故障记录里的概览、资源、事件队列和线程栈整理到一起,方便交叉核对。真正需要开发者完成的,是把这些证据接回当时的业务代码。
下面使用脱敏后的旧版故障案例。旧问题已经有后续修复,本文讨论的是如何从历史证据定位并复查修复;工具的再次分析不等于当前版本又复现了一次。函数名只保留说明职责所需的通用含义,路径、用户文件和设备标识不进入公开材料。
先确认读的是哪一份现场
我在看堆栈前会先记录四件事:故障类型、发生场景、当时的应用版本,以及代码对应的版本。这里的场景是长视频导出后的本地保存,不能直接拿今天的代码解释旧日志,更不能因为当前函数已经改成异步,就判定旧堆栈不可信。
DFX Skills 的 AppFreeze 分析脚本可以分区输出。下面的路径用 skill-root 表示实际工具根目录,不绑定本机安装位置;命令中的 case.log 应替换为自己的原始 AppFreeze 日志。
python <skill-root>/scripts/freeze/main.py -p case.log --section overview python <skill-root>/scripts/freeze/main.py -p case.log --section resources python <skill-root>/scripts/freeze/main.py -p case.log --section event-queue python <skill-root>/scripts/freeze/main.py -p case.log --section fault-stack 我的阅读顺序是先看 overview 判断事件类别,再看 event-queue 判断主线程上的任务是否推进,然后用 fault-stack 找业务职责,最后结合 resources 排查资源压力。四份输出回答的是不同问题,不能把某一个“异常”标签直接当成根因。
如果工具输出比原始日志少,要回到原始字段确认是不是采集缺失;如果堆栈采集超时,也要把这个事实写进结论。没有采到,不等于对应工作没有发生。
两次快照里的同一个任务,比一个栈顶更有说服力
这个案例中,两次事件队列快照显示,同一个任务的开始时间保持不变,执行时长从约 3.521 秒增长到 6.686 秒,后面等待处理的工作也在累积。它给出的线索是:主线程正在一个长任务里停留,新的事件无法及时被处理。
证据 | 对本案的意义 | 不应扩大的结论 |
THREAD_BLOCK_6S | 系统记录了主线程阻塞故障 | 不能单凭原因名定位具体函数 |
同一任务持续 3.521s → 6.686s | 长任务没有及时让出执行 | 不是精确的函数耗时统计 |
导出 → 文件修复 → 整体写回 | 业务责任落到保存阶段 | 不代表编码耗时为零 |
栈中出现 memset / 内存处理 | 当时可能正分配或填充内存 | 不能直接认定系统库有缺陷 |
堆栈采集有退化或超时 | 必须保留证据边界 | 不能编出热点占比 |
这里最关键的交叉验证是:事件队列证明“卡在哪一类执行状态”,业务栈告诉我“为什么这一阶段会做大量工作”,旧代码再说明“工作规模如何随视频大小增长”。三者能对上,修复才有明确落点。
图 1:两次快照指向同一长任务,再由业务栈与旧代码确认保存阶段的同步工作。
资源区里的高 RSS 需要看,但它单独不能证明泄漏或 OOM。这个案例也没有足够的完整采样去写“某函数占用了多少百分比 CPU”。我会把结论停在证据能支持的位置:主线程执行了与大文件体量相关的同步内存和文件操作,足以解释当前阻塞。
async 函数为什么仍然会卡住页面
旧保存链路会读取较大的文件内容、构造新字节数组,再整体写回。调用者虽然 await 了导出函数,但被调函数内部仍可能在主线程同步执行这些步骤。
async function saveVideo(path) { const wholeFile = readAllSync(path); const repaired = rebuildBytes(wholeFile); writeAllSync(path, repaired); }这段是旧问题模式的缩写,不是可直接运行的文件 API。async 只描述返回值和异步暂停能力,不会自动把函数体搬到工作线程;把同步调用套在 Promise 里,同样不能改变它在哪个线程执行。即使在整段工作之前加一个 await,后面连续的计算与同步 I/O 仍然可能长时间占住主线程。
因此,我没有把修复目标写成“加几个 await”。目标应该是两个可以检查的条件:复制使用的缓冲区有固定上限;长文件的读写通过异步接口推进,避免整文件同步搬运。对于必须连续计算的重活,另行判断是否要进入 Worker 或任务池,而不是用循环中的空 Promise 假装迁移线程。
修复的核心,是把文件大小从内存需求里拿掉
后续实现里,有界复制函数复用一个 1 MiB 缓冲区,显式传递源偏移和目标偏移,并处理提前结束、短写与取消。文件描述符由调用方管理,复制函数不关闭传入的 FD。这几条约定比“用了异步 API”更重要,因为它们决定错误时数据和资源会处于什么状态。
1 MiB 是这份实现的块大小,不是所有设备都应照抄的最佳参数。更准确的内存说法是“复制环节的应用缓冲为 O(块大小)”,而不是“整个导出只用 1 MiB”。媒体编解码器、其他任务和短写时的临时切片仍然占内存。
短写尤其容易漏掉:一次 write 返回 n,只能说明本次写入 n 字节。剩余数据要继续写;返回 0 或负数必须终止,不能让循环一直转。
let written = 0; while (written < readBytes) { const view = buffer.subarray(written, readBytes); const n = await writeAt(view, destinationOffset + written); if (!Number.isInteger(n) || n <= 0 || n > view.length) { throw new Error('write made no valid progress'); } written += n; }这段使用注入的 writeAt 接口,配套 bounded-copy.mjs 将读、写、取消和长度检查组合成完整示例。它能在 Node 中通过模拟短读、短写和错误来复测;迁入 ArkTS 时,应按目标文件 API 适配参数,而不是把 Node 示例说成鸿蒙真机实现。
另外,文件偏移不能只校验初始值。若要面向超大文件,还要检查 offset + copied 是否仍为安全整数,目标写入范围有没有越界。复制固定长度时,源文件提前 EOF 应报错;复制到 EOF 时,正常读到结尾才结束。这两种语义需要在函数参数里说清楚。
文件修复只处理自己理解的范围
长视频保存链路里还有 MP4 首样本修复。这里更不适合为了“通用”而把整个文件读进内存:先有界读取头部,识别自己明确支持的盒结构与字段,能定位到必要修改再处理;遇到不支持的布局就按既定策略退出,不能靠猜偏移去改用户文件。
建议把“无须修复”“完成修复”“不支持此格式布局”“处理失败”分成明确结果。前两种可以继续,后两种由上层决定保留原件、提示失败或走替代路径。把所有异常都吞掉并返回成功,会让后面的图库或上传阶段背上一个难定位的损坏文件。
本案后续代码还增加了长视频导出策略。流式复制解决的是一段操作的内存与响应性,并不能取消总磁盘空间、编解码器实例数和并发任务的上限。临时产物与最终产物并存时,要按真实存储开销判断是否能启动;失败时只删除当前任务拥有的临时文件,不能顺手删除源视频。
用故障注入验证修复,比只换一个大视频更快
配套示例专门让读写返回比请求更少的字节,并在指定位置取消或报错。小数据就能暴露“只写了一部分仍报成功”“无进展无限循环”“取消后继续写”等问题,没必要每次都复制十几 GB 才验证控制逻辑。
真机验收则看另一层:导出期间页面能否交互、取消多久被观察到、峰值内存是否随文件大小明显增长、保存产物是否存在视频轨且可播放,以及反复进入退出后资源是否释放。不同层的测试各管自己的结论。
已有历史记录包含 18 项真机用例通过;超长 4K 素材也有压力路径记录。但对某个超大素材,现有记录并没有完整覆盖“最终导出完成并落盘”的自动化证据,所以不能把它写成超大文件全链路全部通过
DFX Skills 最值得复用的用法
我会保留一份很短的案件笔记:故障发生在哪个版本;哪两条独立证据支持判断;对应哪段代码;修改了哪个复杂度或执行位置;用什么用例验证;还有什么没有验证。这样下次换一个 Skills 版本重新分析,结果也有参照,而不是重新相信一段更流畅的文字。
这次案例里,工具帮助我更快读完现场,旧代码解释了阻塞机制,短写和取消测试把修复要求固定下来。最有用的经验是这三步能够互相核对。缺少其中任何一步,笔记都容易停在“日志分析了一遍,代码改了一处”,遇到相关问题,仍然不知道类似卡点该怎么做。
实现来源与配套内容
分析工具来自 OpenHarmony-SIG / developtools_dfx_skills 的 AppFreeze 分析模块;本文不把工具本身算作个人原创。故障数据来自实际历史案例,公开表格保留时长关系与职责链,移除了真实身份、设备和业务路径。
*写在最后:感谢终端BG软件部的鸿蒙DFX团队研发与开源相关skills,把排障专家请到了每位开发者的身边,让稳定性问题不再是基层普通开发者的大难题。笔者已经通过该skill,修复了一个大量很久补丁的链路,并且基于相关建议进行了完全重构,得到了非常好的结果。也再次提醒,希望使用者可以结合现场日志分析,避免出现分析定位不准的情况。