Files
AtomUI/docs/performances/TimePicker

TimePicker 性能优化

路线图位置:../desktop-controls-optimization-roadmap.md Tier 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 不会误清外部对象。
  • Popup shell 和 ArrowDecoratedBox 保持在 ControlTheme 中。

3.2 派生类释放 presenter cache

TimePicker、RangeTimePicker、DatePicker、RangeDatePicker 覆写 NotifyPickerPresenterCleared(),在 owned presenter 被释放时把 _pickerPresenter 置空,避免 detach 后保留 stale presenter 引用。

3.3 性能工具补齐

新增:

  • GalleryPerformance --showcase timepicker
  • AtomUI.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.

命令:

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 百分比。