Skip to content

[Bug] macOS 分栏拖动受固定帧率限制并触发大范围重复布局 #325

Description

@xiaoyumuxi

平台

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 个分栏拖动条复用了这套实现。

复现步骤

  1. 打开一个包含较多内容的工作区,使 Git Log 或 Workbench 的面板具有一定复杂度。
  2. 打开 Git Log。
  3. 连续拖动分支树与提交图之间的分隔条。
  4. 分别尝试拖动 Workbench 侧边栏、底部工具窗口、Run 配置列表、Tests 列表或 Changes 提交区域的分隔条。
  5. 可以观察到分隔条以明显的阶梯方式跟随指针;相邻面板内容越复杂,拖动越容易掉帧。

预期行为

分隔条应按照当前显示器的刷新率跟随指针,包括 120 Hz 显示器。拖动过程中不应积压旧位置、触发无关工作,也不应在指针停止后继续追赶。

实际行为

SplitHandleView 当前通过固定 16 ms 的 Task.sleep 合并指针事件。该调度没有与 VSYNC 同步,即使在 120 Hz 显示器上,实时调整尺寸的更新频率也被限制在约 60 Hz。

每次交付的新尺寸还会修改大型父视图持有的宽度或高度状态,导致较大范围的 SwiftUI 子树失效并重新布局,其中包括滚动视图、文本行、编辑器和工具面板,以及 Git 图的 Canvas。

两者叠加后会出现阶梯式跟随、掉帧和偶发的延迟追赶。

根因

  1. 固定时间间隔的节流没有感知显示器刷新率。
  2. 更新通过 MainActor Task 延迟交付,没有形成“每帧只取最新值”的明确约束。
  3. 实时拖动尺寸通常由大型功能页面直接持有。
  4. 每次尺寸变化都会使远多于相邻两个面板的 UI 失效。
  5. 同一公共实现影响当前全部 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,且不需要再次修改业务页面。

验收标准

  • 在 60 Hz 显示器上连续拖动时达到至少 55 FPS。
  • 在 120 Hz 显示器上连续拖动时达到至少 105 FPS。
  • 分隔条相对指针的视觉延迟不超过两个显示帧。
  • 主线程恢复后不会回放旧位置。
  • 松开指针时准确提交经过约束的最终尺寸。
  • 调整尺寸相关的业务操作和持久化写入不会逐帧执行。
  • 当前全部 8 个分栏拖动条使用修复后的公共路径。
  • 悬停效果、鼠标指针、命中区域、无障碍语义和布局恢复行为保持不变。
  • 使用不依赖真实时间休眠的确定性测试覆盖:最新值合并、取消、最终值交付和连续多次拖动。
  • ./scripts/test-macos.sh 通过。
  • ./scripts/verify-service-boundaries.sh 通过。
  • ./scripts/verify-git-graph.sh 通过。

已考虑的替代方案

  • 将 16 ms 缩短为 8 ms:仍然基于定时器,没有与显示刷新同步,而且会增加主线程布局压力。
  • 完全移除合并:高频指针事件可能产生超过显示器呈现能力的布局请求。
  • 分别优化每个功能页面:公共根因仍然存在,并会造成不一致的拖动体验。
  • 只在鼠标松开后更新面板尺寸:能够避免实时布局成本,但交互反馈较差。

截图或录屏

后续可以附带显示 FPS 指示器的录屏,用于建立修复前基线并验证最终帧预算。

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingmacossomething related to macos strongly

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions