ColorPicker 性能优化
路线图位置:
../desktop-controls-optimization-roadmap.mdPhase E / Tier 3 状态:closed-stateWindow.Deactivatedlifecycle、打开态GradientColorPickerViewbinding、ColorSpectrumbrush 热路径复用、ColorPickerPaletteGroup选择事件隔离、HsvValue预览 / slider thumb brush 复用、透明棋盘背景 brush token 缓存均已完成。
0. 结论
T3.1 已完成低风险生命周期优化:AbstractColorPicker 不再在 attach 时为关闭态 picker 订阅 Window.Deactivated,改为 picker 打开时订阅,关闭或 detach 时释放。
这是结构收益,不是 visual tree 收益。ColorPickerShowCase 当前有 23 个 ColorPicker / GradientColorPicker 实例,页面加载后默认全是关闭态;优化前这些实例会在 attach 阶段全部挂到宿主 Window.Deactivated,优化后关闭态为 0 个订阅。打开态仍保留窗口失活自动关闭行为。
| 指标 | baseline | optimized | 改善 | 结论 |
|---|---|---|---|---|
Closed Window.Deactivated subscriptions |
23 | 0 | 100.00% removed | 有效,结构性收益 |
Open picker Window.Deactivated subscription |
1 / open picker | 1 / open picker | preserved | 行为不变 |
| Runtime visuals | 370 | 370 | 0.00% | 符合预期 |
| Runtime logical | 49 | 49 | 0.00% | 符合预期 |
| Repeated alloc mean | 6787.93 KB | 6772.82 KB | 0.22% | 小幅下降 |
| Repeated mean | 73.19 ms | 72.25 ms | 1.28% | 噪声内,不作为主收益 |
| Repeated median | 66.10 ms | 68.01 ms | -2.89% | 噪声内,不作为回退结论 |
| Repeated P95 | 110.93 ms | 104.11 ms | 6.15% | 噪声内,谨慎看待 |
T3.2/T3.3 继续处理打开态子控件:
GradientColorPickerView的 3 个同 owner、同生命周期RelativeSource={RelativeSource TemplatedParent}binding 改为TemplateBinding。ColorSpectrum仍按原来的 bitmap 生命周期生成图像;只把对应ImageBrush缓存在 bitmap 重建点,HSV 高频变化时复用 brush 并只更新Opacity/Fill引用。
| 指标 | baseline | optimized | 改善 | 结论 |
|---|---|---|---|---|
| Gradient view full TemplatedParent bindings | 3 | 0 | 100.00% replaced | 有效,结构性收益 |
ColorSpectrum UpdateBitmapSources base/overlay brush refs / 1000 updates |
source-derived 1000 / 1000 | 1 / 1 | ~99.90% fewer refs | 有效,交互热路径收益 |
ColorSpectrum UpdateBitmapSources allocation |
n/a | 0.3 bytes/update | n/a | 复测确认近零分配 |
ColorSpectrum full HsvColor update allocation |
24386.6 bytes/update | 21434.6 bytes/update | 12.11% fewer bytes | 有效,打开态交互热路径收益 |
| ColorPickerView.Default materialization | 17.311 ms/item | 13.066-21.383 ms/item | +24.52% to -23.52% | 波动过大,不作为收益 |
| ColorPickerView.NoAlpha materialization | 11.233 ms/item | 6.223-6.333 ms/item | +43.62% to +44.60% | 正向,仍需谨慎 |
| GradientColorPickerView.Default materialization | 15.002 ms/item | 11.009-11.875 ms/item | +20.84% to +26.62% | 正向,仍需谨慎 |
| ColorPicker.GalleryShape materialization | 14.248 ms/item | 10.202-12.252 ms/item | +14.01% to +28.40% | 正向但有抖动 |
| Opened view visual/logical count | unchanged | unchanged | 0.00% | 符合预期 |
本轮判断:优化有效,主收益是打开态 ColorPicker 面板在颜色拖动/滑条联动时不再反复创建 ImageBrush,以及 Gradient view 模板绑定走更轻的 TemplateBinding 路径。页面闭合态 Gallery 导航不是这轮主路径;最新顺序复测期间 load averages 仍约 8.81 到 11.76,物化 timing 只能作为参考,不作为唯一收益依据。
T3.3 本轮补充处理 ColorPickerPaletteGroup:旧实现每个 palette group attach 时都注册一次 PaletteColorItem.IsCheckedChangedEvent.AddClassHandler<ColorPickerPaletteGroup>。这是类级订阅,不是实例级订阅;多个 palette group 同时存在时,后一个 group 的颜色选择可能被先 attach 的 group 截获,且会让每次路由事件多走全局 Raised subscriber。现在改为 palette group 自己的实例级 AddDisposableHandler,detach 时释放。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
Global class handlers per attached ColorPickerPaletteGroup |
1 / group | 0 / group | (1 - 0) / 1 |
100.00% removed | 有效,结构性收益 |
| Correct owner notifications when selecting second palette group | 0 / selection | 1 / selection | (1 - 0) / 1 |
100.00% restored | 正确性修复 |
| Wrong first-group notifications when selecting second palette group | 1 / selection | 0 / selection | (1 - 0) / 1 |
100.00% removed | 正确性修复 |
| ColorPicker materialization timing | smoke-only | smoke-only | n/a | n/a | 单轮 smoke 只用于排查回退,不作为收益证明 |
T3.3 本轮继续处理 HsvValue 预览 / slider thumb brush:旧模板里同一个 HsvValue 分别通过 converter 生成 ColorPreview.Background、第三分量 slider thumb brush、alpha slider thumb brush;拖动取色时这三处每次更新都会创建新的 SolidColorBrush。现在 AbstractColorPickerView 持有两个实例级 brush(带 alpha / 不带 alpha),模板通过 TemplateBinding 复用,HsvValue 变化只更新 brush color。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
Preview/thumb SolidColorBrush allocations per HsvValue update / view |
3 | 0 | (3 - 0) / 3 |
100.00% removed | 有效,结构性收益 |
ColorSpectrum.HsvColorUpdate allocation |
24386.6 bytes/update | 21434.6 bytes/update | (24386.6 - 21434.6) / 24386.6 |
12.11% fewer bytes | 有效,交互热路径收益 |
ColorSpectrum.HsvColorUpdate time |
105.34 us/update | 62.82 us/update | (105.34 - 62.82) / 105.34 |
40.36% faster | 单轮 microbench,辅助参考,不作为页面导航收益 |
| Opened view visual/logical count | 162 / 1 | 162 / 1 | (162 - 162) / 162 |
0.00% | 符合预期 |
| ColorPicker materialization timing | smoke-only | smoke-only | n/a | n/a | 单轮 smoke 只用于排查回退,不作为收益证明 |
T3.3 本轮收口 ColorBlock / ColorSlider 透明棋盘背景:旧实现每次 TransparentBgBrushUtils.Build(size, fillColor) 都重新创建完整 DrawingBrush 图(DrawingBrush + GeometryDrawing + ConicGradientBrush + GradientStops + RectangleGeometry)。同一个 token 在 ColorPreview、第三分量 slider、alpha slider、gradient slider 中重复出现,应该共享同一份 token 派生 brush。现在按 (size, fillColor) 做 32 项有上限缓存;自定义颜色/尺寸仍按 key 隔离,避免跨主题误复用。
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Base/Media/DrawingBrush.cs:15-43:DrawingBrush是持有Drawing的 brush 对象。.referenceprojects/Avalonia/src/Avalonia.Base/Media/GeometryDrawing.cs:10-53:GeometryDrawing持有Geometry/Brush/Pen。.referenceprojects/Avalonia/src/Avalonia.Base/Media/ConicGradientBrush.cs:12-51:ConicGradientBrush持有 center / angle / gradient stops。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
| Same-token transparent background brush refs / 1000 builds | 1000 | 1 | (1000 - 1) / 1000 |
99.90% fewer refs | 有效,结构性收益 |
TransparentBgBrush.BuildSameToken allocation |
10569.1 bytes/update | 0.2 bytes/update | (10569.1 - 0.2) / 10569.1 |
99.998% fewer bytes | 有效,打开态背景热路径收益 |
TransparentBgBrush.BuildSameToken time |
14.33 us/update | 0.17 us/update | (14.33 - 0.17) / 14.33 |
98.81% faster | 单轮 microbench,辅助参考,不作为页面导航收益 |
Realized ColorPickerView same-token transparent brush refs (ColorPreview + 2 visible sliders) |
3 | 1 | (3 - 1) / 3 |
66.67% fewer refs | 状态验证覆盖 |
| ColorPicker materialization timing | smoke-only | smoke-only | n/a | n/a | 单轮 smoke 只用于排查回退,不作为收益证明 |
T3.3 本轮补充处理 Gradient stop/thumb 排序:旧实现用 OrderBy(...).ToList() 在取色、thumb 同步、active stop index 计算三条路径上创建 LINQ iterator 和 materialized list。现在保留稳定排序语义,改为 Count/indexer 复制 + 稳定插入排序,避免 LINQ 链。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
| Gradient stop sort LINQ chains / color lookup | 1 OrderBy().ToList() |
0 LINQ chains | (1 - 0) / 1 |
100.00% | 结构收益;相同 offset 保持原顺序 |
| Gradient thumb sort LINQ chains / sync + active index | 2 OrderBy().ToList() |
0 LINQ chains | (2 - 0) / 2 |
100.00% | 结构收益;相同 value 保持原顺序 |
| Stable equal-key order behavior | stable | stable | n/a | 0.00% | 行为保持 |
| Page-load timing claim | none | none | n/a | n/a | 本轮没有有效前后 timing,不声明页面级速度收益 |
T3.3 本轮补充处理 ColorBlock preview 背景:ColorPicker 空值背景从每次 new SolidColorBrush(Colors.Transparent) 改为 Avalonia 已缓存的 Brushes.Transparent,普通颜色在颜色没变时不再重建 SolidColorBrush / 写入 ColorBlockBackgroundProperty;GradientColorPicker 的空值/同一 brush 引用也做短路。
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Base/Media/Brushes.cs:674-676:Brushes.Transparent返回 known-color immutable brush。.referenceprojects/Avalonia/src/Avalonia.Base/Media/KnownColors.cs:118-136:known-color brush 会缓存复用。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
| ColorPicker empty ColorBlock background brush allocations / update | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;空值使用 cached Brushes.Transparent |
| ColorPicker same-color ColorBlock brush allocations / update | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;颜色没变不再创建新 brush |
| ColorPicker same-color ColorBlock property writes / update | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;避免重复 DirectProperty 写入和 AffectsMeasure |
| GradientColorPicker same-reference background writes / update | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;同一 gradient brush 引用不重复写入 |
说明:这是 preview 背景更新路径 structural-only 收益;没有新增 Gallery timing 对比,不声明页面加载速度提升。
T3.3 本轮最后补齐 GradientColorPickerTrack active thumb 兜底路径:缩减 gradient stop 后,如果当前 ActivatedThumb 已不在 Thumbs 中,旧实现用 Thumbs.First() 取第一个 thumb。Thumbs 是 List<GradientColorSliderThumb>,且已有 Thumbs.Count > 0 保护;现在直接用 Thumbs[0],语义不变,去掉一次 LINQ enumerator。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
| Active thumb fallback LINQ enumerators / gradient stop shrink | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;直接走 List<T> indexer |
| Removed thumb temp list growth reallocations / gradient stop shrink | dynamic | exact delta capacity |
structural | 结构收益 | 缩减数量已知,按 delta 预分配 |
| Default palette group list growth reallocations / picker view default palette setup | dynamic | exact 4 capacity | structural | 结构收益 | 默认 palette 数固定为 4 |
| Active thumb fallback semantics | first thumb | first thumb | n/a | 0.00% | 行为保持 |
| Page-load timing claim | none | none | n/a | n/a | 本轮没有有效前后 timing,不声明页面级速度收益 |
T3.4 本轮补齐 HEX 输入解析路径:ColorPickerInput 原先在确认输入时对 _hexValueInput.Text 创建 trimmed string 后再 Color.TryParse;现在使用 span trim + span Color.TryParse,保持 HEX 解析和 alpha 合成语义。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
| HEX input trim temp strings / confirm input | 1 string risk | 0 strings | (1 - 0) / 1 |
100.00% | structural-only;span trim 后直接 parse |
| HEX parse behavior | Color.TryParse(string) |
Color.TryParse(ReadOnlySpan<char>) |
n/a | 0.00% | 行为保持 |
| Page-load timing claim | none | none | n/a | n/a | 本轮没有有效前后 timing,不声明页面级速度收益 |
T3.4 本轮追加 ColorSpectrum 默认 HSV 缓存空集合收敛:实例创建时 _hsvValues 只需要支持 Count 和 indexer,真正 bitmap 生成后会替换为有容量的 List<Hsv>。因此默认空态改用 Array.Empty<Hsv>(),避免每个 ColorSpectrum 初始化时分配空 List<Hsv>,颜色取样和键盘移动语义不变。
| metric | baseline | optimized | formula | improvement | conclusion |
|---|---|---|---|---|---|
| ColorSpectrum empty HSV cache list allocations / instance | 1 | 0 | (1 - 0) / 1 |
100.00% | structural-only;默认空态复用 Array.Empty<Hsv>() |
| Generated HSV map capacity / bitmap rebuild | exact pixel count | exact pixel count | n/a | 0.00% | 行为保持;生成后仍使用 new List<Hsv>(pixelCount) |
ColorSpectrum arrow-key modifier enum HasFlag calls / keydown |
1 | 0 | (1 - 0) / 1 |
100.00% | structural-only;Control modifier 改为 bitwise check |
| GradientColorPicker excess text-run removal LINQ calls / sync | 1 per removed run | 0 | (1 - 0) / 1 |
100.00% | structural-only;直接 RemoveAt(lastIndex) |
| Page-load timing claim | none | none | n/a | n/a | 本轮没有有效前后 timing,不声明页面级速度收益 |
1. 资格门槛
Gallery source:controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/ColorPickerShowCase.axaml
| 项 | 数值 |
|---|---|
<atom:ColorPicker> |
20 |
<atom:GradientColorPicker> |
3 |
AbstractColorPicker total |
23 |
| 默认打开态 picker | 0 |
真实会话操作频率:
- 页面导航:至少 1 次。
- 打开 picker:示例页通常会逐项试用基础、hover、format、clear、gradient、palette group。
- palette group 选择:打开 palette 示例后每次点击色块触发。
- 窗口失活:低频,但必须保留打开态自动关闭语义。
结论:实例数明显 > 5,页面加载默认关闭态占多数,满足 SKILL Tier 1 §13。优化重点是让 closed controls 不承担 popup-open-only 的全局/window 订阅。
2. 根因
旧逻辑在 OnAttachedToVisualTree() 中执行:
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is Window window)
{
_attachedWindow = window;
window.Deactivated += HandleWindowDeactivated;
}
这把“打开态需要在窗口失活时关闭 picker”的行为成本提前到了页面加载阶段。ColorPickerShowCase 23 个 picker 默认关闭,仍会保留 23 条窗口事件订阅。每次窗口失活时,宿主窗口会遍历这些 handler,即使大部分 picker 没有打开。
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Controls/WindowBase.cs:74:WindowBase暴露Deactivated事件。.referenceprojects/Avalonia/src/Avalonia.Controls/WindowBase.cs:347-351:平台 deactivated 通知触发Deactivated?.Invoke(...)。
因此,这类订阅应按真实打开生命周期配对,而不是按 visual attach 生命周期配对。
ColorPickerPaletteGroup 的旧逻辑在实例 OnAttachedToVisualTree() 内调用 AddClassHandler:
PaletteColorItem.IsCheckedChangedEvent.AddClassHandler<ColorPickerPaletteGroup>(...);
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Base/Interactivity/RoutedEvent.cs:78-94:AddClassHandler订阅 routed event 的全局Raisedobservable。.referenceprojects/Avalonia/src/Avalonia.Base/Interactivity/RoutedEvent.cs:121-134:泛型AddClassHandler<TTarget>最终仍调用类级订阅。.referenceprojects/Avalonia/src/Avalonia.Base/Interactivity/Interactive.cs:30-42:AddHandler写入当前实例事件表。.referenceprojects/Avalonia/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs:21-30:AddDisposableHandler是实例AddHandler+ dispose/remove 的封装。
因此 palette group 的色块选择应该用实例级 handler;类级 handler 只能放在 static constructor 这类每类型一次的注册点。
3. 改动
AbstractColorPicker:
_attachedWindow改为_deactivatedWindow,语义变为“当前已订阅 deactivated 的窗口”。OnAttachedToVisualTree()只配置 trigger handler;如果控件 attach 时已经打开,才补订阅。NotifyPickerOpened()注册Window.Deactivated。NotifyPickerClosed()和OnDetachedFromVisualTree()释放订阅。RegisterWindowDeactivatedHandler(Window? window)先释放旧窗口再订阅新窗口,避免 reattach 或窗口变化时残留旧 handler。
ColorPickerPaletteGroup:
PaletteColorItem.IsCheckedChangedEvent.AddClassHandler<ColorPickerPaletteGroup>改为this.AddDisposableHandler(...)。- handler 不再捕获 attach 时的
this闭包,而是只处理当前 palette group 路由里的事件。 OnDetachedFromVisualTree()释放实例 handler 并清空 disposable。
AbstractColorPickerView:
- 增加带 alpha / 不带 alpha 两个实例级
SolidColorBrush。 ColorPickerViewTheme/GradientColorPickerViewTheme中ColorPreview.Background、ColorSpectrumThirdComponentSlider.ThumbColorValueBrush、ColorSpectrumAlphaSlider.ThumbColorValueBrush从 converter binding 改为TemplateBinding复用 brush。ColorPickerView/GradientColorPickerView在HsvValue变化时同步更新实例 brush 的Color。
状态验证:
- closed ColorPicker 不持有
_deactivatedWindow。 - open 路径会注册。
- close 路径释放。
- detach 路径释放。
- 两个 palette-enabled
ColorPickerView同时存在时,第二个 group 的色块选择只更新第二个 view,不会触发第一个 view。 HsvValue连续变化时,ColorPreview、第三分量 slider、alpha slider 复用原 brush 引用,同时颜色值更新正确。
4. 验证
4.1 构建
dotnet build -c Release -f net10.0 tools/performances/AtomUI.Performance/AtomUI.Performance.csproj
结果:0 Warning(s), 0 Error(s)。
dotnet build -c Debug tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj
结果:构建通过;保留既有 warning DataGridColumn._clipboardContentBinding is never used,非本轮新增。
4.2 状态验证
dotnet run -c Release -f net10.0 --no-build \
--project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- \
--verify-colorpicker-states
结果:ColorPicker state verification passed.
本轮新增覆盖:
- 第二个 palette group 的色块选择只触发第二个
ColorPickerView.ValueChanged。 - 第一个 palette group 不收到跨 group 的错误通知。
- 被选中的色值原样转发。
HsvValue连续变化时,预览块和 slider thumb brush 引用复用且颜色值更新正确。ColorPreview和两个可见ColorSlider共享同 token 透明背景 brush;不同 size 使用不同 key,恢复 size 后复用原 brush。
4.3 控件级复测
命令:
dotnet run -c Release -f net10.0 --no-build \
--project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- \
--suite colorpicker --count 80
| Scenario | baseline ms/item | optimized ms/item | baseline KB/item | optimized KB/item | Visual/root | Logical/root |
|---|---|---|---|---|---|---|
| ColorPicker.Default | 0.784 | 0.768 | 151.4 | 151.4 | 11 | 1 |
| GradientColorPicker.Text | 1.013 | 0.613 | 174.6 | 174.6 | 14 | 1 |
| ColorPicker.GalleryShape | 14.580 | 12.902 | 3808.0 | 3806.9 | 274 | 37 |
说明:控件级 runner 使用 Avalonia.Controls.Window,而生产代码这里订阅的是 AtomUI Window,所以这组 ms/item 只能用于排查结构/分配回退,不能证明本轮订阅优化的速度收益。
4.4 本轮 smoke 复测
单轮,只用于排查明显回退,不作为收益证明:
| Scenario | ms/item | KB/item | Visual/root | Logical/root |
|---|---|---|---|---|
| ColorPicker.Default | 0.929 | 139.8 | 11 | 1 |
| ColorPickerView.Default | 15.073 | 4586.9 | 162 | 1 |
| GradientColorPickerView.Default | 9.248 | 3787.8 | 173 | 1 |
| ColorPicker.GalleryShape | 10.378 | 3464.9 | 274 | 37 |
4.5 交互热路径复测
命令:
dotnet run -c Release -f net10.0 --no-build \
--project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- \
--measure-colorpicker-interactions --count 1000
| Scenario | baseline bytes/update | optimized bytes/update | baseline us/update | optimized us/update | baseline refs | optimized refs |
|---|---|---|---|---|---|---|
| ColorSpectrum.UpdateBitmapSources | 0.3 | 0.3 | 0.66 | 0.64 | 1000 / 1000 source-derived | 1 / 1 |
| ColorSpectrum.HsvColorUpdate | 24386.6 | 21434.6 | 105.34 | 62.82 | 6 / 6 | 6 / 6 |
| TransparentBgBrush.BuildSameToken | 10569.1 | 0.2 | 14.33 | 0.17 | 1000 | 1 |
4.6 Gallery 复测
命令:
dotnet run -c Debug -f net10.0 --no-build \
--project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj -- \
--showcase colorpicker --cold-iterations 5 --iterations 20 --warmup 5
机器状态:复测期间 load averages 约 9.18 13.25 13.93,timing 噪声偏大。
| Set | Mean | Median | P95 | Alloc mean | Visuals | Logical |
|---|---|---|---|---|---|---|
| Cold baseline | 177.11 ms | 178.37 ms | 190.72 ms | 7470.18 KB | 370 | 49 |
| Cold optimized | 173.07 ms | 170.03 ms | 182.46 ms | 7466.55 KB | 370 | 49 |
| Repeated baseline | 73.19 ms | 66.10 ms | 110.93 ms | 6787.93 KB | 370 | 49 |
| Repeated optimized | 72.25 ms | 68.01 ms | 104.11 ms | 6772.82 KB | 370 | 49 |
结论:visual/logical 不变,分配小幅下降;timing 混合且负载较高,不把导航耗时当成本轮主收益。