TimePicker 性能优化
路线图位置:
../desktop-controls-optimization-roadmap.mdTier 1 #4 状态:本轮已完成 Gallery 实测。
0. 结论
本轮优化 InfoPickerInput 的 closed-state presenter 创建:TimePicker / RangeTimePicker 默认关闭时不再在 OnApplyTemplate() 中创建 TimePickerPresenter,而是在第一次打开 picker 前创建,并在 detach 时释放自己创建的 presenter。
这不是把模板视觉搬到 C# 动态创建。Popup shell、ArrowDecoratedBox、clear/icon accessory 仍保留在 axaml;延迟的是 popup 的重内容 presenter,符合 SKILL 的 Popup Lazy Content exception。
| 指标 | baseline | optimized | 改善 |
|---|---|---|---|
| Cold first navigation mean | 464.82 ms | 393.11 ms | 15.4% |
| Cold median | 435.50 ms | 389.74 ms | 10.5% |
| Cold P95 | 623.34 ms | 418.20 ms | 32.9% |
| Cold alloc mean | 25430.67 KB | 25062.11 KB | 1.5% |
| Repeated navigation mean | 148.79 ms | 138.48 ms | 6.9% |
| Repeated median | 142.46 ms | 137.02 ms | 3.8% |
| Repeated P95 | 180.65 ms | 151.63 ms | 16.1% |
| Repeated alloc mean | 23617.32 KB | 23430.26 KB | 0.8% |
| Runtime visuals | 1230 | 1230 | 0.0% |
TimeView / DateTimePanel |
0 / 0 | 0 / 0 | preserved |
1. 资格门槛
Gallery source:controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/TimePickerShowCase.axaml
| 项 | 数值 |
|---|---|
ShowCaseItem |
9 |
<atom:TimePicker> |
15 |
<atom:RangeTimePicker> |
7 |
Runtime InfoPickerInput |
22 |
Runtime AddOnDecoratedBox |
22 |
真实会话操作频率:
- 页面导航:至少 1 次。
- 打开 picker:basic、confirm、interval、12/24 hour、variants、status、range 示例都会触发。
- 选择/hover/confirm:打开后的
TimePickerPresenter事件链会参与状态同步。
结论:实例数 > 5,操作 > 1/session,满足 SKILL Tier 1 §13。
2. 根因
InfoPickerInput.OnApplyTemplate() 原来对所有 picker 都执行:
if (PickerPresenter is null)
{
PickerPresenter = CreatePickerPresenter();
NotifyPickerPresenterCreated(PickerPresenter);
}
这意味着 TimePickerShowCase 的 22 个 closed picker 会在页面加载时创建 22 个 presenter 对象和对应 direct bindings。由于 popup 关闭,presenter 模板不会展开成 TimeView,所以 visual count 没有下降空间;主收益来自跳过 closed-state presenter 对象构造、绑定建立和派生控件缓存同步。
Avalonia source reference:
.referenceprojects/Avalonia/src/Avalonia.Base/Data/TemplateBindingExpression.cs:37-43:TemplateBinding订阅 templated parent 的PropertyChanged,因此首次打开时设置PickerPresenter后,axaml 中的Content="{TemplateBinding PickerPresenter}"会继续更新。.referenceprojects/Avalonia/src/Avalonia.Base/AvaloniaObject.cs:407-422:内部 lazy 值使用SetCurrentValue,避免破坏外部绑定/优先级。.referenceprojects/Avalonia/src/Avalonia.Base/Layout/Layoutable.cs:546:隐藏元素跳过 measure;本轮没有为了省隐藏节点而移动模板视觉。
3. 改动
3.1 InfoPickerInput 延迟 presenter
新增:
EnsurePickerPresenter():仅在IsPickerOpen=true或模板应用时已经处于打开态时创建 presenter。_ownedPickerPresenter:只跟踪本控件内部创建的 presenter。ClearOwnedPickerPresenter():detach 时先关闭 picker,再通知派生类清理缓存,并仅在当前PickerPresenter仍是内部创建对象时清空属性。
关键边界:
- 外部提前设置
PickerPresenter时不创建内部 presenter。 - 外部后续替换
PickerPresenter时,detach 不会误清外部对象。 Popupshell 和ArrowDecoratedBox保持在 ControlTheme 中。
3.2 派生类释放 presenter cache
TimePicker、RangeTimePicker、DatePicker、RangeDatePicker 覆写 NotifyPickerPresenterCleared(),在 owned presenter 被释放时把 _pickerPresenter 置空,避免 detach 后保留 stale presenter 引用。
3.3 性能工具补齐
新增:
GalleryPerformance --showcase timepickerAtomUI.Performance --verify-timepicker-states
TimePicker state verification 覆盖:
- closed
TimePicker/RangeTimePicker不创建PickerPresenter。 - closed input icon 仍存在。
- 第一次 materialize 创建正确 presenter。
- 第二次 materialize 复用同一个 presenter。
- detach 后清理 owned presenter。
4. 验证
4.1 构建
dotnet build tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj -c Debug --framework net10.0 --no-restore
结果:构建通过;保留既有 warning DataGridColumn._clipboardContentBinding is never used,非本轮新增。
dotnet build tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -c Debug --framework net10.0 --no-restore
结果:0 Warning(s), 0 Error(s)。
4.2 状态验证
dotnet run --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj \
-c Debug --framework net10.0 --no-build -- \
--verify-timepicker-states
结果:TimePicker state verification passed.
4.3 Gallery 基线与优化后对比
命令:
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
-c Debug --framework net10.0 --no-build -- \
--showcase timepicker --label baseline \
--cold-iterations 10 --iterations 30 --warmup 5 --timeout-ms 30000 \
--markdown /tmp/timepicker-showcase-baseline.md
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
-c Debug --framework net10.0 --no-build -- \
--showcase timepicker --label lazy-presenter \
--cold-iterations 10 --iterations 30 --warmup 5 --timeout-ms 30000 \
--markdown /tmp/timepicker-showcase-lazy-presenter.md
| Set | Mean | Median | P95 | Alloc mean | Visuals | InfoPicker |
TimeView |
DateTimePanel |
|---|---|---|---|---|---|---|---|---|
| Cold baseline | 464.82 ms | 435.50 ms | 623.34 ms | 25430.67 KB | 1230 | 22 | 0 | 0 |
| Cold optimized | 393.11 ms | 389.74 ms | 418.20 ms | 25062.11 KB | 1230 | 22 | 0 | 0 |
| Repeated baseline | 148.79 ms | 142.46 ms | 180.65 ms | 23617.32 KB | 1230 | 22 | 0 | 0 |
| Repeated optimized | 138.48 ms | 137.02 ms | 151.63 ms | 23430.26 KB | 1230 | 22 | 0 | 0 |
4.4 Shared surface smoke
InfoPickerInput 同时服务 DatePicker。本轮额外跑了 DatePicker showcase,确认关闭态仍没有创建 presenter visual:
dotnet run --project tools/performances/AtomUI.GalleryPerformance/AtomUI.GalleryPerformance.csproj \
-c Debug --framework net10.0 --no-build -- \
--showcase datepicker --label lazy-presenter \
--cold-iterations 10 --iterations 30 --warmup 5 --timeout-ms 30000 \
--markdown /tmp/datepicker-showcase-lazy-presenter.md
结果:DatePickerPresenter=0、RangePickerPresenter=0、TimeView=0、DateTimePanel=0,runtime visuals 1840。
5. 追加结构优化:输入框 preferred-width 计算
共享 DateTimeUtils.CalculateWidestFormattedDateTimeSize() 的月份、星期和 AM/PM 样本扫描改为 for loop / index 分支,不再为每次宽度计算创建 Enumerable.Range 或小时数组。该工具路径被 DatePicker / TimePicker preferred input width 复用。
| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 |
|---|---|---|---|---|---|
| 月份扫描 enumerable / widest datetime width calculation | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;按需月份扫描不再分配 enumerable |
| 星期扫描 enumerable / widest datetime width calculation | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;按需星期扫描不再分配 enumerable |
| AM/PM 小时样本数组 / widest datetime width calculation | 1 array | 0 arrays | (1 - 0) / 1 |
100.00% | 结构收益;小时样本改为索引分支 |
说明:这是 preferred-width 计算路径的结构性收益;本轮不声明 TimePickerShowCase 页面导航 timing 因此提升。
6. 追加结构优化:presenter 事件生命周期
本轮只收敛打开态 TimePickerPresenter / DateTimePickerPanel 的事件注册方式,不改变模板视觉、padding、popup shell 或选择语义。
| 指标 | baseline | optimized | 公式 | 提升 | 结论 |
|---|---|---|---|---|---|
TimePickerPresenter re-template retained handlers / presenter |
7 | 0 | (7 - 0) / 7 |
100.00% | 旧 TimeView / Now / Confirm 事件在重新套模板前可完整解绑 |
TimePickerPresenter anonymous pointer handler callsites |
2 | 0 | (2 - 0) / 2 |
100.00% | Confirm enter/exit 改为 method-group handler |
TimePickerPresenter choosing-state subscription lambda callsites |
1 | 0 | (1 - 0) / 1 |
100.00% | TimeView.IsPointerInSelector 订阅改为 method-group handler |
DateTimePickerPanel pointer-enter delegate allocations / new cell |
1 | 0 | (1 - 0) / 1 |
100.00% | 每个 panel 复用一个 cached handler,新建 TimeViewCell 不再各自创建 delegate |
| attached presenter choosing-state stale subscription risk | 1 | 0 | (1 - 0) / 1 |
100.00% | OnApplyTemplate() 后如果已经 attached,会刷新当前 TimeView observable 订阅 |
说明:这是结构与生命周期收益。DatePicker suite 中的 presenter-only 场景覆盖了 shared DateTimePickerPanel,但本轮不声明新的页面 timing 百分比。