性能诊断
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”时,按以下顺序查找最后出现的阶段:
workbench-model-prepare-*:工作台发起并完成首次模型准备。workbench-runtime-create-dispatched:工作台开始调用 runtime 创建接口。hybrid-local-device-discovery-*和hybrid-route-resolved:混合服务完成设备发现并选定本地或云端路由。hybrid-create-forwarded:创建请求已转发到选定的 runtime API。local-device-resolved、local-primary-model-prepared、local-supervisor-model-prepared和local-payload-built:本地/远程 Executor IPC 客户端完成设备解析、模型准备和 payload 构建。local-rpc-dispatched与local-rpc-resolved:runtime.tasks.create已发出并返回。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 延迟 | 目的 |
|---|---|---|---|
| 100 | 10 KiB/s | 0ms | 基线 |
| 500 | 10 KiB/s | 50ms | 常规并发 |
| 1000 | 100 KiB/s | 200ms | 高吞吐 |
| 3000 | 1 KiB/s | 50ms | 大量低频终端 |
| 8192 | 10 KiB/s | 200ms | 单 Backend cache 容量 |
| 9000 | 1 KiB/s | 50ms | 超容量淘汰 |
| 1000 | 100 KiB/s | 1000ms | 强制背压 |
每组覆盖随机断开 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 也不会包裹应用根节点。