perf(macos): 消除拖动分隔条导致的全局渲染卡顿 - #390
Conversation
…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
8764a42 to
2e1e14f
Compare
…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.
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.
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>
|
@xiaoyumuxi 这次 Review 发现 3 个需要处理的问题,均由本 PR 引入,当前存在阻塞合并的问题:
我已经把复现步骤、影响和建议分别写在对应的行级评论中。建议修复后补上工作区切换/feature identity、窄窗口拖动,以及带真实 PID 的 watchdog 清理测试。 值得保留的是把拖动状态下沉到 目前 PR 的 CI 状态是通过的,但描述中也提到尚未用 Instruments 实测实际帧率;在上述功能和测试基础设施问题修复后,建议再补一次 macOS 实机验证。 |
|
帧率问题测试过的,大概是稳到58fps左右的,那个AI没有测试而已 |
|
两个pr的目标分支似乎错了 |
嗯嗯,改了,刚发现 |
|
这个ci过了就可以合了 改好了 |
概述
本 PR 包含两部分独立工作:
dd69647f)——重新接入工作台顶栏的项目/分支切换器 overlay 和推送对话框,用.overlayPreferenceValue锚点方案替换原来的.popover(isPresented:)。6b60e1b2、a8e879e3、8764a429)——本 PR 的主体。根本原因:拖动分隔条时,
draggedSize写入的是特性视图(GitLogView、RunView、ChangesSidebarView、LanguageTestsView)自己的@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模式),写入不触发失效:GitChangeSectionsCachefilter遍历 change 列表 → 单趟派生GitCurrentReferenceCachefirst(where: \.isCurrent)线性扫描RunConfigurationTokenCachesplit@AppStorage字符串EditorTabFrameStore@State字典:布局阶段的 preference 写入每次都失效整个编辑器区域另外
gitCommitsVersion单调计数器把图布局的.task(id:)从 O(n) 数组比较换成 O(1) 整数比较。Review 说明
后两个提交值得单独理解,而不是 squash 掉:
a8e879e3:补全 transcript 回放中丢失的类型定义(GitReferenceRowActions、GitReferenceRowView、GitLogThreePaneLayout),修复LitheSignpost(OSSignpostIntervalState、String键字典,因为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(用.manualdelivery 同步驱动)、LitheSplitPaneGeometryTests、WorkbenchPaneCornerGeometryTests、GitReferenceRowsBuilderTests、GitChangeSectionsCacheTests。实际帧率未实测。 以上描述的是失效范围的结构性收缩,机制上成立,但实际 fps 改善需要在 Instruments 里确认。
LitheSignpost提供了 DEBUG 下的 body 求值计数器,对比拖动前后各视图的 body 次数是最直接的验证方式。