平台
macOS
Lithe 版本
- Commit:
9b375ed5
- 分支:
preview
操作系统与架构
macOS 27.0,Apple Silicon arm64
相关实现
macos/Sources/Lithe/Views/Workbench/SplitHandleView.swift
macos/Sources/Lithe/Views/Components/FrameCoalescedDragUpdateBuffer.swift
当前 Git Log、Changes、Workbench、Run 和 Tests 中共有 8 个分栏拖动条复用了这套实现。
复现步骤
- 打开一个包含较多内容的工作区,使 Git Log 或 Workbench 的面板具有一定复杂度。
- 打开 Git Log。
- 连续拖动分支树与提交图之间的分隔条。
- 分别尝试拖动 Workbench 侧边栏、底部工具窗口、Run 配置列表、Tests 列表或 Changes 提交区域的分隔条。
- 可以观察到分隔条以明显的阶梯方式跟随指针;相邻面板内容越复杂,拖动越容易掉帧。
预期行为
分隔条应按照当前显示器的刷新率跟随指针,包括 120 Hz 显示器。拖动过程中不应积压旧位置、触发无关工作,也不应在指针停止后继续追赶。
实际行为
SplitHandleView 当前通过固定 16 ms 的 Task.sleep 合并指针事件。该调度没有与 VSYNC 同步,即使在 120 Hz 显示器上,实时调整尺寸的更新频率也被限制在约 60 Hz。
每次交付的新尺寸还会修改大型父视图持有的宽度或高度状态,导致较大范围的 SwiftUI 子树失效并重新布局,其中包括滚动视图、文本行、编辑器和工具面板,以及 Git 图的 Canvas。
两者叠加后会出现阶梯式跟随、掉帧和偶发的延迟追赶。
根因
- 固定时间间隔的节流没有感知显示器刷新率。
- 更新通过 MainActor Task 延迟交付,没有形成“每帧只取最新值”的明确约束。
- 实时拖动尺寸通常由大型功能页面直接持有。
- 每次尺寸变化都会使远多于相邻两个面板的 UI 失效。
- 同一公共实现影响当前全部 8 个分栏拖动条。
Git 图拓扑计算不是主要原因:它由提交数据变化驱动,而不是由面板宽度驱动。主要成本来自拖动期间重复进行的 SwiftUI 求值、布局、文本重排和 Canvas 合成。
建议方案
第一阶段:与显示刷新同步地交付拖动更新
用公共的 display-link 调度器替换固定 16 ms 的休眠:
- 只保存最新的待处理 translation。
- 每个显示刷新周期最多交付一次。
- 自动适配 60 Hz 和 120 Hz 显示器。
- 不排队保存历史指针位置。
- 拖动结束时同步交付最终 translation。
- 保留当前全局手势坐标和尺寸约束。
不应仅把 16 ms 改成另一个固定时间。
第二阶段:隔离实时调整尺寸的状态
引入公共 LitheSplitLayout 组件:
- 临时拖动尺寸由分栏容器内部持有。
- 只有需要持久化最终尺寸时才通知功能页面。
- 实时拖动期间关闭隐式动画。
- 将失效范围限制在分栏容器和相邻面板。
- 保持现有分隔条外观、鼠标指针、命中区域、无障碍语义和最小/最大尺寸约束。
迁移现有调用点:
- Git Log:3 个
- Changes:1 个
- Workbench:2 个
- Run:1 个
- Tests:1 个
公共 API 应允许在 SwiftUI 方案仍无法稳定满足帧预算时,将内部实现替换为 NSSplitView,且不需要再次修改业务页面。
验收标准
已考虑的替代方案
- 将 16 ms 缩短为 8 ms:仍然基于定时器,没有与显示刷新同步,而且会增加主线程布局压力。
- 完全移除合并:高频指针事件可能产生超过显示器呈现能力的布局请求。
- 分别优化每个功能页面:公共根因仍然存在,并会造成不一致的拖动体验。
- 只在鼠标松开后更新面板尺寸:能够避免实时布局成本,但交互反馈较差。
截图或录屏
后续可以附带显示 FPS 指示器的录屏,用于建立修复前基线并验证最终帧预算。
平台
macOS
Lithe 版本
9b375ed5preview操作系统与架构
macOS 27.0,Apple Silicon arm64
相关实现
macos/Sources/Lithe/Views/Workbench/SplitHandleView.swiftmacos/Sources/Lithe/Views/Components/FrameCoalescedDragUpdateBuffer.swift当前 Git Log、Changes、Workbench、Run 和 Tests 中共有 8 个分栏拖动条复用了这套实现。
复现步骤
预期行为
分隔条应按照当前显示器的刷新率跟随指针,包括 120 Hz 显示器。拖动过程中不应积压旧位置、触发无关工作,也不应在指针停止后继续追赶。
实际行为
SplitHandleView当前通过固定 16 ms 的Task.sleep合并指针事件。该调度没有与 VSYNC 同步,即使在 120 Hz 显示器上,实时调整尺寸的更新频率也被限制在约 60 Hz。每次交付的新尺寸还会修改大型父视图持有的宽度或高度状态,导致较大范围的 SwiftUI 子树失效并重新布局,其中包括滚动视图、文本行、编辑器和工具面板,以及 Git 图的 Canvas。
两者叠加后会出现阶梯式跟随、掉帧和偶发的延迟追赶。
根因
Git 图拓扑计算不是主要原因:它由提交数据变化驱动,而不是由面板宽度驱动。主要成本来自拖动期间重复进行的 SwiftUI 求值、布局、文本重排和 Canvas 合成。
建议方案
第一阶段:与显示刷新同步地交付拖动更新
用公共的 display-link 调度器替换固定 16 ms 的休眠:
不应仅把 16 ms 改成另一个固定时间。
第二阶段:隔离实时调整尺寸的状态
引入公共
LitheSplitLayout组件:迁移现有调用点:
公共 API 应允许在 SwiftUI 方案仍无法稳定满足帧预算时,将内部实现替换为
NSSplitView,且不需要再次修改业务页面。验收标准
./scripts/test-macos.sh通过。./scripts/verify-service-boundaries.sh通过。./scripts/verify-git-graph.sh通过。已考虑的替代方案
截图或录屏
后续可以附带显示 FPS 指示器的录屏,用于建立修复前基线并验证最终帧预算。