perf(Core): 控件主题快照按 slot 惰性编译,ApplyTheme 同步路径再省约 61ms

This commit is contained in:
chinboy
2026-10-12 07:42:17 +08:00
parent b3557e188d
commit 4fab9a043e
8 changed files with 565 additions and 71 deletions
+17 -5
View File
@@ -589,9 +589,9 @@ ThemeCompileInput
-> apply Map overrides
-> format Alias Token
-> apply Alias overrides and AtomUI final normalization
-> evaluate each registered Control
-> freeze token values and resources
-> ThemeSnapshot
-> resolve each registered Control slot state (cheap config comparison)
-> freeze global token values and resources
-> ThemeSnapshot (control slots compile on first query)
```
编译阶段允许使用可变 builder,因为算法天然需要逐步派生。builder 的生命周期只限于一次编译,不能进入
@@ -621,7 +621,15 @@ snapshot,也不能通过公开 API 返回。
Control 默认 Token evaluator 必须接收 Control appearance 和 Effective Global Token,不能继续只读取全局
`IsDark`。
任何 validation、conversion 或 algorithm 错误都返回失败结果,不返回部分 snapshot。
控件槽位编译契约:`ApplyTheme` 返回时全局 token 与资源完整提交;控件级 `ControlThemeSnapshot`
按槽位惰性编译,首次查询时经同一 `CompileControl` 函数与同一缓存键编译,结果值与急切编译逐位
相等(急切参考路径 `CompileControlsEagerForTest` 差分锁定)。首帧内首次实例化的控件一次性支付
内联编译成本(~0.7ms 量级)。主题切换复用同样惰性:子快照槽位记录父快照引用,首访时物化并共享
同一对象。失败语义同步后移:控件级编译错误(不支持的算法模式、evaluator 抛出等)不再使
`ApplyTheme` 整体失败,而是在首次查询该槽位时抛出、后续每次查询重复抛出。
任何 validation、conversion 或 algorithm 错误都返回失败结果,不返回部分 snapshot——该原子性指
全局维度(Seed/算法链/全局 token/资源);控件维度的失败面按槽位延后到首查(见上方契约段)。
## 8. ThemeSnapshot
@@ -636,7 +644,7 @@ ThemeSnapshot
+-- GlobalTokenValues: dense immutable TokenValueTable
+-- GlobalResources
+-- PresetColorPalettes
\-- Controls[registry control slot]
\-- Controls: LazyControlSnapshots[registry control slot] (compile on first query)
+-- Appearance
+-- EffectiveGlobalTokenDelta
+-- ControlTokenValues: dense immutable TokenValueTable
@@ -648,6 +656,10 @@ ThemeSnapshot
- Token schema 由生成器分配在当前 registry revision 内稳定的 slot;完整 Token 值表使用稠密不可变数组,不使用
object-key 字典保存每个值。
- Control 表按 registry slot 存放引用,`ControlTokenIdentity` 只用于配置边界和诊断。
- Control 表是按槽位惰性编译的容器(只暴露 Count 与索引器,不实现枚举);构造期解析槽位状态
(已物化 / 复用父槽 / 待编译),保留字节估算为构造期基线加首访物化增量。缓存在插入时采样
`EstimatedRetainedBytes`,物化增长不回灌——估算漂移上界为全部槽位字节,只影响驱逐时机,
不影响正确性。
- 稀疏覆盖、Control Effective Global Token 差量和 Avalonia 资源投影使用不可变集合或 `FrozenDictionary`。
- 颜色资源在 snapshot 中已经转换为不可变 Brush。
- palette、集合和其他引用值必须在冻结前深复制为不可变值。
@@ -50,7 +50,7 @@ harness 为空窗口真实桌面注册(UseDesktopControls)。
热点兑现核对:A 估 60-90ms 实测 −24.2ms(部分份额已被前轮 914ce40ef 取走);B 估 50-70ms 实测
−71.7ms(达成,经方案 B 惰性挂载实现——原计划"生成等价 C# wrapper"机制因 Avalonia 编译期 include
展开为急切 Build 调用且运行时 ResourceInclude 在 AOT 下不可用而废弃);D 估 20-30ms 实测 3.1-4.7ms
(估计基于 Task 2 前缀续算前的测量,主份额已被 Task 2 取走);C 待用户批准(契约修订门,见热点 C 节)。
(估计基于 Task 2 前缀续算前的测量,主份额已被 Task 2 取走);C 估 62-68ms 实测 −60.9ms(SyncTotal,已实施,见热点 C 节"实施与实测")。
正确性验收:Core 528/528、Generator 458/458、Desktop 全套 4184 零崩溃(4 个失败为急切基线同样复现的
存量负载敏感 flaky);Gallery osx-arm64 全裁剪 NativeAOT 发布通过 + 产物运行冒烟通过;整分支新鲜
@@ -225,6 +225,30 @@ ThemeSnapshot.Controls 由 IReadOnlyList<ControlThemeSnapshot> 改为按需槽
预期收益:空窗口 CompileControls 72.3 → **~2-5ms**(仅 Window 所需槽位);
代价:每控件首次实例化 +~0.5-0.7ms(Gallery 页面导航首批控件摊销)。
### 实施与实测(2026-10-11 已实施,契约修订经用户批准)
实施:新增 `LazyControlSnapshots`(Count + 索引器,不实现枚举)+ `LazyControlSnapshotSource`
(冻结编译输入)+ `ControlSlotState`(Materialized/ReuseParent/Uncompiled 构造期急切解析);
`CompileControls` 改惰性解析循环,旧急切循环原样保留为 `CompileControlsEagerForTest` 差分钩子;
`ThemeSnapshot.EstimatedRetainedBytes` 改实时属性(构造期基线 + `MaterializedBytes` 增量),
估算器删全量枚举;契约修订落入 [runtime.md](../../architecture/systems/theming/runtime.md) §7/§8。
spec:`docs/superpowers/specs/2026-10-11-hotspot-c-lazy-control-snapshots-design.md`。
实测(交错 10× 采样中位数,对照 b3557e188 基线):
| 指标 | 基线 | 惰性 | Δ |
|---|---:|---:|---:|
| UseAtomUI.SyncTotal | 452.2 | 391.2 | **−60.9** |
| UseAtomUI.PostConfigure | 255.9 | 189.8 | **−66.1** |
| Mark.FrameworkCompleted | 686.7 | 644.9 | −41.8 |
| Mark.FirstRender | 962.4 | 935.6 | −26.8 |
| Mark.DispatcherIdle | 1032.1 | 1001.1 | −31.0 |
验收:193 槽位真实注册表差分(主变体 + ReuseParent/Uncompiled 混合变体)逐 token/逐资源/逐保留字节
结构相等 ✓;启动 MaterializedCount 实测 **0**(精确棘轮)✓;并发单飞(同槽/异槽)✓;估算增量恒等 ✓;
主题切换 Committed + 首查契约 ✓。差分方法论修正:Easing/Brush/Transform 每编译克隆自有不可变副本
(所有权边界),引用不等是既有设计,差分按结构比较(eager-vs-eager 探针证实与惰性化无关)。
---
## 热点 D:ThemeManagerBuilder.Build 46.1ms —— Revision 指纹与冻结仍全量
@@ -357,4 +381,4 @@ ICU 初始化与 JIT 无算法空间。`LanguageManager` 的三次小复制已
问题 6 已由 MatchedBundleResolver 实施。
本文所有预期收益为源码复杂度推算,**未经实现验证**;每项实施后回填实测并更新状态。
【2026-10-11 状态更新:热点 A/B/D 已实施并实测回填(见上方"最终实测收益"表);热点 C 待用户批准(契约修订门);热点 E 维持"不立项"结论。】
【2026-10-11 状态更新:热点 A/B/C/D 已实施并实测回填(见上方"最终实测收益"表与热点 C 节"实施与实测");热点 E 维持"不立项"结论。】
@@ -0,0 +1,155 @@
# 热点 C 设计:控件主题快照按 slot 惰性编译
日期:2026-10-11
状态:设计已获用户批准(含两个实施细节决策点)
上游材料:`docs/superpowers/progress/2026-10-11-macos-startup-remaining-hotspots.md` 热点 C 节;
`docs/superpowers/plans/2026-10-11-startup-remaining-hotspots-optimization.md` Task 6(契约修订批准门)
## 背景与基线
`ThemeCompiler.CompileControls`(ThemeCompiler.cs:237)在主题编译末期对注册表全部 187 个控件急切编译
`ControlThemeSnapshot`,实测 72.3ms:
| 子项 | 实测 |
|---|---:|
| Own Token 计算(87 个控件,1032 个派生公式) | 34.5ms |
| Token 冻结(TokenValueTable.Freeze + 逐值保留字节估算) | 17.5ms |
| 资源投影(BuildResourceMap 装箱/冻结/FrozenDictionary) | 9.9ms |
| 循环、复用判定、快照分配 | ~10.4ms |
运行时对控件快照的唯一消费是按槽位点查(`snapshot.Controls[slot]`),空窗口首帧前查询次数为零——
约 62ms 预编译换取零次查询。本设计把控件级快照改为首查时编译,预期同步启动路径再省 ~62-68ms。
## 契约修订(用户已在 Task 6 门批准)
现行(`docs/architecture/systems/theming/runtime.md` §7 编译器管线 "evaluate each registered Control →
freeze → ThemeSnapshot" 为一次同步完成,§8 ThemeSnapshot 的 `Controls[registry control slot]` 为编译期
全量数组;即计划 Task 6 所称"首帧完整主题提交"):
`ApplyTheme` 返回时全部控件快照已编译、立即可查。
修订后:`ApplyTheme` 返回时**全局 token/资源完整提交**,控件级快照**首次查询时编译**。
runtime.md §7 的管线描述与 §8 的 `Controls` 语义随实施同步修订。
- 任何资源查询的**结果值不变**(差分测试机械锁定);
- 公开 API 不变(`ThemeSnapshot`/`ControlThemeSnapshot` 均为 internal);
- 首帧内首次实例化的控件各自一次性支付 ~0.7ms(含 JIT)内联编译;
- "切换主题后控件 token 立即可查"变为"首次查询时可查",结果相同、时点不同。
## 消费者清单(源码核实,穷举)
`ThemeSnapshot.Controls` 的全部消费点共 7 处,形态只有 `Count` 检查与索引器点查两种:
1. `ThemeTokenResolver.cs:50-54`(token 取值)、`:65-80`(按名回退扫描)、`:107-112`(资源键回退)
2. `ThemeTokenResourceProvider.cs:53-59`、`:87`、`:100-102`(TryGetResource 三路径)
3. `ThemeRetainedBytesEstimator.cs:42-43`(**唯一的枚举消费者**——估算重写对象)
4. `ThemeCompiler.cs:278`(主题切换复用:`controls[slot] = ReusableParent.Controls[slot]`)
## 设计
### 1. LazyControlSnapshots(新 internal 类型)
替代 `IReadOnlyList<ControlThemeSnapshot>`,**只暴露 `Count` 与索引器 `[int slot]`,刻意不实现
`IEnumerable`**——枚举即全量物化,正是 `EstimateSnapshot` 的陷阱,从类型上禁绝。
```csharp
internal sealed class LazyControlSnapshots
{
// 状态在快照构造期急切解析(廉价配置比较),编译推迟到首访。
private enum SlotState : byte { Empty, ReuseParent, Uncompiled }
private readonly SlotState[] _states;
private readonly ControlThemeSnapshot?[] _materialized; // 物化缓存
private readonly ControlThemeSnapshot[] _eagerEmpty; // Empty 槽位的急切共享实例
private readonly LazyControlSnapshotSource _source; // Uncompiled 槽位的编译输入(冻结)
private readonly ThemeSnapshot? _reusableParent; // ReuseParent 槽位的父快照
private readonly object _gate = new();
private long _materializedBytes; // 估算增量累计(Interlocked)
private int _materializedCount; // 测试观测口
internal int Count => _states.Length;
internal int MaterializedCount => _materializedCount; // 测试观测口
internal long MaterializedBytes => Interlocked.Read(ref _materializedBytes);
internal ControlThemeSnapshot this[int slot] =>
_materialized[slot] ?? Materialize(slot);
}
```
`Materialize(slot)`:按状态分派——Empty 返回急切实例;ReuseParent 读父快照索引器
(父槽未物化则此时物化父槽并共享同一对象);Uncompiled 进 `_gate` 双检后经
`ControlCompilationCache.GetOrCompile`(缓存键不变:GlobalCompilationCacheKey + descriptor +
config + appearance)编译,回填 `_materialized[slot]` 并 `Interlocked.Add` 估算字节与计数。
### 2. 急切解析 + 惰性编译(决策点 1,已批准)
`CompileControls` 保留每槽位廉价判定循环(FindControlConfig / IsControlChanged / CanReuseControl,
~9.5ms),把每槽位解析为三种状态;只推迟昂贵的 `CompileControl`(~62ms)。
被否决的备选"全惰性"(槽位判定也推迟):可多省 ~8ms,但 FindControlConfig 的光标是跨槽位顺序
扫描优化,惰性化需改为每槽位二分查找并重排 changeSet 游标——复杂度高、收益小。
### 3. 主题切换:首访时父槽位物化复用
`ThemeCompiler.cs:278` 的复用改写为子槽位记录 `ReuseParent(parent, slot)`,切换期零编译。
首访时物化父槽并共享——复用语义本就是同一对象引用,物化父槽(~0.7ms)与子槽自编译成本相同。
未物化槽位在切换期不被被动触发(changeSet 惰性继承成立)。
### 4. 估算记账:急切基线 + 增量回填(决策点 2,已批准)
`ThemeSnapshot.EstimatedRetainedBytes` = 全局部分急切(现状)+ Empty 槽位急切 +
`controls.MaterializedBytes`(物化时增量)。ReuseParent 槽共享父对象只计引用字节。
`ThemeRetainedBytesEstimator.EstimateSnapshot` 不再枚举槽位(删除 :42-43 的 foreach,
改为读 `MaterializedBytes` 快照值)。
已知取舍:两个缓存(ThemeSnapshotCache / ControlCompilationCache)在**插入时**采样
`EstimatedRetainedBytes`(BoundedLruCache 尺寸委托),物化增长不回灌。估算是启发式容量阀,
漂移上界 = 实消费槽位字节(≤187 槽位 × 单槽 KB 级),可接受,写入代码注释。
### 5. 并发
查询可从任意线程进入:索引器先无锁读 `_materialized[slot]`,未命中进 `_gate` 双检。
`BoundedLruCache` 自带 `_gate` 锁与单飞创建(InFlightCount 语义),缓存路径安全。
编译输入(TokenValueTable 已 Freeze、NormalizedControlThemeConfig、算法列表、外观)在快照
构造期冻结,`CompileControl` 纯构造新对象,无共享突变。
## 等价性论证
同一 `ThemeSnapshot` 内编译输入(globalValues/globalResources/palettes/config/appearance/registry)
在构造期冻结,槽位延迟编译与批量编译走**同一个** `CompileControl`(ThemeCompiler.cs:354)函数与
同一个缓存键——逐位相等是结构性质,由差分测试机械证明。
## 测试矩阵(临时测试,验收后按生命周期删除)
1. **187 槽位差分**:真实桌面注册构建两个同输入快照——急切参考(测试钩子直调现有
CompileControls 急切路径)与惰性——逐槽位比对 token 值、资源键值、保留字节。
2. **空窗口物化计数**:启动后 `MaterializedCount` ≈ 0–2(仅 Window 链),精确门限回归。
3. **主题切换**:Light/Dark/Compact/局部 scope 切换前后同一控件查询结果不变;切换耗时不回归
(对照急切基线,容差内)。
4. **并发首查**:N 线程同时首访同一槽位,编译工厂调用计数 == 1,全部返回同一实例。
5. **估算增量**:物化 N 槽后 `EstimatedRetainedBytes` == 急切基线 + Σ 已物化槽位估算 +
复用槽引用字节。
6. **复用共享**:切换后 ReuseParent 槽位首访返回的对象与父槽位 `ReferenceEquals`。
## 改动文件
- 新增:`src/AtomUI.Core/Theme/Compilation/LazyControlSnapshots.cs`
- 修改:`ThemeSnapshot.cs`(Controls 类型 + 估算接线)、`ThemeCompiler.cs`(CompileControls
解析循环输出状态机、复用路径改 ReuseParent 标记)、`ThemeRetainedBytesEstimator.cs`
(删枚举改读增量)、`ThemeTokenResolver.cs` / `ThemeTokenResourceProvider.cs`
(类型跟随,索引器签名不变)
- 测试钩子:ThemeCompiler 暴露 internal 急切编译入口(IVT,仅供差分测试对照)
- 文档:`docs/architecture/systems/theming/runtime.md` §7 编译器管线与 §8 ThemeSnapshot
`Controls` 语义修订(全局急切 + 控件级首查编译)
## 非目标
- 不改变任何查询结果、缓存键、公开 API;
- 不改 `BoundedLruCache`(漂移接受,见 §4);
- 不动全局 token/资源的急切提交;
- 不做"全惰性"(槽位判定推迟)。
## 风险与回退
- 首查线程上 ~0.7ms 内联编译:可感知性极低(与控件实例化同帧既有成本同量级);
- 估算漂移导致缓存提前/滞后驱逐:量级有界,驱逐只影响性能不影响正确性;
- 回退:单提交 revert 恢复急切 CompileControls(无跨模块状态迁移)。
@@ -0,0 +1,195 @@
using AtomUI.Theme.Configuration;
using AtomUI.Theme.DesignTokens;
using AtomUI.Theme.Schema;
namespace AtomUI.Theme.Compilation;
/// <summary>
/// 控件快照槽位状态,快照构造期急切解析(见 ThemeCompiler.CompileControls)。
/// </summary>
internal enum ControlSlotState : byte
{
/// <summary>构造期已物化(无配置且无 Own Token 的槽位在构造期拿到廉价空快照)。</summary>
Materialized,
/// <summary>复用父快照同槽位对象:切换期零编译,首访时物化父槽并共享同一引用。</summary>
ReuseParent,
/// <summary>编译输入已冻结,首访时经 CompileControl / ControlCompilationCache 编译。</summary>
Uncompiled,
}
/// <summary>
/// Uncompiled 槽位的冻结编译输入。快照构造期由 ThemeCompiler 填充;CompileControl 纯读这些
/// 输入构造新对象、无共享突变,因此惰性编译与急切编译逐位等价(同一函数、同一缓存键)。
/// </summary>
internal sealed class LazyControlSnapshotSource
{
private readonly ThemeSchemaRegistry _registry;
private readonly NormalizedControlThemeConfig?[] _configs;
private readonly IReadOnlyList<ThemeAlgorithmDescriptor> _globalAlgorithms;
private readonly ThemeAppearance _globalAppearance;
private readonly DesignToken _globalBuilder;
private readonly TokenValueTable _globalValues;
private readonly IReadOnlyDictionary<object, object?> _globalResources;
private readonly ControlCompilationCache? _cache;
private readonly GlobalCompilationCacheKey _globalKey;
internal LazyControlSnapshotSource(
ThemeCompileInput input,
NormalizedControlThemeConfig?[] configs,
ThemeAppearance globalAppearance,
DesignToken globalBuilder,
TokenValueTable globalValues,
IReadOnlyDictionary<object, object?> globalResources,
ControlCompilationCache? cache)
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(configs);
_registry = input.Registry;
_configs = configs;
_globalAlgorithms = input.EffectiveConfig.Algorithms;
_globalAppearance = globalAppearance;
_globalBuilder = globalBuilder;
_globalValues = globalValues;
_globalResources = globalResources;
_cache = cache;
// 与急切路径的 globalCompilationCacheKey ??= 语义等价:source 只在存在 Uncompiled 槽位时构造。
_globalKey = GlobalCompilationCacheKey.Create(input);
}
internal ControlThemeSnapshot Compile(int slot)
{
var descriptor = _registry.Controls[slot];
var config = _configs[slot];
ControlThemeSnapshot Factory()
{
return ThemeCompiler.CompileControl(
descriptor,
config,
_globalAlgorithms,
_globalAppearance,
_globalBuilder,
_globalValues,
_globalResources,
_registry.GlobalTokens);
}
return _cache is null
? Factory()
: _cache.GetOrCompile(_globalKey, descriptor, config, _globalAppearance, Factory);
}
}
/// <summary>
/// 控件主题快照的按槽位惰性容器。只暴露 Count 与索引器,刻意不实现 IEnumerable——
/// 枚举即全量物化,正是快照保留字节估算要避免的形态(见 ThemeRetainedBytesEstimator)。
/// 状态与编译输入在快照构造期急切解析并冻结;昂贵的 CompileControl 推迟到首访
/// (空窗口首帧前对控件快照的查询次数为零,急切预编译是纯启动损耗)。
/// 线程契约:索引器可从任意线程进入;未物化槽位经 _gate 双检单飞物化,无锁快读命中
/// 已物化槽位(引用写入原子、无撕裂;读到过期 null 只是多进一次锁,双检兜底)。
/// </summary>
internal sealed class LazyControlSnapshots
{
// 与 ThemeRetainedBytesEstimator.ReferenceBytes 对齐:复用槽共享父对象,只计引用字节。
private const long SharedSlotReferenceBytes = 8;
private readonly ControlSlotState[] _states;
private readonly ControlThemeSnapshot?[] _materialized;
private readonly ThemeSnapshot? _reusableParent;
private readonly LazyControlSnapshotSource? _source;
private readonly object _gate = new();
private long _materializedBytes;
private int _materializedCount;
internal LazyControlSnapshots(
ControlSlotState[] states,
ControlThemeSnapshot?[] preMaterialized,
ThemeSnapshot? reusableParent,
LazyControlSnapshotSource? source)
{
ArgumentNullException.ThrowIfNull(states);
ArgumentNullException.ThrowIfNull(preMaterialized);
if (states.Length != preMaterialized.Length)
{
throw new ArgumentException("The state and pre-materialized arrays must have the same length.");
}
if (source is null && Array.IndexOf(states, ControlSlotState.Uncompiled) >= 0)
{
throw new ArgumentException("Uncompiled slots require a compilation source.");
}
if (reusableParent is null && Array.IndexOf(states, ControlSlotState.ReuseParent) >= 0)
{
throw new ArgumentException("ReuseParent slots require a reusable parent snapshot.");
}
_states = states;
_materialized = preMaterialized;
_reusableParent = reusableParent;
_source = source;
// 构造期已物化的 Empty 槽位计入估算基线;Materialize 只累计首访物化的增量
// (设计文档 §4:急切基线 + 增量回填)。
foreach (var snapshot in preMaterialized)
{
if (snapshot is not null)
{
_materializedBytes += snapshot.EstimatedRetainedBytes;
}
}
}
internal int Count => _states.Length;
// 测试观测口:首访物化(ReuseParent + Uncompiled)的槽位计数,不含构造期 Empty 实例。
internal int MaterializedCount => Volatile.Read(ref _materializedCount);
internal long MaterializedBytes => Interlocked.Read(ref _materializedBytes);
internal ControlThemeSnapshot this[int slot]
{
get
{
if ((uint)slot >= (uint)Count)
{
throw new ArgumentOutOfRangeException(nameof(slot));
}
return _materialized[slot] ?? Materialize(slot);
}
}
private ControlThemeSnapshot Materialize(int slot)
{
lock (_gate)
{
if (_materialized[slot] is { } existing)
{
return existing;
}
ControlThemeSnapshot snapshot;
if (_states[slot] == ControlSlotState.ReuseParent)
{
// 物化父槽(父槽未物化则级联物化)并共享同一对象——复用语义本就是同一引用。
snapshot = _reusableParent!.Controls[slot];
}
else
{
snapshot = _source!.Compile(slot);
}
_materialized[slot] = snapshot;
var delta = _states[slot] == ControlSlotState.ReuseParent
? SharedSlotReferenceBytes
: snapshot.EstimatedRetainedBytes;
// 饱和语义与估算器 Add 对齐:溢出回绕(需 ~9.2e18 保留字节,物理不可达)不允许
// 产生负值——负尺寸会让 BoundedLruCache 的容量记账抛异常。
if (Interlocked.Add(ref _materializedBytes, delta) < 0)
{
Interlocked.Exchange(ref _materializedBytes, long.MaxValue);
}
Interlocked.Increment(ref _materializedCount);
return snapshot;
}
}
}
@@ -151,41 +151,13 @@ internal sealed class ThemeCompiler
return Failed(diagnostics);
}
var appearance = ResolveAppearance(input.EffectiveConfig.Algorithms);
var reuseGlobal = CanReuseGlobal(input, appearance);
DesignToken globalBuilder;
TokenValueTable globalValues;
IReadOnlyDictionary<object, object?> globalResources;
IReadOnlyDictionary<PresetPrimaryColor, PaletteInfo> palettes;
if (reuseGlobal)
{
var parent = input.ReusableParent!;
globalValues = parent.GlobalTokenValues;
globalResources = parent.GlobalResources;
palettes = parent.PresetColorPalettes;
globalBuilder = CreateBuilder(globalValues, palettes, input.Registry.GlobalTokens);
}
else
{
globalBuilder = CompileGlobalBuilder(input);
globalValues = TokenValueTable.Freeze(globalBuilder, input.Registry.GlobalTokens);
globalResources = BuildResourceMap(globalBuilder, input.Registry.GlobalTokens, globalValues);
palettes = FreezePresetPalettes(globalBuilder);
}
var controls = CompileControls(
input,
appearance,
globalBuilder,
globalValues,
globalResources,
reuseGlobal,
controlCompilationCache);
var global = CompileGlobal(input);
var controls = CompileControls(input, global, controlCompilationCache);
var fingerprint = ThemeContentFingerprint.Compute(
input.DefinitionRevision,
input.EffectiveConfig,
input.Registry.Revision,
appearance);
global.Appearance);
return new ThemeCompileResult(
new ThemeSnapshot(
@@ -193,13 +165,13 @@ internal sealed class ThemeCompiler
input.Definition,
input.DefinitionRevision,
fingerprint,
appearance,
global.Appearance,
input.Registry.Revision,
input.Registry,
input.EffectiveConfig,
globalValues,
globalResources,
palettes,
global.Values,
global.Resources,
global.Palettes,
controls),
CopyDiagnostics(diagnostics),
null);
@@ -210,6 +182,47 @@ internal sealed class ThemeCompiler
}
}
internal readonly record struct GlobalCompileOutcome(
ThemeAppearance Appearance,
bool ReuseGlobal,
DesignToken Builder,
TokenValueTable Values,
IReadOnlyDictionary<object, object?> Resources,
IReadOnlyDictionary<PresetPrimaryColor, PaletteInfo> Palettes);
private static GlobalCompileOutcome CompileGlobal(ThemeCompileInput input)
{
var appearance = ResolveAppearance(input.EffectiveConfig.Algorithms);
var reuseGlobal = CanReuseGlobal(input, appearance);
DesignToken globalBuilder;
TokenValueTable globalValues;
IReadOnlyDictionary<object, object?> globalResources;
IReadOnlyDictionary<PresetPrimaryColor, PaletteInfo> palettes;
if (reuseGlobal)
{
var parent = input.ReusableParent!;
globalValues = parent.GlobalTokenValues;
globalResources = parent.GlobalResources;
palettes = parent.PresetColorPalettes;
globalBuilder = CreateBuilder(globalValues, palettes, input.Registry.GlobalTokens);
}
else
{
globalBuilder = CompileGlobalBuilder(input);
globalValues = TokenValueTable.Freeze(globalBuilder, input.Registry.GlobalTokens);
globalResources = BuildResourceMap(globalBuilder, input.Registry.GlobalTokens, globalValues);
palettes = FreezePresetPalettes(globalBuilder);
}
return new GlobalCompileOutcome(
appearance,
reuseGlobal,
globalBuilder,
globalValues,
globalResources,
palettes);
}
private static bool CanReuseGlobal(ThemeCompileInput input, ThemeAppearance appearance)
{
var parent = input.ReusableParent;
@@ -234,15 +247,91 @@ internal sealed class ThemeCompiler
input.EffectiveConfig.GlobalTokens);
}
private static IReadOnlyList<ControlThemeSnapshot> CompileControls(
// 惰性编译:每槽位急切解析状态(FindControlConfig / IsControlChanged / CanReuseControl 廉价
// 配置比较,实测 ~9.5ms),只推迟昂贵的 CompileControl(~62ms,87 个控件 1032 个派生公式)。
// 空窗口首帧前对控件快照的查询次数为零——急切预编译换零查询是纯启动损耗。
private static LazyControlSnapshots CompileControls(
ThemeCompileInput input,
ThemeAppearance globalAppearance,
DesignToken globalBuilder,
TokenValueTable globalValues,
IReadOnlyDictionary<object, object?> globalResources,
bool reuseGlobal,
GlobalCompileOutcome global,
ControlCompilationCache? controlCompilationCache)
{
var count = input.Registry.Controls.Count;
var states = new ControlSlotState[count];
var preMaterialized = new ControlThemeSnapshot?[count];
var configs = new NormalizedControlThemeConfig?[count];
var configIndex = 0;
var parentConfigIndex = 0;
var changedControlIndex = 0;
var hasUncompiled = false;
for (var slot = 0; slot < count; slot++)
{
var descriptor = input.Registry.Controls[slot];
var config = FindControlConfig(
input.EffectiveConfig.Controls,
descriptor.Identity,
ref configIndex);
var parentConfig = input.ReusableParent is null
? null
: FindControlConfig(
input.ReusableParent.EffectiveConfig.Controls,
descriptor.Identity,
ref parentConfigIndex);
bool? controlChanged = input.ChangesFromReusableParent is null
? null
: IsControlChanged(
input.ChangesFromReusableParent.ChangedControls,
descriptor.Identity,
ref changedControlIndex);
if (CanReuseControl(
input.ReusableParent,
descriptor,
config,
parentConfig,
global.ReuseGlobal,
controlChanged))
{
// 主题切换复用:切换期零编译;首访时物化父槽并共享同一对象。
states[slot] = ControlSlotState.ReuseParent;
continue;
}
if (config is null && !descriptor.HasOwnTokens)
{
states[slot] = ControlSlotState.Materialized;
preMaterialized[slot] = new ControlThemeSnapshot(
descriptor.Slot,
global.Appearance,
FrozenDictionary<int, object?>.Empty,
FrozenDictionary<object, object?>.Empty,
TokenValueTable.Empty,
FrozenDictionary<object, object?>.Empty);
continue;
}
states[slot] = ControlSlotState.Uncompiled;
configs[slot] = config;
hasUncompiled = true;
}
var source = hasUncompiled
? new LazyControlSnapshotSource(
input,
configs,
global.Appearance,
global.Builder,
global.Values,
global.Resources,
controlCompilationCache)
: null;
return new LazyControlSnapshots(states, preMaterialized, input.ReusableParent, source);
}
// 仅供差分测试的急切参考路径:与惰性 CompileControls 在相同输入下逐位等价(同一 CompileControl、
// 同一缓存键)。生产编译一律走惰性路径;保留本入口用于等价性验证与未来回归对照。
internal IReadOnlyList<ControlThemeSnapshot> CompileControlsEagerForTest(
ThemeCompileInput input,
ControlCompilationCache? controlCompilationCache = null)
{
var global = CompileGlobal(input);
var controls = new ControlThemeSnapshot[input.Registry.Controls.Count];
GlobalCompilationCacheKey? globalCompilationCacheKey = null;
var configIndex = 0;
@@ -272,9 +361,10 @@ internal sealed class ThemeCompiler
descriptor,
config,
parentConfig,
reuseGlobal,
global.ReuseGlobal,
controlChanged))
{
// 惰性父快照在此物化对应槽位,与惰性子快照共享同一对象,差分语义不变。
controls[slot] = input.ReusableParent!.Controls[slot];
continue;
}
@@ -282,7 +372,7 @@ internal sealed class ThemeCompiler
{
controls[slot] = new ControlThemeSnapshot(
descriptor.Slot,
globalAppearance,
global.Appearance,
FrozenDictionary<int, object?>.Empty,
FrozenDictionary<object, object?>.Empty,
TokenValueTable.Empty,
@@ -296,10 +386,10 @@ internal sealed class ThemeCompiler
descriptor,
config,
input.EffectiveConfig.Algorithms,
globalAppearance,
globalBuilder,
globalValues,
globalResources,
global.Appearance,
global.Builder,
global.Values,
global.Resources,
input.Registry.GlobalTokens);
}
@@ -309,7 +399,7 @@ internal sealed class ThemeCompiler
globalCompilationCacheKey ??= GlobalCompilationCacheKey.Create(input),
descriptor,
config,
globalAppearance,
global.Appearance,
CompileCurrentControl);
}
@@ -351,7 +441,9 @@ internal sealed class ThemeCompiler
(config is null ? parentConfig is null : config.Equals(parentConfig));
}
private static ControlThemeSnapshot CompileControl(
// private → internal:LazyControlSnapshotSource 在首访物化时直调本函数(同一编译路径,
// 惰性与急切逐位等价的依据)。仅程序集内与 IVT 测试项目可见。
internal static ControlThemeSnapshot CompileControl(
ControlTokenDescriptor descriptor,
NormalizedControlThemeConfig? config,
IReadOnlyList<ThemeAlgorithmDescriptor> globalAlgorithms,
@@ -30,7 +30,7 @@ internal static class ThemeRetainedBytesEstimator
return retainedBytes;
}
internal static long EstimateSnapshot(ThemeSnapshot snapshot)
internal static long EstimateSnapshotBase(ThemeSnapshot snapshot)
{
var retainedBytes = Add(ObjectOverhead, EstimateValue(snapshot.ThemeId));
retainedBytes = Add(retainedBytes, EstimateValue(snapshot.DefinitionRevision.SourceIdentity));
@@ -39,12 +39,10 @@ internal static class ThemeRetainedBytesEstimator
retainedBytes = Add(retainedBytes, snapshot.GlobalTokenValues.EstimatedRetainedBytes);
retainedBytes = Add(retainedBytes, EstimateResourceMap(snapshot.GlobalResources));
retainedBytes = Add(retainedBytes, EstimatePalettes(snapshot.PresetColorPalettes));
// 控件部分 = 槽位表开销 + 首访物化增量(LazyControlSnapshots.MaterializedBytes),
// 由 ThemeSnapshot.EstimatedRetainedBytes 实时求和;此处不枚举槽位——枚举即全量物化,
// 正是惰性化要避免的形态。
retainedBytes = Add(retainedBytes, ObjectOverhead + snapshot.Controls.Count * ReferenceBytes);
foreach (var control in snapshot.Controls)
{
retainedBytes = Add(retainedBytes, control.EstimatedRetainedBytes);
}
return retainedBytes;
}
@@ -92,7 +90,9 @@ internal static class ThemeRetainedBytesEstimator
};
}
private static long Add(long left, long right)
// internal:ThemeSnapshot.EstimatedRetainedBytes 与 LazyControlSnapshots 的增量累计共用
// 同一饱和语义(long.MaxValue 封顶),负值不允许进入 BoundedLruCache 尺寸记账。
internal static long Add(long left, long right)
{
if (right < 0 || left > long.MaxValue - right)
{
@@ -19,7 +19,7 @@ internal sealed class ThemeSnapshot
TokenValueTable globalTokenValues,
IReadOnlyDictionary<object, object?> globalResources,
IReadOnlyDictionary<PresetPrimaryColor, PaletteInfo> presetColorPalettes,
IReadOnlyList<ControlThemeSnapshot> controlSlots)
LazyControlSnapshots controlSlots)
{
ThemeId = themeId;
Definition = definition;
@@ -33,7 +33,11 @@ internal sealed class ThemeSnapshot
GlobalResources = globalResources;
PresetColorPalettes = presetColorPalettes;
Controls = controlSlots;
EstimatedRetainedBytes = ThemeRetainedBytesEstimator.EstimateSnapshot(this);
// 全局部分 + 槽位表开销在构造期冻结;控件快照字节随首访物化增量累计。
// 已知取舍(设计文档 §4):ThemeSnapshotCache / ControlCompilationCache 在插入时采样本值,
// 物化增长不回灌——估算是启发式容量阀,漂移上界 = 实消费槽位字节(≤187 槽位 × 单槽 KB 级),
// 驱逐只影响性能不影响正确性。
_estimatedRetainedBytesBase = ThemeRetainedBytesEstimator.EstimateSnapshotBase(this);
}
internal string ThemeId { get; }
@@ -47,6 +51,9 @@ internal sealed class ThemeSnapshot
internal TokenValueTable GlobalTokenValues { get; }
internal IReadOnlyDictionary<object, object?> GlobalResources { get; }
internal IReadOnlyDictionary<PresetPrimaryColor, PaletteInfo> PresetColorPalettes { get; }
internal IReadOnlyList<ControlThemeSnapshot> Controls { get; }
internal long EstimatedRetainedBytes { get; }
internal LazyControlSnapshots Controls { get; }
private readonly long _estimatedRetainedBytesBase;
internal long EstimatedRetainedBytes =>
ThemeRetainedBytesEstimator.Add(_estimatedRetainedBytesBase, Controls.MaterializedBytes);
}
@@ -233,7 +233,10 @@ public class DenseThemeCompilerTests
result.Success.ShouldBeTrue();
resourceProjections.ShouldBe(1);
var controlSnapshot = result.Snapshot!.Controls.ShouldHaveSingleItem();
// Controls 为按槽位惰性容器(不实现枚举),单槽位语义用 Count + 索引表达。
var controls = result.Snapshot!.Controls;
controls.Count.ShouldBe(1);
var controlSnapshot = controls[0];
controlSnapshot.EffectiveGlobalTokenDelta.ShouldBeEmpty();
controlSnapshot.EffectiveGlobalResourceDelta.ShouldBeEmpty();
controlSnapshot.ControlTokenValues.ShouldBeSameAs(TokenValueTable.Empty);
@@ -253,7 +256,9 @@ public class DenseThemeCompilerTests
result.Success.ShouldBeTrue();
resourceProjections.ShouldBe(1);
var controlSnapshot = result.Snapshot!.Controls.ShouldHaveSingleItem();
var controls = result.Snapshot!.Controls;
controls.Count.ShouldBe(1);
var controlSnapshot = controls[0];
controlSnapshot.EffectiveGlobalTokenDelta.ShouldBeEmpty();
controlSnapshot.EffectiveGlobalResourceDelta.ShouldBeEmpty();
controlSnapshot.ControlTokenValues.Count.ShouldBe(1);
@@ -348,10 +353,14 @@ public class DenseThemeCompilerTests
reusableParent: parent,
changeSet: changeSet)).Snapshot!;
buttonEvaluations.ShouldBe(2);
inputEvaluations.ShouldBe(1);
child.Controls[button.Slot].ShouldNotBeSameAs(parent.Controls[button.Slot]);
child.Controls[input.Slot].ShouldBeSameAs(parent.Controls[input.Slot]);
// 惰性契约(runtime.md §7 控件槽位编译契约):评估发生在首访而非编译期。
// 首访后 button 被父子两次独立求值(2),input 复用父对象只求值一次(1)——
// "changeSet 只重算变更控件"的不变量保持不变。
buttonEvaluations.ShouldBe(2);
inputEvaluations.ShouldBe(1);
}
[Fact]