Tooltip 性能优化
路线图位置:
../desktop-controls-optimization-roadmap.mdTier 0 #6 状态:本轮已完成结构清理;headless bench 无法触发 Show 流程(pointer 事件不会自动 fire),bench 验证留待 Gallery 级 hover 场景。
1. 范围
src/AtomUI.Desktop.Controls/Tooltip/ToolTipService.cs— 全局 ToolTip 服务(singleton via AvaloniaLocator),处理所有 ToolTip.Tip 控件的 hover-触发。src/AtomUI.Desktop.Controls/Tooltip/ToolTip.cs— 附加属性 + Show/Close 状态机;本轮追加同色背景写入短路。
2. 改动
ToolTipService.StartShowTimer 复用 DispatcherTimer
原实现(每次 Show 触发都 allocate 一个 timer + 闭包):
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay), Tag = (this, control) };
_timer.Tick += (o, e) =>
{
if (_timer != null)
{
Open(control);
}
};
_timer.Start();
}
private void StopTimer()
{
_timer?.Stop();
_timer = null;
}
ToolTipService 是 AvaloniaLocator 注册的 singleton(ThemeManagerBuilderExtensions.cs:41),整个会话只有一个实例,但每次 hover 进入新控件 → HandleTipControlChanged → StartShowTimer 都 new DispatcherTimer(...) + 一个 lambda 闭包(捕获 this 和 control)。然后 StopTimer 把字段置 null —— timer 对象 GC,闭包 GC。
Form 30 个 tooltipped 字段,用户 hover 划过一遍:30 次 timer 分配 + 30 次闭包分配。
新实现:复用 timer 实例,Tick handler 是绑定到方法的 EventHandler(不是闭包):
private void StartShowTimer(int showDelay, Control control)
{
if (_timer == null)
{
_timer = new DispatcherTimer();
_timer.Tick += HandleShowTimerTick;
}
_timer.Stop();
_timer.Tag = control;
_timer.Interval = TimeSpan.FromMilliseconds(showDelay);
_timer.Start();
}
private void HandleShowTimerTick(object? sender, EventArgs e)
{
if (_timer is null)
{
return;
}
_timer.Stop();
if (_timer.Tag is Control pendingControl)
{
Open(pendingControl);
}
}
private void StopTimer()
{
_timer?.Stop();
if (_timer != null)
{
_timer.Tag = null;
}
}
节省(结构性,每次 Show):
| 项 | 旧 | 新 |
|---|---|---|
DispatcherTimer 实例 |
1 per Show | 1 lifetime |
Tick lambda 闭包 |
1 per Show | 0(method group 引用) |
Tag tuple (this, control) 装箱 |
1 per Show | 0(直接存 control) |
_timer != null 状态切换 |
new/null 反复 | 持久存在,仅 Stop/Start |
Form/Slider/Steps/DataGrid 30+ tooltipped 字段 hover 一遍:~30 timer allocations 消除 + ~30 closure allocations 消除。
ToolTip.SetToolTipColor 同色背景短路
preset/custom color tooltip 每次打开都会把 Background 写成新的 SolidColorBrush 并 InvalidateVisual()。本轮在颜色与当前 solid background 完全一致时直接返回,避免重复 brush 分配和重复 render invalidation;颜色不同或当前 background 不是默认 opacity solid brush 时仍按旧路径写入。
| 指标 | 优化前 | 优化后 | 公式 | 提升 | 结论 |
|---|---|---|---|---|---|
| Tooltip same-color background brush allocations / open | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;重复 hover 同色 tooltip 不再分配新 brush |
| Tooltip same-color explicit invalidations / open | 1 | 0 | (1 - 0) / 1 |
100.00% | 结构收益;颜色没变不再主动 invalidation |
说明:这是 hover-show 路径的 structural-only 收益;未声明页面加载 timing 提升。
3. 不在本轮范围
ToolTip.Open中 9 条 CompositeDisposable 绑定(ToolTip.cs:380-390):每次 Show 创建一个CompositeDisposable+ 9 个 binding observer。Close 时全部 dispose。这是按"每次 hover 进入不同控件需要重新绑定到当前 AdornedControl 属性"设计的,不能简单复用。要改的话需要把 popup 改成观察一个_currentAdornedControl字段而不是直接 bind 到 control 属性。结构性较大改动,留作 follow-up。ToolTipService.Update在每次 pointer move 都走 visual 树查找 tooltipped 祖先(ToolTipService.cs:95-117):这是 ToolTip 服务的根本设计——hover 即触发,无可缓存的状态。pointer-move 频率高但单次成本低(visual 树深度 < 30)。无可优化点。AbstractTag风格的"构造器 token binding 迁主题 Setter":ToolTip 没有这个反模式。Theme 已经是干净的 Setter 形式。
4. 验证
4.1 控件库构建
dotnet build src/AtomUI.Desktop.Controls/AtomUI.Desktop.Controls.csproj -c Debug --framework net10.0
→ Build succeeded. 0 Warning(s) 0 Error(s)
4.2 微基准
AtomUI.Performance headless 环境无法触发 RawPointerEventArgs,所以 ToolTipService 的 Show 流程跑不到。本轮改动只能靠结构性论证 + Gallery 走查验证。
未建立 --suite tooltip,原因同上:bench 跑不出 hover 流程,建场景成本 > 收益。
4.3 Gallery 视觉走查(人工)
需走查的最小用例:
- TooltipShowCase:基本 Tip 显示/消失、preset color、custom color、不同 placement、IsArrowVisible 切换。
- FormShowCase:表单字段的 tooltipped 帮助 icon hover 体验。
- SliderShowCase:thumb 上的 value tooltip。
行为不变性:
- Show delay / between-show delay 行为:第一次 hover 默认 400ms,连续 hover 间 100ms(与原一致)。
- Close 行为:pointer 离开/点击其他位置触发 close,与原一致。
- 多个 tooltipped 控件之间快速切换:timer 复用不引入跨控件污染(每次
_timer.Tag = control覆盖)。
5. 复杂度自评(SKILL Process Gate 3)
| 指标 | 数值 |
|---|---|
新增 Ensure*/Clear*/Sync* 方法 |
0(分离 lambda 为命名方法 HandleShowTimerTick,是清理) |
| 新增 try/finally 标志位 | 0 |
| 同一文件新增 disposable 字段 | 0 |
| C# 净改 | +11 / -7 |
| Theme Static Rule 检查 | 未触发 |
6. 改动文件清单
| 路径 | 改动 |
|---|---|
src/AtomUI.Desktop.Controls/Tooltip/ToolTipService.cs |
StartShowTimer 改为复用 timer;新增 HandleShowTimerTick method group |
总计:1 个生产文件,~18 行改动。
7. 已知不足
- 无 microbench 对比数据(headless 限制)。real-world hover-heavy 场景(Form / DataGrid / Slider tooltip)走 Gallery 实测才能得到数字。
ToolTip.Open9 条 popup binding 的 per-show 分配是更大的潜在杠杆,留作下一轮 Tooltip-specific 优化(需要更小心的 popup-state 绑定重定向)。