Skip to content

perf(macos): 消除拖动分隔条导致的全局渲染卡顿 - #390

Merged
1lck merged 11 commits into
previewfrom
codex/restore-switcher-ui
Sep 2, 2026
Merged

perf(macos): 消除拖动分隔条导致的全局渲染卡顿#390
1lck merged 11 commits into
previewfrom
codex/restore-switcher-ui

Conversation

@xiaoyumuxi

@xiaoyumuxi xiaoyumuxi commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

概述

本 PR 包含两部分独立工作:

  1. 恢复 switcher popover 集成dd69647f)——重新接入工作台顶栏的项目/分支切换器 overlay 和推送对话框,用 .overlayPreferenceValue 锚点方案替换原来的 .popover(isPresented:)
  2. 消除拖动分隔条导致的渲染卡顿6b60e1b2a8e879e38764a429)——本 PR 的主体。

根本原因:拖动分隔条时,draggedSize 写入的是特性视图GitLogViewRunViewChangesSidebarViewLanguageTestsView)自己的 @State,导致整个工具窗口每帧失效——60fps 下 ScrollView、LazyVStack、上百行 commit 行全部重新求值。


方案

1. 拖动状态下沉到容器

LitheSplitPaneView 是一个泛型两栏分割容器,draggedSize 由它自己持有。两侧内容作为宿主上一次 body 构造好的值传入,容器重新求值时传入的是相同值,SwiftUI 直接跳过它们的 body。失效范围从整个工具窗口缩小到这个容器本身。接入 4 处调用点。

2. 去掉 16ms 人为延迟

原来三处拖动处理用 buffer + Task.sleep(16ms) 节流,每次更新都主动推迟一帧,手感发飘。LitheDragUpdateScheduler 改用 DispatchQueue.main.async 合并——同一 runloop turn 内多次写入塌缩为一次,但不推迟到下一帧。Delivery 枚举(.mainRunLoopTurn / .manual)让它可以在测试中同步驱动。

3. 窗格圆角改用静态路径

原来的圆角是覆盖整个窗格的 even-odd Shape,窗格尺寸每帧变化就重新三角化完整路径。改成四个固定尺寸的角落缺口路径,尺寸不随窗格变化,可被缓存复用。

4. 恢复工具窗口的结构化标识

工具窗口内容经 AnyView 擦除后 SwiftUI 无法比较,每次都当新视图重建。加 ModuleToolContent: View, Equatable 包一层,配合 registry 新增的 contentIdentity,恢复可比较性。

Git 引用树问题更严重:原来是递归 -> AnyView 函数,擦除每一层类型、阻止 LazyVStack 惰性化、任何变化都重跑整棵树。改成扁平行模型 GitReferenceRow + GitReferenceRowsBuilder,用 LazyVStack 惰性渲染,每行独立 Equatable

5. 分组闭包 + 手写 ==

闭包每次 body 都是新分配的,Equatable 永远不成立。GitReferenceRowActions 把所有闭包收进一个 struct,GitReferenceRowView.== 显式忽略它,只比较渲染数据。沿用了代码库已有的 GitGraphRowActions 模式。

6. 记忆化重复派生

全部用「非 observable 引用盒 + @State 持有」(参考已有的 EditorViewportStore 模式),写入不触发失效:

缓存 替换的开销
GitChangeSectionsCache 四次 filter 遍历 change 列表 → 单趟派生
GitCurrentReferenceCache 每次 body 做 first(where: \.isCurrent) 线性扫描
RunConfigurationTokenCache 每次 body 重新 split @AppStorage 字符串
EditorTabFrameStore @State 字典:布局阶段的 preference 写入每次都失效整个编辑器区域

另外 gitCommitsVersion 单调计数器把图布局的 .task(id:) 从 O(n) 数组比较换成 O(1) 整数比较。


Review 说明

后两个提交值得单独理解,而不是 squash 掉:

  • a8e879e3:补全 transcript 回放中丢失的类型定义(GitReferenceRowActionsGitReferenceRowViewGitLogThreePaneLayout),修复 LitheSignpostOSSignpostIntervalStateString 键字典,因为 StaticString 不符合 Hashable)。
  • 8764a429:修复真实 bug——分支/远程/标签列表渲染为空。三个行数组只被读取、从未写入;缓存文件存在于磁盘但没有调用方。本提交补上 .task(id: referenceRowsTaskIdentity) 填充行数组,同时把上述四个缓存真正接进各自的宿主视图。

测试

  • ./scripts/build-macos.sh ——通过。
  • ./scripts/test-macos.sh ——97 个套件共 905 个测试。29 个失败,全部集中在 "Run entry points" 套件,是预先存在的问题:stash 本分支的改动、在干净 main 上跑同一套件同样失败(32 个 issue)。该套件不引用 RunView 或本 PR 涉及的任何类型。其余 96 个套件全部通过。
  • 为提取出的纯逻辑新增单元测试:LitheDragUpdateSchedulerTests(用 .manual delivery 同步驱动)、LitheSplitPaneGeometryTestsWorkbenchPaneCornerGeometryTestsGitReferenceRowsBuilderTestsGitChangeSectionsCacheTests

实际帧率未实测。 以上描述的是失效范围的结构性收缩,机制上成立,但实际 fps 改善需要在 Instruments 里确认。LitheSignpost 提供了 DEBUG 下的 body 求值计数器,对比拖动前后各视图的 body 次数是最直接的验证方式。

…anes

Stages 0-5 of the rendering performance plan:

- Replace 16ms Task.sleep drag coalescing with zero-latency
  DispatchQueue.main.async via LitheDragUpdateScheduler (3 sites)
- Replace per-frame full-pane even-odd Shape tessellation with four
  fixed-size static corner notch paths
- Restore module tool window structural identity via ModuleToolContent
  Equatable wrapper, defeating AnyView erasure
- Sink drag state into LitheSplitPaneView containers so divider drags
  only invalidate the small container, not the feature view (4 sites)
- Flatten recursive AnyView reference tree into GitReferenceRows with
  LazyVStack and Equatable row views
- Add GitChangeSectionsCache for ChangesSidebarView
- Add EditorTabFrameStore reference box to stop layout-pass preference
  writes from invalidating the entire EditorAreaView body
- Add RunConfigurationTokenCache and GitCurrentReferenceCache to
  memoize repeated derivations
- Add gitCommitsVersion monotonic token replacing O(n) array comparison
- Add LitheSignpost DEBUG body evaluation counter
- Add tests for LitheDragUpdateScheduler, LitheSplitPaneGeometry,
  WorkbenchPaneCornerGeometry, GitReferenceRowsBuilder,
  GitChangeSectionsCache
Add missing type definitions (GitReferenceRowActions, GitReferenceRowView,
GitLogThreePaneLayout) lost during transcript replay, remove duplicate
GitReferenceTreeNode, fix WorkbenchWorkspaceSplitView action routing, and
fix LitheSignpost to use OSSignpostIntervalState and String-keyed dictionary.
- GitLogView: fill local/remote/tag reference rows via .task(id: referenceRowsTaskIdentity)
  using GitReferenceRowsBuilder; add GitReferenceRowsIdentity combined key;
  replace linear currentReference scan with GitCurrentReferenceCache;
  switch .task(id:) for graph layout to use gitCommitsVersion integer token;
  remove unused commitFileTreeItems @State
- ChangesSidebarView: replace four filter passes with GitChangeSectionsCache
  (single-pass derivation of displayed/tracked/added/staged)
- RunView: replace per-body String.split with RunConfigurationTokenCache
  (two separate instances for collapsed executions and pin tokens)
- EditorAreaView: replace @State editorTabFrames dictionary with EditorTabFrameStore
  reference box so preference writes no longer invalidate the editor body
@xiaoyumuxi
xiaoyumuxi requested a review from 1lck as a code owner September 1, 2026 13:02
@xiaoyumuxi xiaoyumuxi changed the title perf(macos): eliminate drag-induced rendering jank across split panes perf(macos): 消除拖动分隔条导致的全局渲染卡顿 Sep 1, 2026
@xiaoyumuxi
xiaoyumuxi force-pushed the codex/restore-switcher-ui branch from 8764a42 to 2e1e14f Compare September 1, 2026 13:08
…g state

GitLogView: split gitLogQuery into hasActiveGitLogFilter (no Date() call)
and gitLogQuery(now:). The debounced .task captures Date() once at fire
time so afterDate/beforeDate are stable for the lifetime of each query.

RunView: remove liveConfigurationListWidth, configurationListDragStart,
resolvedListWidth, and constrained() — all became dead code when the
configuration list pane was migrated to LitheSplitPaneView.
@xiaoyumuxi
xiaoyumuxi marked this pull request as draft September 1, 2026 13:19
Swift 6.2 (the CI toolchain) rejects a call to a @MainActor-isolated
init inside the nonisolated @State default-value context. The init
itself is safe to call from any context — it only assigns self.delivery;
no @mainactor state is touched until the first method call.

Annotating the init nonisolated preserves full @mainactor isolation on
all mutating methods while making the @State-held initialisation
accepted by both Swift 6.2 and 6.3.
@xiaoyumuxi
xiaoyumuxi marked this pull request as ready for review September 1, 2026 13:52
1lck and others added 2 commits September 1, 2026 22:15
The testing helper detaches into its own process group, so the previous
group-scoped walk only sampled the shell wrapper waiting on its child.
Walk the ppid tree instead and capture a listing plus a thread-stack
sample of every descendant before terminating the run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread macos/Sources/Lithe/Views/Workbench/WorkbenchModuleUIRegistry.swift
Comment thread macos/Sources/Lithe/Views/Git/GitLogView.swift
@1lck

1lck commented Sep 2, 2026

Copy link
Copy Markdown
Owner

@xiaoyumuxi 这次 Review 发现 3 个需要处理的问题,均由本 PR 引入,当前存在阻塞合并的问题:

  1. ModuleToolContent 已开始依赖 contentIdentity,但内置 Maven、Run、Tests、Debug renderer 没有提供 identity,跨工作区切换时可能复用旧 feature view。
  2. Git Log 三栏布局移除了 commit pane 的最小宽度,连续拖动左右面板后,中间提交列表可能被压缩到不可用。
  3. Swift stall watchdog 在终止子进程前同步执行最多 6 次 sample,真实 stall 时终止动作可能额外延迟约 90 秒,超出配置的 stall timeout。

我已经把复现步骤、影响和建议分别写在对应的行级评论中。建议修复后补上工作区切换/feature identity、窄窗口拖动,以及带真实 PID 的 watchdog 清理测试。

值得保留的是把拖动状态下沉到 LitheSplitPaneView、将 Git 引用树扁平化为可比较的惰性行模型,以及用手动 delivery 测试拖动合并逻辑;这些方向与现有架构一致,也确实降低了渲染路径上的重复工作。

目前 PR 的 CI 状态是通过的,但描述中也提到尚未用 Instruments 实测实际帧率;在上述功能和测试基础设施问题修复后,建议再补一次 macOS 实机验证。

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator Author

@xiaoyumuxi 这次 Review 发现 3 个需要处理的问题,均由本 PR 引入,当前存在阻塞合并的问题:

  1. ModuleToolContent 已开始依赖 contentIdentity,但内置 Maven、Run、Tests、Debug renderer 没有提供 identity,跨工作区切换时可能复用旧 feature view。
  2. Git Log 三栏布局移除了 commit pane 的最小宽度,连续拖动左右面板后,中间提交列表可能被压缩到不可用。
  3. Swift stall watchdog 在终止子进程前同步执行最多 6 次 sample,真实 stall 时终止动作可能额外延迟约 90 秒,超出配置的 stall timeout。

我已经把复现步骤、影响和建议分别写在对应的行级评论中。建议修复后补上工作区切换/feature identity、窄窗口拖动,以及带真实 PID 的 watchdog 清理测试。

值得保留的是把拖动状态下沉到 LitheSplitPaneView、将 Git 引用树扁平化为可比较的惰性行模型,以及用手动 delivery 测试拖动合并逻辑;这些方向与现有架构一致,也确实降低了渲染路径上的重复工作。

目前 PR 的 CI 状态是通过的,但描述中也提到尚未用 Instruments 实测实际帧率;在上述功能和测试基础设施问题修复后,建议再补一次 macOS 实机验证。

  1. 这个应该需要其他的来进行适配当前的改动,我会提一个issue的,我这个PR已经很大了不方便再去改动了的
  2. 这个压得太小是他们使用的问题吧,我记得IDEA本身也是可以压得非常小的,这个不当使用应该是使用者自己调节到喜欢的位置而不是我们限定死非要显示完整吧
  3. 看门狗问题跟另外一个PR一样,属于已提交的改动,为了通过CI不得已加入的提交

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator Author

@xiaoyumuxi 这次 Review 发现 3 个需要处理的问题,均由本 PR 引入,当前存在阻塞合并的问题:

  1. ModuleToolContent 已开始依赖 contentIdentity,但内置 Maven、Run、Tests、Debug renderer 没有提供 identity,跨工作区切换时可能复用旧 feature view。
  2. Git Log 三栏布局移除了 commit pane 的最小宽度,连续拖动左右面板后,中间提交列表可能被压缩到不可用。
  3. Swift stall watchdog 在终止子进程前同步执行最多 6 次 sample,真实 stall 时终止动作可能额外延迟约 90 秒,超出配置的 stall timeout。

我已经把复现步骤、影响和建议分别写在对应的行级评论中。建议修复后补上工作区切换/feature identity、窄窗口拖动,以及带真实 PID 的 watchdog 清理测试。
值得保留的是把拖动状态下沉到 LitheSplitPaneView、将 Git 引用树扁平化为可比较的惰性行模型,以及用手动 delivery 测试拖动合并逻辑;这些方向与现有架构一致,也确实降低了渲染路径上的重复工作。
目前 PR 的 CI 状态是通过的,但描述中也提到尚未用 Instruments 实测实际帧率;在上述功能和测试基础设施问题修复后,建议再补一次 macOS 实机验证。

  1. 这个应该需要其他的来进行适配当前的改动,我会提一个issue的,我这个PR已经很大了不方便再去改动了的
  2. 这个压得太小是他们使用的问题吧,我记得IDEA本身也是可以压得非常小的,这个不当使用应该是使用者自己调节到喜欢的位置而不是我们限定死非要显示完整吧
  3. 看门狗问题跟另外一个PR一样,属于已提交的改动,为了通过CI不得已加入的提交

帧率问题测试过的,大概是稳到58fps左右的,那个AI没有测试而已

@1lck

1lck commented Sep 2, 2026

Copy link
Copy Markdown
Owner

两个pr的目标分支似乎错了

@xiaoyumuxi
xiaoyumuxi changed the base branch from main to preview September 2, 2026 02:22
@xiaoyumuxi

Copy link
Copy Markdown
Collaborator Author

两个pr的目标分支似乎错了

嗯嗯,改了,刚发现

1lck
1lck previously approved these changes Sep 2, 2026
@1lck

1lck commented Sep 2, 2026

Copy link
Copy Markdown
Owner

这个ci过了就可以合了 改好了

@1lck
1lck merged commit e4544e1 into preview Sep 2, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants