跳到主要内容

性能诊断

Wework 内置一个默认关闭的前端性能诊断开关,用于定位 release 包中“运行一段时间后变卡”的问题。诊断代码只在显式开启后运行;关闭时不会安装 React Profiler,也不会采集定时样本。

多实例调试​

使用 pnpm --filter wework dev:mac 启动日常 debug 应用时,默认直接使用正式版的 Executor Home,因此项目和任务会与本机正式版 Wework 共享;每个 Wework 进程仍通过自己子进程的 stdio 与 executor 通信,不会发生端点覆盖或误连其他 executor。需要临时隔离项目和任务时,使用 pnpm --filter wework dev:mac -- --executor-isolation。

ai:verify 和桌面 E2E 不使用上述共享默认值。它们会显式创建临时 Executor Home、项目目录、设备 ID 和唯一 Electron app-data namespace,使任务、项目、应用数据以及单实例锁都与正式版和其他验证会话隔离。

开发环境默认让这些实例复用同一个 Cargo target 目录,以便 executor 源码变化后继续使用增量编译产物。需要排查共享构建缓存问题时,可以设置 WEGENT_DISABLE_SHARED_CARGO_TARGET=1,让当前启动过程使用项目内的默认 target 目录。

启动耗时诊断​

桌面启动门控包括本地运行时和当前路由的首屏就绪条件。冷启动时,Electron 直接创建新的 sidecar,executor 通过 stdout 报告 ready。Renderer 在当前首屏所需数据和可操作界面就绪后发送 renderer.startupReady,触发窗口交接。debug 和正式包使用同一交接流程。

初始窗口首选尺寸为 1440×960;任一维度占满或超出所在屏幕的可用区域时,启动页与主窗口都使用系统标准最大化,否则保持普通窗口。最大化交接直接继承最大化状态,避免将整屏尺寸往返换算为普通窗口边框尺寸。

启动页的动画舞台支持原生窗口拖动,底部状态区域和恢复按钮保留交互。舞台与主界面使用同一次启动解析的亮暗主题;采用密度 2.5、速度 1.0 的流星线条从四周勾勒标记,星头在首次接触字形前淡出,标记上只保留勾勒线条,长尾逐渐收细和淡出。浅色模式使用浅白背景与清晰蓝线,深色模式使用发光星头;成形后立即开始从左向右的渐变扫光,等待阶段不再调度 Canvas 绘制。底栏与舞台背景一致,主题在样式和动画首次绘制前确定,避免启动闪色。

完整标记和光丝按窗口及底栏上沿等比缩放,边缘与状态内容之间保留安全间距。若极短窗口的底栏已盖过全页中心,暂时不绘制装饰图标;空间恢复后继续同一条动画时间轴。

交接时,启动页在约 200ms 内补齐成形、冻结当前扫光色位并将进度推至 100%。完成帧经过两次绘制确认并停留 50ms 后,宿主对齐主窗位置和尺寸,使用 capturePage() 确认主窗已有非空合成帧,再以 600ms 原生窗口透明度淡出启动页(macOS/Windows)。Linux 的 Electron 原生窗口透明度不生效。启动页关闭并将焦点交给主窗口后,宿主记录启动成功。启用减少动态效果时,直接呈现完成帧并跳过收束、停留和淡出动画。

排查启动页长时间不消失时,对齐 Frontend logging initialized、app IPC stdio ready、当前路由的数据加载记录(例如 runtime work list finished)与宿主的 renderer-startup-ready、main-window-show、startup-splash-close 记录,分别判断 executor 冷启动、首屏数据加载和窗口交接的耗时。renderer-startup-ready 到 startup-splash-close 之间包含完成帧呈现、主窗合成和淡出,应单独计量。后台云端同步耗时通过对应同步日志排查。

桌面 E2E 的普通页面重载统一复用 workspace-flows.mjs 中的 reloadMainWindow。 控制通道连接不代表原生启动交接已结束;该函数先等待启动页关闭,再重载并等待新控制客户端。 场景内直接组合 reloadApp 和 awaitReadyAfter 会绕过这一门禁,可能使交接中的 capturePage() 失效并隐藏主窗口。

首屏门禁与闲时任务​

工作台首屏门禁要求本地 executor 可用,并满足当前路由的数据与可操作条件。需要 Codex 的功能会在用户实际使用时启动它。插件自动升级、内置插件市场准备和临时图片清理等维护工作通过统一的 renderer 闲时任务调度器执行。

闲时任务同时满足以下条件才会开始:

  • 工作台首屏已经可用。
  • 用户近期没有键盘、指针、触摸或滚轮操作,且 renderer 获得足够的 idle callback 时间片。
  • Electron 主进程采样的系统 CPU、可用内存和系统空闲时间未达到压力阈值。

同一任务 ID 的待执行请求会合并,所有闲时任务串行启动。用户重新操作时,尚未开始的任务会延后;压力探测失败时也不会执行任务。调度器只决定何时开始,不允许任务在 renderer 主线程执行同步重活:文件扫描和复制必须进入 Executor 的 blocking 线程池,目录清理使用 Electron 主进程异步 I/O,网络同步保持异步。

内置插件市场在首屏后计算确定性的内容 SHA-256。目标目录中的 .wework-content-sha256 与当前内容一致时不再复制;内容变化时才通过 staging 目录替换,并把 hash 纳入 Codex 本地市场注册键。这样 Wework 升级后仍会及时刷新内置插件,而相同版本启动不会重复复制整个市场。

Runtime 任务创建诊断​

前端会将任务创建关键阶段以 [Wework] Runtime task create diagnostic 前缀写入持久化日志,无需开启性能诊断。记录只包含阶段、任务/设备标识、模型标识、耗时和结果,不包含消息正文、凭据或模型连接配置。

排查“乐观任务已显示,但 executor 没有收到 runtime.tasks.create”时,按以下顺序查找最后出现的阶段:

  1. workbench-model-prepare-*:工作台发起并完成首次模型准备。
  2. workbench-runtime-create-dispatched:工作台开始调用 runtime 创建接口。
  3. hybrid-local-device-discovery-* 和 hybrid-route-resolved:混合服务完成设备发现并选定本地或云端路由。
  4. hybrid-create-forwarded:创建请求已转发到选定的 runtime API。
  5. local-device-resolved、local-primary-model-prepared、local-supervisor-model-prepared 和 local-payload-built:本地/远程 Executor IPC 客户端完成设备解析、模型准备和 payload 构建。
  6. local-rpc-dispatched 与 local-rpc-resolved:runtime.tasks.create 已发出并返回。
  7. hybrid-create-resolved 或 hybrid-create-failed:混合服务观察到最终结果。

缺少下一个阶段通常表示调用停在两条记录之间。结合相同 taskId 对齐前端日志、云端 WebSocket RPC 日志和 executor 日志,确认是模型同步、设备发现、payload 构建、IPC 发送还是 executor 响应卡住。

开启方式​

在 Wework 窗口中按隐藏快捷键:

macOS: Cmd + Option + Shift + P
Windows/Linux: Ctrl + Alt + Shift + P

快捷键会打开 Developer Commands 菜单。选择 Enable Performance Diagnostics 会写入 localStorage 中的 wework:perf-debug 标记并自动刷新应用;再次打开菜单选择 Disable Performance Diagnostics 会关闭诊断并刷新。

开发环境也可以通过 URL 参数临时切换:

?weworkPerf=1  # 开启
?weworkPerf=0 # 关闭

也可以设置构建/运行环境变量 VITE_WEWORK_PERF_DEBUG=1,用于本地复现时默认开启。

Debug 面板​

Developer Commands 菜单中的 Debug Panel 用于排查 Wework 当前运行任务的问题。面板会展示:

  • 当前活跃 runtime task 的地址、任务是否可识别、running 原始值、任务状态和 pane 派生运行状态。
  • 当前 pane 的发送阶段、消息数量、队列、transcript 加载状态、subagent 状态和 goal 状态。
  • Transcript 加载消息与当前流式输出消息的字段和预期 UI 样式对比。
  • 最近的 console.debug 日志。

Debug 面板可以展开、收起、刷新、复制快照和清空日志。收起后只保留右下角状态条,避免遮挡主界面。

Runtime 内存快照​

Debug 面板的快照会附带当前 runtime pane 的轻量内存摘要,用于定位 WebView 或 executor 内存异常:

  • 消息数量、角色分布、状态分布和正文长度汇总。
  • processing block 数量、类型分布和工具输出长度汇总。
  • 队列消息、guidance 消息、代码评论上下文和 transcript 范围状态。
  • 当前 runtime task 在 work list 中的 running 原始值,以及由 pane 推导出的运行状态。

快照只记录摘要,不会把完整命令输出、原始 Codex 事件或完整 transcript 内容复制到 Debug 面板。需要排查原始内容时应查看 executor 日志或 DevTools 采样,而不是通过前端快照搬运大文本。

Runtime transcript 与列表载荷​

为降低前端和 executor 的内存压力,runtime task 列表、runtime handle 摘要和 transcript 响应只保留 UI 必需字段。命令输出、streaming delta、cached message、原始请求/响应等大块原始载荷不会放进 runtime work list 发送给前端。

前端显示对话时仍以 transcript/message action 生成的 WorkbenchMessage 为准;任务列表和状态轮询只用于状态、标题、运行态和 workspace 信息。排查“列表很慢”或“切换任务内存上涨”时,应优先确认是否又把原始消息或命令输出加入了 runtime list/handle/transcript 元数据路径。

Codex 0.147 的历史读取必须遵循线程持久化时确定的 historyMode。Executor 先通过 thread/read(includeTurns: false) 读取元数据:paginated 线程使用 thread/turns/list(itemsView: notLoaded),再通过 thread/items/list 加载每个 turn 的完整 item;legacy 线程则直接使用 thread/turns/list(itemsView: full)。旧 rollout store 不支持 thread/items/list,因此不能仅根据 Codex 版本强制走 item 分页。

Codex 会从历史 API 中过滤 <codex_internal_context>,因此 Wework 发送目标或续接请求时必须按 clientUserMessageId 保留可见用户内容,并在加载 transcript 时与 provider item 合并。对于升级前没有本地 presentation 的 legacy 会话,仅当最旧页的首个 turn 完全没有用户消息时,才使用 thread preview 恢复首条需求;首个 turn 已包含文字、图片或附件消息时不得重复补写 preview。

Pane 缓存与资源生命周期​

桌面工作台最多缓存 10 个普通 pane,并按最近使用顺序淘汰。非活跃且已停止运行的 pane 会释放 transcript 消息、历史 DOM、分页范围、导航索引和 processing 展开状态;再次切回时从 runtime transcript 原始数据重新加载。

Electron 对话统一使用 @tanstack/react-virtual 的消息行虚拟列表,不再根据消息数量切换实现。跟随态和阅读态均使用 anchorTo: 'end';阅读态关闭 followOnAppend 和末端容差,导航与滚动条拖动时才切换为 anchorTo: 'start'。滚动快照保存距底部距离,阅读态额外保存消息及文字锚点身份。同步布局与共享 ResizeObserver 通过同一个 resizeItem 入口写入真实行高。活动中的流式消息即使位于可见范围和 overscan 之外,也必须保留在虚拟 range 中,使其高度增长持续进入 TanStack Virtual 的测量;否则消息重新挂载时从估算高度切换到真实高度,会破坏历史阅读位置。用户停留在底部时,高度变化继续按末端距离补偿;用户主动向上滚动后,则记录视口内首个文本滚动锚点及其视口偏移,并在流式消息重新测量时恢复该文本锚点。这样既能保持底部自动跟随,也能避免正在阅读的文本随流式输出持续向上漂移。渲染范围在可见区前后各保留 2 条消息。消息行不再使用 IntersectionObserver 做第二层窗口化;单条超长 Markdown 仍保留独立的块级窗口化,以限制一个可见消息内部的 DOM 数量。未挂载富文本的块保留轻量纯文本内容,避免快速滚动时出现只有高度、没有可绘制内容的空白区。其余 IntersectionObserver 用途包括跟随底部状态和附件预览等独立功能。

桌面对话使用正常 DOM 坐标:顶部 scrollTop = 0,底部为 scrollHeight - clientHeight。TanStack、滚动条和导航共用该范围,不再转换负偏移。useConversationVirtualizer 管理虚拟几何,控制器管理跟随、阅读、导航和拖动;行高及 Markdown 缓存按宽度、字体和正文校验,普通滚动不自动启动诊断采集。

非分屏工作台由 pane 栈父容器持有共享内容宽度,并把稳定值传给新任务 Pane;Pane 自身测量只作为首次启动、零宽测量和分屏模式的回退。环境信息栏是否停靠必须在新 Pane 的首帧使用这份共享宽度决定,不能先按宽度 0 渲染全宽聊天区,再在下一帧扣除 320px 侧栏,否则消息和 Composer 会出现一次横向闪动。

单条 assistant 消息可能包含大量工具块,并被拆成多个 ToolBlocksDisplay 段。依赖完整消息上下文的派生数据(例如文件编辑耗时)必须在消息级别只计算一次,再映射到各显示段;不得让每个显示段重复扫描整条消息。没有对应展示块时应直接走空结果快路径,工具名称匹配也应避免为每个 block 创建拆分数组或集合。

每个对话只缓存有界的 TanStack 测量快照,并与距底部滚动快照一起恢复。修改这套逻辑时,应覆盖短对话、长对话、底部流式跟随、屏幕外流式消息持续测量、向上滚动后的文本锚点稳定性、历史位置恢复、切换后重开、导航强制挂载和归档缓存淘汰。

Terminal 和内置浏览器属于有状态活动资源,不跟随普通 pane 淘汰。只要 pane 中仍有 Terminal 或浏览器标签,它就保持挂载,以保留终端进程和网页会话;关闭对应资源后,该 pane 才重新受普通缓存上限约束。修改这条边界时,必须同时覆盖普通 pane 的 LRU 淘汰、资源 pane 保活、消息行虚拟化和桌面内存 E2E。

云端终端压测与验收​

协议边界和发布顺序见云端终端兼容与发布。以下命令从仓库根目录串行执行;需要已安装项目依赖、uv、Cargo 和 redis-server。

env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh quick
env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh baseline
env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh capacity
env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh over-capacity
pnpm --filter wework e2e:desktop -- --cloud-only --segment core-task-flow
  • 上述压测启动仅绑定 127.0.0.1 的临时 Redis,结束后销毁;不需要真实密码、用户 Token 或 API Key。load-token 只是 Executor 内存测试的占位值。
  • 只有显式保留 TERMINAL_LOAD_REDIS_URL 才连接指定 Redis;应使用专用测试实例。脚本只清理本轮已知的精确 session key,不扫描或清空 Redis;共享实例的 INFO 包含其他流量,不能归因于本轮测试。
  • baseline 覆盖 3 个 Backend / 1000 会话;capacity 和 over-capacity 覆盖单 Backend 的 8192 / 9000 会话,用于验证不均匀分布、缓存容量和淘汰。
  • Backend 脚本验证 session store/cache 与跨实例失效;Executor 脚本使用生产 LocalSessionHandler 和内存 PTY,验证序号、消费 ACK、consumer 接管、回放及背压。它们不包含完整 Socket.IO、真实网络或 xterm,吞吐不能当作端到端容量。
  • 真实 Electron E2E 覆盖 v1/v2 渲染、缺省/v1/v2 attach、关闭 v2 后协商、超过 512 KiB 的输出、resize、重连、协议固定和关闭;v1 不发送消费 ACK。Backend 重启后先等待本次进程的目标设备注册日志,再检查在线状态,避免 Redis 残留记录造成误判。证据位于 wework/test-results/desktop-e2e/<run>/terminal-compatibility-*.json。桌面构建共用资源目录,不能并行执行。

上线验收门禁​

本地测试不能替代生产等价环境的长时间验证,也不能替代实际待发布/回滚二进制的混合版本验证。2026-09-07 本地单元/契约测试、两档压测和真实 Electron E2E 已通过;完整历史产物矩阵及以下长压仍待完成,不视为已经获准上线。完整 CI 分类器测试也仍需 Bash 4+ 环境执行。

每组至少运行 30 分钟,使用真实 Backend、Redis、Executor PTY 和 Wework,覆盖多 Backend 及单副本倾斜:

会话数单会话输出ACK 延迟目的
10010 KiB/s0ms基线
50010 KiB/s50ms常规并发
1000100 KiB/s200ms高吞吐
30001 KiB/s50ms大量低频终端
819210 KiB/s200ms单 Backend cache 容量
90001 KiB/s50ms超容量淘汰
1000100 KiB/s1000ms强制背压

每组覆盖随机断开 10% 客户端、断网 3 秒后恢复连接,并在连接恢复后 1 秒内重新 attach,验证 v2 开始补发;另覆盖 ACK 延迟/丢失、10 MiB 突发输出、五终端并行、主题切换和隐藏后激活。另测 Backend 重启、Redis 失效、旧 consumer 控制、错误握手及已有会话切版本被拒绝;确认会话身份和输出标记,不能只凭界面恢复判断无损。结束后清理测试会话和隔离资源。

验收目标(不是本地脚本已测得的性能结论):

  • v2 序号缺口、重复写入、静默丢失为 0;v1 验证基础输入输出和重连可用,不要求其不存在的无损回放能力。
  • Executor 回放不超过 512 KiB;384 KiB 暂停读取,降到 128 KiB 或以下恢复。30 分钟内存趋于稳定,不随总输出线性增长。
  • 同地域输入回显 P95 ≤ 50ms,扣除网络 RTT 后链路 P95 ≤ 20ms:每个样本先计算 回显延迟 - 配对 RTT,再将差值升序排列,取第 ceil(0.95 × 样本数) 项(与 Backend 压测脚本的 nearest-rank 算法一致),不能将两个 P95 相减;连续 yes 后 Ctrl-C 停止 P95 ≤ 200ms。
  • 断网 3 秒后,连接恢复 1 秒内重新 attach,v2 开始补发;10 MiB 突发后 Renderer 可交互,结束标记及后续命令正常显示。
  • 隔离 Redis 压测增量 SCAN=0、KEYS=0;共享 Redis 用代码与精确 key 操作计数核对,不用全局计数归因。

压力与观测​

  • 已鉴权的稳定数据转发不访问 SQL;session cache 命中不查 Redis 元数据,但 Socket.IO 多副本 Pub/Sub、ACK 转发仍有 Redis 流量。建连、鉴权及会话创建另计。
  • 每个 Backend 默认缓存 8192 条,活跃授权错峰每 4~5 秒精确重校验;超过容量观察 miss/eviction。每进程复用 Redis pool,Pub/Sub 独占池内连接,不设置终端专属连接上限。失效 listener 断线时拒绝使用缓存授权,重连清空缓存。
  • 同时采集 CPU、内存、网络吞吐、Socket 输出/ACK 事件、Redis 连接和拒绝数,区分有效输出与两跳转发开销。
  • Backend 指标:terminal_ws_events_total、terminal_session_cache_requests_total、terminal_session_cache_evictions_total、terminal_session_store_operations_total、terminal_session_store_duration_seconds。
  • Executor /metrics:terminal_output_batches_total、terminal_output_bytes_total、terminal_replayed_batches_total、terminal_replay_bytes、terminal_ack_lag_bytes、terminal_backpressured_sessions。
  • Wework 性能事件:remote-terminal-write、remote-terminal-replay-request。指标、trace 和诊断日志不记录终端原始内容或凭据;Redis 观测使用 INFO commandstats,禁止 SCAN、KEYS、MONITOR。

本地 Codex 流式日志​

本地 executor 的 Codex 调试日志默认保留 delta 详情,便于定位流式输出顺序、阶段识别和最终内容覆盖问题。默认会记录 Codex 原始 delta 与运行态分类摘要。

Developer Commands 菜单中的 Enable Stream Logs / Disable Stream Logs 会同时切换前端本地 chat stream 日志和 Codex executor stream 日志。优先使用这个菜单项做现场排查;它会让前端 console.debug 中的 stream 订阅/事件日志与 executor 的 Codex stream 详情保持同一个开关状态。

为避免 debug 包在长回复或高频 token 输出时产生过多日志,runtime work 内部的 cache/emit mapping 日志默认关闭。这类日志会为同一个 delta 额外记录缓存和 UI 事件分发路径,只有排查本地 runtime work 路由时才需要打开。

可用环境变量:

WEGENT_CODEX_STREAM_DEBUG=0          # 关闭 Codex 原始 delta / 分类详情
WEGENT_CODEX_STREAM_DEBUG=1 # 开启 Codex 原始 delta / 分类详情(默认)
WEGENT_CODEX_STREAM_MAPPING_DEBUG=1 # 开启 runtime work cache/emit mapping 详情

流式消息渲染​

Wework 将 executor 高频到达的文本增量与 Markdown 展示节奏分离。消息状态仍实时接收并保持完整内容,AssistantMarkdown 使用轻量缓冲器在浏览器帧上逐步推进可见文本:积压较多时自适应加速,接近队尾时保留少量字符并缓慢释放,以平滑 executor 批量发送和短暂空档造成的视觉顿挫。流结束、内容被替换或发生非追加更新时会立即对齐完整文本,不会影响最终消息正确性。

过程文本、thinking 和 plan block 的实时更新只传输 content_delta,不能在每个 delta 或完成节点重复发送从开头累计的完整 content。完成节点通常只携带 status: done;若 provider 的完成快照仅比已流式内容多一个尾部,则只补发该尾部增量。Wework 在浏览器动画帧内合并同一 block 的连续增量后再更新 React 状态,避免长程任务形成二次方级别的 IPC 复制和渲染压力。流式 block 更新属于可丢弃的 bulk 事件;发生 app IPC 背压时,终态事件仍保持优先,并由 transcript 对账恢复权威内容。Electron 主进程不得为每个 response.* 增量写 stdout;终端或父进程未及时消费输出时,同步 console 写入会阻塞 Browser 主线程和全部窗口输入。

流式消息不执行 Pretext 全文高度测量,而是使用稳定的离屏占位高度;消息完成后再精确测量并缓存。已完成消息优先按消息对象和宽度命中高度缓存,避免每次流式更新都重新计算历史消息全文 hash。Composer、Workspace 操作栏、右侧工作区和底部终端也使用稳定属性与 memo 边界,避免随每个文本增量重复渲染。

拖动底部终端面板调整高度时,高度更新按浏览器动画帧合并,并在拖动期间关闭高度过渡,避免高频指针事件触发过量 React 更新和终端重排。松开指针时必须提交最终高度,并恢复面板打开、关闭所需的过渡效果。

排查流式卡顿时应区分以下情况:

  • 帧率稳定但输出一阵快、一阵慢:检查 stream message 事件间隔,通常是 executor 增量批量到达或存在网络/IPC 空档。
  • 存在长帧、密集样式重算或 Markdown 解析:检查是否绕过了文本缓冲、破坏了 Streamdown 组件引用稳定性,或重新引入了逐字符 DOM 动画。
  • GC 时间异常高:确认 DevTools 是否开启 Heap Allocations。该采集项会显著放大长时间录制中的 GC;只排查交互流畅度时应关闭它。

流式文本缓冲的单元测试位于 wework/src/components/chat/useBufferedStreamingText.test.ts。修改缓冲水位或推进速率时,应同时验证 Unicode 字符边界、非追加更新和流结束立即对齐行为。

常驻动画渲染​

状态旋转图标和文字扫光属于常驻动画:即使页面没有输入或流式消息,它们也可能持续驱动 Web Content 进程。实现这类效果时,应保持每帧只更新可合成的 transform 或 opacity,避免动画化 background-position、mask-position 等需要重新绘制的属性,也不要直接旋转带有复杂描边的 SVG。

旋转图标应把静态 SVG 放在固定尺寸的 HTML 包装层中,由包装层执行 transform 动画并声明 will-change: transform。文字扫光应始终正常渲染基础文字,并使用固定宽度的裁剪窗口扫过单个高亮文字副本;窗口与副本通过方向相反、时间一致的 transform 保持文字位置不变。不得按字素拆分常驻动画,因为动画节点和 animationiteration 事件会随文字长度线性增长。用于柔化扫光边缘的渐变蒙版必须保持静态,不能动画化蒙版位置。两类实现都必须保留 prefers-reduced-motion 行为。

不能仅凭 CSS 属性名称判断动画是否进入合成线程。will-change 只是提示,clip-path 等看似可合成的属性在实际 Electron 版本中仍可能每帧触发主线程工作。修改常驻动画后,应在同一 Electron 版本、相同元素数量和相同窗口状态下分别录制至少 10 秒的旧实现与新实现,并验证:

  • 暂停动画后 CPU 明显下降,以证明动画与占用存在因果关系。
  • 新实现不再随帧产生 Paint 或 PaintImage。
  • UpdateLayoutTree 和 Layerize 的事件数量与总耗时不再随动画帧持续增长。
  • 动画方向、周期、颜色、尺寸和可见状态与修改前一致。

如果新实现仍持续产生绘制或图层化事件,应继续修改实现,而不是以视觉上流畅或平均 CPU 偶尔下降作为验收依据。

采集内容​

开启后,诊断模块会采集:

  • 浏览器 Long Task。
  • 事件循环延迟,超过 120ms 时记录。
  • 每 5 秒一次的内存、DOM 节点数、resource 数量和可见性状态。
  • React root commit 耗时,超过 24ms 时记录。
  • 手动标记事件。

最近 300 条事件保存在内存中,并暴露在 window.__WEWORK_PERF__ 上。诊断数据不会上传到服务端。

现场取证​

release 包默认编译 Electron DevTools 能力,但主 WebView 默认保持不可检查状态,因此其 Chromium 原生右键菜单不包含 Inspect Element。按隐藏快捷键打开 Developer Commands 并选择 Open DevTools 时,原生侧才会动态设置 webContents.openDevTools() 并打开 Inspector;该入口与 Performance Diagnostics 开关相互独立,要求 macOS 13.3 或更高版本。内置浏览器子 WebView 只在 debug 构建中启用 Inspector;macOS 会在 Inspector frontend 首次显示前强制 detach,因此 F12 打开独立窗口,不会停靠、重置子视图尺寸或覆盖工作台。release 构建通过显式 build cfg 禁用子 WebView Inspector。需要构建不含主 WebView Inspector 能力的发行包时,设置 WEWORK_RELEASE_DEVTOOLS=0。如需在本地诊断启动时自动打开主 WebView Inspector,可以用环境变量:

WEWORK_WEBVIEW_DEVTOOLS=1 /path/to/WeWork.app/Contents/MacOS/WeWork

DevTools 打开后,在卡顿后执行:

window.__WEWORK_PERF__.snapshot();

返回值包含当前 URL、页面可见性、DOM 节点数、内存快照、导航时序、resource 数量、最近事件,以及 Wework 进程组快照。macOS 上 Chromium XPC 进程会被系统改挂到 PID 1;诊断会通过 LaunchServices 将当前 Wework 实例对应的 Web Content、GPU 和 Networking 进程重新关联进来。

Performance Diagnostics 有意保留独立的采集器:它仍通过 wework/electron/src/host/process-diagnostics.ts 读取 macOS ps 的 RSS;Process Explorer 则使用 Electron metrics 与 systeminformation,其中 Electron 管理的进程使用 Electron workingSetSize。两者数值接近但不可互换:不要复用另一侧的内存阈值;如果要合并采集链路,应作为独立变更重新建立 memory E2E 基线。

进程组同时提供 rss_kib 和 physical_footprint_kib:RSS 包含共享映射和可回收驻留页,通常明显高于真实内存压力;判断泄漏或系统资源占用时应优先比较 physical_footprint_kib,RSS 只作为地址空间驻留的辅助指标。需要持续观察时可以多次执行,重点比较:

  • memory.usedJSHeapSize 是否持续上涨。
  • processMemory.groups[].physical_footprint_kib 是否在任务结束并冷却后仍持续上涨。
  • 增长来自 webkit-webcontent、codex-app-server、executor 还是 main。
  • domNodeCount 是否持续上涨。
  • 是否存在密集 longtask 或 event-loop-lag。
  • 是否存在重复的 slow-react-commit。

主工作台的全高侧栏和横跨内容区的顶部栏应使用普通语义背景,避免在大面积常驻表面应用 backdrop-filter。这类滤镜可能让 Chromium 为整个区域保留额外图形 backing store;排查 Web Content 内存时,应在相同窗口尺寸和页面状态下比较启用前后的 physical_footprint_kib,并把 DevTools 堆快照产生的短期可回收内存高水位排除在稳定基线之外。

流式进度文字的扫光宽度直接使用 ResizeObserverEntry 的尺寸,不在挂载时同步读取 getBoundingClientRect()。尺寸通知到来前,扫光层以零宽度初始化,基础文字保持可见。长轮次会反复挂载进度提示,同步读取布局后再写入样式会累积强制布局开销;排查时应使用同一 CI 构建的 source map 将性能追踪热点映射回源码。

也可以手动打点:

window.__WEWORK_PERF__.mark("before-open-task", { taskId: "..." });

关闭方式​

再次按隐藏快捷键打开 Developer Commands 菜单,然后选择 Disable Performance Diagnostics 会关闭诊断并刷新应用。也可以在控制台执行:

localStorage.removeItem("wework:perf-debug");
location.reload();

关闭后 window.__WEWORK_PERF__ 不再安装,React Profiler 也不会包裹应用根节点。