TreeView 性能优化
路线图位置:
../desktop-controls-optimization-roadmap.mdTier 1 #1 状态:本轮已完成 Gallery 实测。
0. 结论
本轮优化 NodeSwitcherButton 的模板结构:每个树节点的 switcher 原来固定创建 5 个 IconPresenter(expand/collapse/rotation/loading/leaf),实际同一时刻只显示一个。现在模板只保留 1 个 IconPresenter,由 NodeSwitcherButton.CurrentIcon 和 IsCurrentIconVisible 选择当前图标。
这不是 axaml 节点搬到 C# 动态创建;功能视觉仍在 ControlTheme 中,C# 只维护运行态属性。
本轮追加优化 TreeViewItem 的连接线渲染路径:显示树线时不再每次 Render() 都创建 Pen,改为按 item 缓存 line pen,并在 BorderBrush 或 BorderThickness.Top 变化时重建。随后继续收敛 TreeView 拖拽指示线:拖拽过程中不再每帧创建 indicator Pen,按 DragIndicatorBrush / DragIndicatorLineWidth 复用缓存。
后续追加优化 checkbox 父节点三态聚合:子节点 checked 状态变化后,父链原先分别用 All(...) 和 Any(...) 两次 LINQ 扫描子项,并最多调用两轮 TreeContainerFromItem。现在改为一次手写扫描,同时得出 isAllChecked / isAnyChecked,在已确定混合态时提前退出。该项是交互路径 structural-only 收益,不声明页面导航 timing 提升。
| 指标 | baseline | optimized | 改善 |
|---|---|---|---|
| Cold first navigation mean | 1049.62 ms | 776.51 ms | 26.0% |
| Cold alloc mean | 54136.18 KB | 37558.07 KB | 30.6% |
| Repeated navigation mean | 332.44 ms | 213.41 ms | 35.8% |
| Repeated median | 323.32 ms | 206.68 ms | 36.1% |
| Repeated P95 | 403.63 ms | 243.17 ms | 39.8% |
| Repeated alloc mean | 49964.64 KB | 33732.33 KB | 32.5% |
| Runtime visuals | 2108 | 1646 | 21.9% |
IconPresenter |
332 | 112 | 66.3% |
Icon / PathIcon |
302 | 60 | 80.1% |
| 追加指标 | baseline | optimized | 改善 |
|---|---|---|---|
TreeViewItem leaf line Pen allocations / repeated render |
2 pens | 0 pens after first render | 100.00% |
TreeViewItem branch line Pen allocations / repeated render |
1 pen | 0 pens after first render | 100.00% |
TreeView drag indicator Pen allocations / dragging render frame |
1 pen | 0 pens after first render | 100.00% |
| TreeView checkbox parent aggregation child scans / parent update | 2 passes | 1 pass | 50.00% |
| TreeView checkbox parent aggregation container lookups / parent update with N children | up to 2N |
up to N |
50.00% |
| TreeView checkbox parent aggregation LINQ pipelines / parent update | 2 | 0 | 100.00% |
1. 资格门槛
Gallery source:controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/TreeViewShowCase.axaml
| 项 | 数值 |
|---|---|
<atom:TreeView> |
15 |
declared <atom:TreeViewItem> |
87 |
ShowCaseItem |
10 |
Runtime MotionActor / tree rows |
55 |
真实会话操作频率:
- 页面导航:至少 1 次。
- 展开/折叠:基础示例、line 示例、自定义 switcher、搜索结果、右键菜单示例都会触发。
- 过滤搜索:2 个
SearchEdit示例。 - 拖拽:
IsDraggable=True示例。 - 右键菜单:
ItemContextMenuRequest示例。
结论:实例数 > 5,操作 > 1/session,并已有 Gallery 数字,满足 SKILL Tier 1 §13。
2. 根因
NodeSwitcherButtonTheme.axaml 原模板为每个 switcher 固定创建 5 个图标 presenter:
ExpandIconPresenterCollapseIconPresenterRotationIconPresenterLoadingIconPresenterLeafIconPresenter
Gallery baseline 显示 IconPresenter=332、Icon/PathIcon=302,远高于页面直接声明的图标数量。TreeView 当前真实稳定形态有 55 个节点 header,5 个 switcher presenter 会放大成约 275 个额外 icon presenter 结构。
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Controls/Primitives/TemplatedControl.cs:333:template.Build(this)创建模板树。.referenceprojects/Avalonia/src/Avalonia.Controls/Primitives/TemplatedControl.cs:338:模板根加入 visual children。.referenceprojects/Avalonia/src/Avalonia.Base/Layout/Layoutable.cs:546/:624:IsVisible=false的元素会跳过 measure,但前面的模板创建成本已经发生。src/AtomUI.Controls/Icon/IconPresenter.cs:50:IconProperty影响 measure。src/AtomUI.Controls/Icon/IconPresenter.cs:95/:96:IconPresenter会把当前PathIcon加入 visual/logical children。
主导子系统:Templates / visual tree structure,其次是 IconPresenter 绑定与子图标结构。
可证伪假设:如果把 NodeSwitcherButton 的 5 个固定 IconPresenter 收敛到 1 个,同时保持所有图标状态和 selector 行为,TreeViewShowCase 的 IconPresenter 与 visual count 应显著下降,并带来 repeated navigation >= 5% 的时间或分配改善;否则回退。
3. 改动
3.1 NodeSwitcherButton 增加运行态只读属性
新增:
CurrentIcon:当前应该交给模板 presenter 显示的PathIcon?。IsCurrentIconVisible:Leaf 模式且IsLeafIconVisible=false时隐藏 presenter。
状态映射:
IconMode |
当前图标 |
|---|---|
Default + unchecked |
ExpandIcon |
Default + checked |
CollapseIcon |
Rotation |
RotationIcon |
Loading |
LoadingIcon |
Leaf + visible |
LeafIcon |
Leaf + hidden |
null |
触发更新的属性:IconMode、IsChecked、IsLeafIconVisible、ExpandIcon、CollapseIcon、RotationIcon、LoadingIcon、LeafIcon。
3.2 模板保留单个 presenter
NodeSwitcherButtonTheme.axaml 将 5 个 presenter 替换成:
<atom:IconPresenter Name="CurrentIconPresenter"
RenderTransform="{TemplateBinding RotationIconRenderTransform}"
Icon="{TemplateBinding CurrentIcon}"
IsVisible="{TemplateBinding IsCurrentIconVisible}" />
保留的样式行为:
- 普通尺寸:
IconSize。 - Rotation 模式:
IconSizeXS。 - Loading 模式:
ColorPrimaryicon brush。 - checked + Rotation:
RotationIconRenderTransform=rotate(90deg)。 - motion:
Background与RotationIconRenderTransformtransition 不变。
3.3 TreeViewItem 连接线 Pen 缓存
TreeViewItem.RenderTreeNodeLine() 原先在每条连接线绘制处直接 new Pen(BorderBrush, penWidth):
- 非最后一个展开分支节点:1 条竖线,1 个
Pen。 - 叶子节点:竖线 + 横线,2 个
Pen。
现在每个 TreeViewItem 缓存一个 _treeNodeLinePen,同一个 item 在后续 render 中复用;当 BorderBrush 引用或 BorderThickness.Top 变化时重建。BorderBrushProperty / BorderThicknessProperty 已在 AffectsRender<TreeViewItem> 中注册,因此 token / theme 变化仍会触发重绘。
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Base/Media/Pen.cs:17-40:Pen是 mutableAvaloniaObject,Brush/Thickness是 styled properties;render 热路径不应按帧创建。.referenceprojects/Avalonia/src/Avalonia.Base/Visual.cs:446-500:AffectsRender会在相关属性变化时 invalidate render。
3.4 TreeView 拖拽指示线 Pen 缓存
TreeView.Render() 在拖拽期间会绘制 drop indicator。原实现每个 dragging render frame 都执行:
new Pen(DragIndicatorBrush, DragIndicatorLineWidth)
现在 TreeView 实例缓存 _dragIndicatorPen,当 DragIndicatorBrush 引用或 DragIndicatorLineWidth 变化时重建。DragIndicatorRenderInfoProperty、DragIndicatorBrushProperty、DragIndicatorLineWidthProperty 已在 ConfigureDragAndDrop() 中注册 AffectsRender<TreeView>,因此拖拽位置和主题 token 变化仍按原路径触发重绘。
3.5 Checkbox 父节点状态聚合一次扫描
SetupParentNodeCheckedStatus() 原先在每个父节点上分别执行:
parentTreeItem.Items.All(...)
parentTreeItem.Items.Any(...)
两次 LINQ 扫描都需要把 child item 映射到 realized TreeViewItem。现在 GetChildCheckStatus() 在一次循环内同时计算:
isAllChecked:所有可勾选子项均为 checked,非可勾选子项按原语义不阻止 all checked。isAnyChecked:存在 checked 或 indeterminate 的可勾选子项。
当 !isAllChecked && isAnyChecked 已成立时提前退出,因为父节点最终一定是 indeterminate。验证覆盖单子项 checked -> parent indeterminate、全 checked -> parent checked、再取消一个子项 -> parent indeterminate,并检查 CheckedItems 同步。
4. 验证
4.1 构建
dotnet build src/AtomUI.Desktop.Controls/AtomUI.Desktop.Controls.csproj -c Debug --framework net10.0
结果:0 Warning(s), 0 Error(s)。
dotnet build tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj -c Debug --framework net10.0
结果:构建通过;保留既有 warning DataGridColumn._clipboardContentBinding is never used,非本轮新增。
后续追加复测:
dotnet build -c Release -f net10.0 --no-restore tools/performances/AtomUI.Performance/AtomUI.Performance.csproj
dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-treeview-states
结果:构建 0 Warning(s), 0 Error(s);TreeView state verification passed.
4.2 Gallery 基线与优化后对比
命令:
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
-c Debug --framework net10.0 --no-build -- \
--showcase treeview --label baseline \
--cold-iterations 10 --iterations 30 --warmup 5 --timeout-ms 30000 \
--markdown /tmp/treeview-showcase-baseline.md
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
-c Debug --framework net10.0 --no-build -- \
--showcase treeview --label optimized \
--cold-iterations 10 --iterations 30 --warmup 5 --timeout-ms 30000 \
--markdown /tmp/treeview-showcase-optimized.md
| Set | Mean | Median | P95 | Alloc mean | Visuals | IconPresenter | PathIcon |
|---|---|---|---|---|---|---|---|
| Cold baseline | 1049.62 ms | 1056.51 ms | 1189.91 ms | 54136.18 KB | 2108 | 332 | 302 |
| Cold optimized | 776.51 ms | 770.78 ms | 830.95 ms | 37558.07 KB | 1646 | 112 | 60 |
| Repeated baseline | 332.44 ms | 323.32 ms | 403.63 ms | 49964.64 KB | 2108 | 332 | 302 |
| Repeated optimized | 213.41 ms | 206.68 ms | 243.17 ms | 33732.33 KB | 1646 | 112 | 60 |
4.3 Regression matrix
Matrix:tools/performances/AtomUI.Performance/Suites/TreeView/Regression.md
自动状态验证:
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
-c Debug --framework net10.0 --no-build -- \
--verify-treeview-states
结果:TreeView state verification passed.
本轮覆盖:
- 默认 expand/collapse 图标切换。
- rotation switcher checked transform。
- loading async node icon。
- leaf icon visible / hidden。
- search/filter page load path。
- context menu page load path。
追加 TreeView render pen 缓存验证:
dotnet build -c Release -f net10.0 --no-restore tools/performances/AtomUI.Performance/AtomUI.Performance.csproj
dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-treeview-states
说明:该追加项是结构性 render 分配优化;当前 headless harness 没有逐帧 render allocation 计数器,因此只用 build / state verifier 做正确性回归,不使用单次 timing 证明速度收益。
5. 复杂度自评
| 指标 | 数值 |
|---|---|
新增 Ensure*/Clear*/Sync* 方法 |
0 |
| 新增 try/finally 标志位 | 0 |
| 新增 disposable 字段 | 0 |
| 新增订阅 / timer / reparented element | 0 |
| axaml 节点搬到 C# 动态创建 | 否 |
| Theme Static Rule | 未触发 |
| 生产文件范围 | 2 个文件,均在 TreeView |
新增 CurrentIcon 是 direct runtime state,模板仍使用 {TemplateBinding},没有引入 RelayBind 或同生命周期 disposable plumbing。
6. 后续
TreeView 仍有第二类热点:TreeViewItemHeader.BuildFilterHighlightRuns() 在搜索高亮中存在 per-character Run 模式。该问题只在 filter 交互触发,不属于本轮页面导航主因;下一轮应先建立 filter action 的专项基线,再考虑段级 Run 合并。
7. 追加结构优化:TreeView 过滤高亮无命中 fast path
TreeViewItemHeader.BuildFilterHighlightRuns() 在 HighlightedMatch 模式下先检查首个命中;无命中时不再创建空 ranges list。同时补齐一个正确性短路:FilterHighlightWords == "" 时清空高亮 runs 并返回,避免旧逻辑在 IndexOf("") 上无法推进。HighlightedWhole 和 BoldedMatch 的现有行为保持不变:whole 仍整段着色,bold 仍按旧逻辑作用到所有字符。
| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 |
|---|---|---|---|---|---|
| 无命中过滤高亮 ranges list / header rebuild | 1 list | 0 list | (1 - 0) / 1 |
100.00% | TreeView filter 无命中路径少一次临时集合分配 |
| 命中高亮首个命中查找 / header rebuild | 2 次 | 1 次 | (2 - 1) / 2 |
50.00% | 首个匹配位置复用,不重复搜索 |
| 空过滤词死循环风险 | 1 条 | 0 条 | (1 - 0) / 1 |
100.00% | 正确性修复,不作为性能收益 |
说明:这是 filter 交互路径的结构性收益;本轮不声称 TreeViewShowCase 页面导航 timing 因此提升。
8. 追加结构优化:filter 选择闭包启动列表
TreeView.HandleSelectionChanged() 在 filter 模式下不再先把 selected containers 放进 startupItems 临时列表,而是在找到 container 时直接计算并合并闭包。
| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 |
|---|---|---|---|---|---|
| filter selection startup list / selection changed | 1 list | 0 list | (1 - 0) / 1 |
100.00% | 结构收益;filter 选择变化少一次临时集合分配 |
| selected container second pass / selection changed | 1 pass | 0 pass | (1 - 0) / 1 |
100.00% | 结构收益;闭包在发现 container 时直接合并 |
说明:这是 filter 交互路径的结构性收益;不声明页面导航 timing 提升。
9. 追加结构优化:TreeNodePath 路径列表容量
ExpandTreeViewPath()、CollapseTreeViewPath()、TraverTreeViewPath() 的路径列表按 TreeNodePath.Segments.Count 预分配,临时展开状态列表也按同一长度预分配。路径内容、展开/恢复顺序不变。
| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 |
|---|---|---|---|---|---|
| TreeView pathNodes list growth / path traversal | dynamic capacity | exact capacity | structural | 分配更紧 | 路径节点数已知时避免 List 增长 |
| TreeView path expand-status list growth / temporary traversal | dynamic capacity | exact capacity | structural | 分配更紧 | 展开状态数量与路径长度一致 |
| TreeView checked reset added list growth / checked items reset | dynamic capacity | exact capacity | structural | 分配更紧 | reset 通知 added 列表按 CheckedItems.Count 预分配 |
说明:这是默认展开/选中路径的结构性收益;不声明页面导航 timing 提升。