Skip to content

[epic] macOS 交互帧预算:声明式 UI 热路径与可调节表面 #467

Description

@xiaoyumuxi

这是什么

本 issue 是 macOS 参考产品交互帧预算 的 tracking / 母任务,不是一次性重写 CodeEditorView 的 PR。

仓库里已经有过几轮针对性修复(分栏拖动合并、脏标记不每键广播、Git 图行级 EquatableLitheSplitPaneView 把拖动尺寸留在容器内)。这些能消掉某些 hitch,改不了「SwiftUI 响应式树 + TextKit 富文本 + 热路径 O(N)」这条天花板

目标:把卡顿从「感觉修一处 UI」收成可拆、可验收、可回滚的子任务。Windows 不在范围内。

#322 的区别

任务 谁拥有 不拥有
AppModel 拆分、scoped Feature、工具窗不再 @EnvironmentObject AppModel #322 不写 GPU、不写高亮、不写 LSP 协议
输入 / 搜索弹层 / 全部分栏 / Git 图 / LSP 的帧预算 本 epic 不重开一套 AppModel 架构

扇出的实施见 #322。本 epic 只加性能不变量:场景 T/N 下,已经 dirty 的按键不得触发 Workbench 重求值;scheduleObjectWillChangeRelay 在输入热路径上为 0。

不要按模块拆,按热路径拆

可弹出、可拖大小的表面很多(底栏 Git / Terminal / Run / Tests / Debug / Maven / Problems、侧栏工程树 / Changes / Search / Database、Git Log 内部三条分隔条、Diff 双栏、Search Everywhere 等)。它们共用同一套壳,不是每个模块都要单独重构绘制。

判断要不要新开子 issue 的尺子:

这个表面是不是有一条别人没有的热路径(每键 / 每帧 / 每个 PTY chunk / 每次滚动)?
若只是「开着时跟着 AppModel 抖一下」——那是 #322,不要新 issue。

表面全清单(综合覆盖,不是只修 Git 图)

A. 工作台壳上的分栏(一次修好,所有工具窗受益)

WorkbenchView 自己有两条 SplitHandleView

  • 左侧栏宽度(工程树 / Changes / Search / Database / PR)
  • 底部工具窗高度(Git Log / Terminal / Run / Tests / Problems / Maven / Spring / Debug 互斥叠在同一条底栏上)

拖终端、拖 Git、拖 Run,拖的是同一条底栏

B. 工具窗内部的分栏(issue 325 点名的 8 条 + Diff)

位置 条数 文件
Workbench 侧栏 + 底栏 2 WorkbenchView.swift
Git Log 内部(分支树 / 图 / 详情) 3 GitLogView.swift
Changes 提交区 1 ChangesSidebarView.swift
Run 配置列表 1 RunView.swift
Tests 列表 1 LanguageTestsView.swift
Git Worktrees 另有 GitWorktreesView.swift
Diff 双栏 1 套独立实现 DiffSplitPaneView.swift(尚未迁到 LitheSplitPaneView

这些全部是 D1 的覆盖清单。少测一条,就会再出现「Git 修好了终端还阶梯」。

LitheSplitPaneView 已把「拖动尺寸留在容器、松手 onCommit」做成公共组件。D1 的工作是:所有可拖分栏都迁进去,禁止再把逐帧宽度写进 Feature @Published

WorkbenchModuleUIRegistry 已用 ModuleToolContent: Equatable 避免底栏每帧因 AnyView 重求值。新 renderer 必须带 contentIdentity

C. 弹层 / overlay

表面 类型 归属
Search Everywhere overlay,每次 new 完整视图 S
Replace in Project overlay 并进 S
Settings / Clone sheet #322 即可
Go to Line dialog 非目标
Branch switcher / Git filter popover #322
Implementation chooser overlay 先并进 S 或小修补

D. 底栏 / 侧栏模块怎么归类

模块 打开/切换顿 拖大小 内容绘制
工程树 #322 D1 侧栏 暂不单独开;大仓滚动若 SwiftUI 爆了再开
Changes #322 D1 不单独开绘制
Search 侧栏 #322 D1 与 S 是两条:侧栏 vs 全局弹层
Database #322(模块激活) D1 第一阶段不做
Git Log #322 D1(外+内) 只有提交图 → D2
Terminal #322 D1 底栏 已是 session.nativeView / AppKit,不开绘制引擎
Run / Maven / Tests / Debug #322 D1 输出若整份刷,基线后再决定是否开 perf-output
Problems / References / Spring #322 D1 列表,不是 16ms 热路径
Diff 审查 #322 DiffSplitPaneView → 迁入 D1 内容偏编辑器,严重再并进 T
History / GitHub PR #322 侧栏或无 第一阶段不做

真正独有绘制的只有:编辑器(T)Git 图(D2)、以及基线证明之后才可能出现的工程树/输出控制台。终端画面已经是保留模式 NSView,差的是 chrome 订了整个 AppModel

根因(压缩保留,方案写在子 issue)

对比常见编辑器:

VS Code IntelliJ IDEA Lithe macOS
用户看到的壳 Electron / Chromium IntelliJ Platform UI SwiftUI + AppKit
真正决定输入手感的东西 Monaco:GPU canvas,只画可见行 EditorImpl:视口绘制 + 增量 Lexer NSTextView + TextKit:通用富文本控件
输入热路径走不走 UI 框架 diff 不走 不走 走。任意 Feature 可能打醒整棵 Workbench
分词 / 高亮 Worker 里增量 TextMate / tree-sitter 增量 Lexer 主线程 NSRegularExpression,多趟扫文本
命令面板 常驻、预热、虚拟列表 索引预热 每次现场 new 一个完整 SwiftUI 弹层
搜索后端 独立进程 ripgrep Indexes + Stub Index JSON C ABI,首次还要 activateCapability
进程隔离 Renderer / Extension Host IDE 进程 + 外部工具链 可选模块跑在应用进程内

按体感权重:

  1. NSTextView + 主线程正则 + 每键整串拷贝(textDidChangeapplyLiveEditorText(textView.string)
  2. AppModel.scheduleObjectWillChangeRelay() 扇出到整棵 Workbench([重构] 按职责拆分 AppModel 并收敛 Git UI 边界 #322
  3. SwiftUI 承担本该是保留视图的分栏 / Git 图 / overlay
  4. JSON C ABI + LSP 全文 didChange 退路(initializesynchronization: {}
  5. 模块冷启动、WebKit 冷启动、无 signpost

相关症状 issue(已关 ≠ 根因消失):#161 #162 #171 #187 #324 #325

子任务拆分(本 issue 只当母 issue)


#467 [epic] macOS 交互帧预算
├── #322 已有:AppModel / Workbench 所有权 ← 只链接
├── #perf-0 基线:fixture + signpost ← 第 0 周,先合、不合优化
├── #perf-T1 禁止每键 textView.string
├── #perf-T2 增量高亮(删全文注释正则)
├── #perf-T3 LSP incremental didChange + 诊断合帧
├── #perf-S Search Everywhere + Replace in Project 常驻/预热
├── #perf-D1 所有可拖分栏迁到 LitheSplitPaneView(含 Diff)
└── #perf-D2 Git 提交图改一块 NSView 脏矩形

GPU / 视口编辑器先不要建子 issue。 等 T1+T2 合完、500 KiB 输入仍常 > 33ms 且 Instruments 顶栈是 NSLayoutManager,再开 #perf-engine

实施顺序(不可按模块兴趣并行开火):


perf-0 → #322(可与 0 部分重叠) → T1 → T2 → T3 → S 与 D1/D2 可并行

T 系列必须串行。S 和 D 在 T1 合完后可以两人并行。终端 / Run / Tests 不另开绘制 issue,它们吃 D1 + #322

每个子 issue 只拥有一个不变量,模板:不变量 / 改哪些文件 / 验收数 / 明确不改。禁止一个 PR 同时改高亮、分栏、Search 和 LSP。

第 0 周场景(先立尺子)

用提交进仓库的 fixture,不要拿每个人的 Spring 大仓当测试床。每场景 3 次中位数。Release 测体感,Debug 只看栈。

ID 场景 假设
T 10 KiB / 500 KiB / 2 MiB 连续输入 10 秒,停笔 2 秒 每键 O(N)、LSP、blame
D 连续拖:侧栏、底栏、Git Log 内部分隔条、Run、Tests、Diff 分栏尺寸打进大页面
S 冷启动后第一次双击 Shift 开/关 20 次;再热路径 20 次 overlay 创建、模块激活
N 打开 500 KiB 后空闲 10 秒 扇出、poll、FPS monitor
Term yes 或狂吐日志 10 秒 若只有工具栏抖 → 只做 #322;若栈顶是 SwiftUI Text 再开 perf-output
R Run/测试输出刷一屏 同 Term

埋点:editor.input / search.everywhere / split.drag / appmodel.relay。测量期 FrameRateMonitor 关掉或降到 2Hz(它每个 vsync 都 Task { @MainActor })。

非目标

  • 不在一个 PR 里重写 234KB 的 CodeEditorView.swift
  • 不改 JSON 命令名 / C ABI / 错误码 / 跨平台契约
  • 不把 macOS 方案套到 Windows
  • 不把 debounce 从 180ms 改成 50ms、不把 16ms 改成 8ms 当优化
  • 不用 Preview / Debug 手感给 Release 关 issue
  • 不为了跑分关掉高亮
  • 不给 Terminal / Run / Maven / Database 各开一个「重构绘制引擎」
  • 不把 [重构] 按职责拆分 AppModel 并收敛 Git UI 边界 #322 的 Phase 1–6 抄进本 issue

门禁(标题里「禁止继续走老路径」的可检查形式)

  • 输入路径禁止 textView.string 全量拷贝
  • 新分栏不得把逐帧尺寸写进 Feature @Published;必须用 LitheSplitPaneView(Diff 迁入同一组件)
  • Search / Replace overlay 不得在变为可见时 init 完整视图;空查询不 activateSearchModule()
  • 诊断回灌不得触发 Workbench objectWillChange
  • 2 MiB / 5 万行关掉语义高亮、LSP、blame
  • 新底栏 renderer 必须提供 contentIdentity,禁止光 AnyView

验收(60 Hz、Release、500 KiB)

第一阶段(T/S/D + #322,不换引擎)成功定义:输入不再随文档线性恶化;全部可拖分栏不再阶梯跟随;Shift-Shift 热路径进入两帧内。体感目标是从「1/3」到「1/2」,不是追平 VS Code。

  • perf-0 基线表贴在本 issue
  • 输入 p95 主线程任务 < 33ms;由全文同步造成的 >50ms 任务为 0
  • 侧栏、底栏、Git Log×3、Changes、Run、Tests、Worktrees、Diff 连续拖动 ≥ 55 FPS,指针延迟 ≤ 2 帧
  • 第一次 Shift-Shift 到可输入 < 80ms;热路径 < 32ms
  • 空闲 10 秒 AppModel.objectWillChange < 5
  • 工具窗 View 不再通过 @EnvironmentObject AppModel 吃全树刷新([重构] 按职责拆分 AppModel 并收敛 Git UI 边界 #322
  • ./scripts/test-macos.sh./scripts/verify-service-boundaries.sh./scripts/verify-git-graph.sh 通过

第二阶段(仅当 T2 后数据证明 TextKit 到顶)才开视口/GPU 引擎 issue。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

wait-Freezing在冻结开发周期中需要单点专项开发

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions