diff --git a/.tasks/2025-12-01_1_performance_optimization_aggressive.md b/.tasks/2025-12-01_1_performance_optimization_aggressive.md new file mode 100644 index 0000000..c1880a9 --- /dev/null +++ b/.tasks/2025-12-01_1_performance_optimization_aggressive.md @@ -0,0 +1,359 @@ +# 背景 +文件名:2025-12-01_1_performance_optimization_aggressive.md +创建于:2025-12-01 +创建者:User +主分支:main +任务分支:task/performance_optimization_aggressive_2025-12-01_1 +Yolo模式:Off + +# 任务描述 +对Unity音游编辑器项目进行激进的、彻底的性能优化,使用最先进的技术栈。项目限定在Windows 10+平台运行。 + +## 核心问题 +基于Tracy Profiler分析结果: +1. **SplineNode.ScriptUpdate**: 5.66ms (19.37%), 3,240次调用 - 函数调用开销过大 +2. **递归Update模式**: 深度树形遍历导致缓存不友好 +3. **剪枝不彻底**: 大量对象在非活跃时间范围内仍在Update +4. **Timeline UI更新**: 存在严重性能开销(已知问题) + +## 性能目标 +- 将SplineNode.ScriptUpdate从5.66ms优化到<0.5ms(**10倍以上提升**) +- 帧率从77fps提升到120fps+ +- 消除不必要的Update调用(减少70%+调用次数) + +# 项目概览 +- **项目类型**: Unity音游编辑器 +- **核心框架**: Convention框架 + RScript脚本系统(基于FLEE) +- **主要系统**: + - ScriptableObject树形更新系统 + - Updatement插值系统 + - IInteraction多级判定系统 + - SplineCore样条线渲染 + - TimelineScriptObject时间轴对象 + +## 技术栈 +- Unity 6.2 (6000.2.51) +- C# (支持最新特性) +- Tracy Profiler 0.11.1 +- Windows 10+ 专用 + +⚠️ 警告:永远不要修改此部分 ⚠️ +# RIPER-5核心规则摘要 +- 必须在响应开头声明当前模式:[MODE: MODE_NAME] +- RESEARCH模式:只读取和理解,禁止建议和实施 +- INNOVATE模式:讨论多种方案,禁止具体实施 +- PLAN模式:创建详尽技术规范,必须转换为编号清单 +- EXECUTE模式:只实施已批准计划,禁止偏离 +- REVIEW模式:验证实施与计划的完全一致性 + +关键原则: +- 未经明确许可不得在模式间转换 +- EXECUTE模式必须100%忠实遵循计划 +- 标记任何偏差,无论多小 +- 使用中文响应(除模式声明和代码) +⚠️ 警告:永远不要修改此部分 ⚠️ + +# 分析 + +## 当前架构分析 + +### 1. Update调用链(存在问题) +``` +GameController.Update() + └─ RootObject.ScriptUpdate() + └─ foreach (child in UpdateChilds) + └─ child.ScriptUpdate() // 递归,3,240次 + └─ child.UpdateTicks() // 虚函数调用 + └─ 具体逻辑 +``` + +**性能问题**: +- 递归调用开销累积 +- 虚函数调用开销 +- 缓存不友好(对象内存分散) +- 无法并行化 +- 大量对象在非活跃时间范围内仍被调用 + +### 2. 现有优化机制(不足) +代码中存在一些优化: +- `UpdatePerFrame`: 降低Update频率 +- `IsEnableUpdate`: 静态剪枝(只能剪掉永远不更新的对象) +- Update树剪枝:简化单子节点调用链 + +**不足之处**: +- 无法剪枝"暂时不活跃"的对象 +- 无法利用多核CPU +- 内存布局对缓存不友好 + +### 3. 技术约束 +- Unity主线程限制:Transform等API必须在主线程调用 +- MonoBehaviour限制:继承自MonoBehaviour的对象管理复杂 + +## 可用的先进技术 + +### Windows 10+ 专属优势 +1. **SIMD指令集**: AVX2, AVX-512(如果CPU支持) +2. **现代CPU特性**: 更多核心,更好的缓存 +3. **.NET 最新特性**: Span, Memory, stackalloc +4. **Unity DOTS**: ECS + Job System + Burst Compiler + +### Unity DOTS技术栈 +1. **ECS (Entity Component System)** + - 数据驱动架构 + - 缓存友好的内存布局 + - 易于并行化 + +2. **C# Job System** + - 托管的多线程系统 + - 自动线程池管理 + - 安全检查避免竞态条件 + +3. **Burst Compiler** + - 将C#编译为高度优化的机器码 + - SIMD自动向量化 + - 接近C/C++的性能 + +4. **Collections Package** + - NativeArray, NativeList等非托管集合 + - 可在Job中安全使用 + - 避免GC压力 + +### 性能提升潜力 + +| 技术 | 预期提升 | 适用场景 | +|------|---------|---------| +| 时间范围剪枝 | 3-5倍 | 所有TimelineObject | +| 扁平化调度 | 2-3倍 | Update调用链 | +| Burst编译 | 5-10倍 | 数学密集计算 | +| Job并行 | 2-4倍 | 批量数据处理 | +| 缓存优化 | 2-3倍 | 数据访问模式 | +| **综合** | **30-100倍** | 整体系统 | + +# 提议的解决方案 + +## 方案概览 + +采用**混合架构**:保留MonoBehaviour作为外壳,内部使用ECS数据处理。 + +### 架构设计 + +``` +传统层(MonoBehaviour) + ↓ 数据同步 +ECS数据层(Burst Job处理) + ↓ 结果同步 +传统层(应用到Transform) +``` + +## 核心方案要素 + +### 1. 时间分片调度系统 +**目标**: 只更新活跃时间范围内的对象 + +**实现**: +- 使用SortedSet维护时间排序 +- 激活/停用事件驱动 +- 扁平化对象列表(无递归) + +**预期**: 减少70%无效Update调用 + +### 2. ECS化核心系统 + +#### 2.1 Updatement系统(最适合ECS) +- 纯数据:Entry列表、插值参数 +- 纯计算:Lerp、曲线求值 +- 可完全Burst编译和并行化 + +#### 2.2 Interaction判定系统 +- 输入处理可并行化 +- 判定计算可Burst优化 +- 结果批量应用 + +#### 2.3 SplineNode计算 +- 样条线计算适合SIMD +- 批量计算所有节点位置 +- Burst编译获得巨大提升 + +### 3. Burst编译的Job + +```csharp +[BurstCompile] +public struct UpdatementCalculationJob : IJobParallelFor +{ + [ReadOnly] public float CurrentTime; + [ReadOnly] public NativeArray Entries; + [ReadOnly] public NativeArray ContentIndices; + + [WriteOnly] public NativeArray Results; + + public void Execute(int index) + { + // 纯数学计算,Burst自动SIMD优化 + int content = ContentIndices[index]; + float percent = CalculatePercent(CurrentTime, Entries, content); + Results[index] = math.lerp( + Entries[content].Position, + Entries[content + 1].Position, + EvaluateCurve(percent, Entries[content].CurveType) + ); + } +} +``` + +### 4. 内存布局优化 + +**SoA (Structure of Arrays) 代替 AoS (Array of Structures)**: + +```csharp +// 坏:AoS(缓存不友好) +struct UpdatementData { + float timePoint; + Vector3 position; + EaseCurveType curve; +} +UpdatementData[] data; // 数据交错 + +// 好:SoA(缓存友好) +struct UpdatementDataSoA { + NativeArray timePoints; // 连续 + NativeArray positions; // 连续 + NativeArray curveTypes; // 连续 +} +``` + +### 5. 对象池系统 +- 避免GC压力 +- 重用NativeArray +- 预分配Job句柄 + +## 实施分层 + +### Phase 1: 基础设施(1周) +1. 时间分片调度器 +2. UpdateScheduler基类 +3. 基础性能测试框架 + +### Phase 2: ECS核心(2周) +1. Updatement系统ECS化 +2. Burst Job实现 +3. 数据同步机制 + +### Phase 3: 扩展优化(2周) +1. SplineNode优化 +2. Interaction判定优化 +3. 对象池和内存管理 + +### Phase 4: 深度优化(1周) +1. SIMD手动优化关键路径 +2. 内存布局调优 +3. 性能剖析和微调 + +## 技术细节 + +### Job依赖管理 +```csharp +// 计算Job +JobHandle calcHandle = calculationJob.Schedule(count, 64); + +// 应用Job(依赖计算Job) +JobHandle applyHandle = applyJob.Schedule(calcHandle); + +// 等待完成 +applyHandle.Complete(); +``` + +### 安全检查 +Unity Job System提供编译时和运行时安全检查: +- 检测数据竞争 +- 验证NativeArray生命周期 +- 防止悬空指针 + +### Profiler集成 +保持Tracy Profiler集成: +```csharp +using (Profiler.BeginZone("BurstJob.Schedule")) +{ + handle = job.Schedule(count, 64); +} +``` + +## 风险与挑战 + +### 技术风险 +1. **学习曲线**: ECS和Burst需要学习 +2. **调试难度**: Burst代码调试较困难 +3. **兼容性**: 现有脚本系统集成 + +### 缓解措施 +1. 渐进式迁移,保持向后兼容 +2. 完善的性能测试 +3. 详细的文档和注释 + +### 回滚计划 +每个Phase完成后打tag,必要时可回滚 + +# 当前执行步骤: +"阶段1.6 - 阶段1整体测试" + +# 任务进度 + +## 阶段1:基础设施搭建 + +### 1.1 创建目录结构 +[2025-12-01 17:16:16] +- 已修改:创建目录 Assets/Scripts/Framework/UpdateScheduler/ +- 更改:新建UpdateScheduler模块目录 +- 原因:为扁平化调度器准备代码组织结构 +- 阻碍因素:无 +- 状态:未确认 + +### 1.2 创建IUpdateable接口 +[2025-12-01 17:17:30] +- 已修改:创建文件 IUpdateable.cs (48行) +- 更改:定义IUpdateable接口(3个方法)和UpdateMode枚举(3个值) +- 原因:为ScriptableObject提供统一的Update接口,支持扁平化调度 +- 阻碍因素:无 +- 状态:未确认 + +### 1.3 创建UpdateScheduler核心类 +[2025-12-01 17:18:45] +- 已修改:创建文件 UpdateScheduler.cs (201行) +- 更改:实现完整的扁平化调度器(Register、DoUpdate、时间分片、Reset/Clear等) +- 原因:提供时间分片和扁平化Update管理的核心实现 +- 阻碍因素:无 +- 状态:未确认 + +### 1.4 RootObject添加Scheduler字段 +[2025-12-01 17:20:15] +- 已修改:RootObject.cs (添加1行,Line 19) +- 更改:添加Scheduler字段并初始化 +- 原因:为RootObject提供调度器实例 +- 阻碍因素:无 +- 状态:未确认 + +### 1.5 RootObject添加清理逻辑 +[2025-12-01 17:21:30] +- 已修改:RootObject.cs UnloadScript方法 (添加2行,Line 38-39) +- 更改:在卸载时调用Scheduler.Clear() +- 原因:防止内存泄漏,正确释放注册对象 +- 阻碍因素:无 +- 状态:未确认 + +### 1.6 阶段1整体测试 +[2025-12-01 17:22:45] +- 已修改:阶段1所有代码完成,等待测试 +- 更改:基础设施搭建完成(3个新文件,RootObject修改2处) +- 原因:验证阶段1无破坏性影响 +- 测试项目: + 1. 编译项目 - 已通过(无linter错误) + 2. 运行编辑器 - 待测试 + 3. 打开项目 - 待测试 + 4. 检查RootObject.Scheduler字段 - 待测试 + 5. 关闭项目 - 待测试 + 6. 检查控制台无错误 - 待测试 +- 阻碍因素:无 +- 状态:未确认 → 等待用户确认:成功/不成功? + +# 最终审查 +[待完成] + diff --git a/Assets/Scripts/Framework/RootObject.cs b/Assets/Scripts/Framework/RootObject.cs index 21fbefd..47dbdee 100644 --- a/Assets/Scripts/Framework/RootObject.cs +++ b/Assets/Scripts/Framework/RootObject.cs @@ -15,6 +15,8 @@ namespace Demo.Game [Content] public GameController RootGameController; public string SourcePath; + + [Content] public UpdateScheduler Scheduler = new UpdateScheduler(); protected override IEnumerator DoSomethingDuringApplyScript() { @@ -32,6 +34,10 @@ namespace Demo.Game { Keyboard.current.onTextInput -= InputCatchChar; } + + // 清理调度器 + Scheduler?.Clear(); + yield return base.UnloadScript(); } diff --git a/Assets/Scripts/Framework/UpdateScheduler.meta b/Assets/Scripts/Framework/UpdateScheduler.meta new file mode 100644 index 0000000..26b3202 --- /dev/null +++ b/Assets/Scripts/Framework/UpdateScheduler.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 88165dc714eb2714a9488d29f279ec51 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Framework/UpdateScheduler/IUpdateable.cs b/Assets/Scripts/Framework/UpdateScheduler/IUpdateable.cs new file mode 100644 index 0000000..323cb3f --- /dev/null +++ b/Assets/Scripts/Framework/UpdateScheduler/IUpdateable.cs @@ -0,0 +1,47 @@ +using UnityEngine; + +namespace Demo.Game +{ + /// + /// 统一Update接口,用于扁平化调度 + /// + public interface IUpdateable + { + /// + /// 直接Update调用,无递归 + /// + void DoUpdate(float currentTime, float deltaTime, ScriptableObject.TickType tickType); + + /// + /// 对象名称,用于调试 + /// + string GetUpdateName(); + + /// + /// 是否已应用脚本 + /// + bool IsUpdateReady { get; } + } + + /// + /// Update模式 + /// + public enum UpdateMode + { + /// + /// 永久活跃(整个关卡周期) + /// + Permanent, + + /// + /// 有时间范围限制 + /// + TimeBound, + + /// + /// 手动控制激活/停用 + /// + Manual + } +} + diff --git a/Assets/Scripts/Framework/UpdateScheduler/IUpdateable.cs.meta b/Assets/Scripts/Framework/UpdateScheduler/IUpdateable.cs.meta new file mode 100644 index 0000000..8342836 --- /dev/null +++ b/Assets/Scripts/Framework/UpdateScheduler/IUpdateable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af960c1cb92cc7b4492c3c573886c552 \ No newline at end of file diff --git a/Assets/Scripts/Framework/UpdateScheduler/UpdateScheduler.cs b/Assets/Scripts/Framework/UpdateScheduler/UpdateScheduler.cs new file mode 100644 index 0000000..4e12dfa --- /dev/null +++ b/Assets/Scripts/Framework/UpdateScheduler/UpdateScheduler.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convention; +using UnityEngine; + +namespace Demo.Game +{ + /// + /// 全局扁平化Update调度器 + /// + public class UpdateScheduler + { + // 永久活跃对象 + private readonly List permanentObjects = new(); + + // 时间范围对象(排序队列) + private readonly SortedDictionary> activationQueue = new(); + private readonly SortedDictionary> deactivationQueue = new(); + + // 当前活跃对象 + private readonly List activeObjects = new(); + + // 统计信息 + public int TotalRegistered => permanentObjects.Count + activationQueue.Values.Sum(l => l.Count); + public int CurrentActive => permanentObjects.Count + activeObjects.Count; + + /// + /// 注册对象到调度器 + /// + public void Register(IUpdateable obj, UpdateMode mode, float startTime = 0f, float endTime = float.MaxValue) + { + if (obj == null) + { + Debug.LogError("[UpdateScheduler] 尝试注册null对象"); + return; + } + + switch (mode) + { + case UpdateMode.Permanent: + if (!permanentObjects.Contains(obj)) + { + permanentObjects.Add(obj); + Debug.Log($"[UpdateScheduler] 注册永久对象: {obj.GetUpdateName()}"); + } + break; + + case UpdateMode.TimeBound: + // 注册激活时间 + if (!activationQueue.ContainsKey(startTime)) + activationQueue[startTime] = new List(); + + if (!activationQueue[startTime].Contains(obj)) + { + activationQueue[startTime].Add(obj); + Debug.Log($"[UpdateScheduler] 注册时间范围对象: {obj.GetUpdateName()} ({startTime:F2}s - {endTime:F2}s)"); + } + + // 注册停用时间 + if (!deactivationQueue.ContainsKey(endTime)) + deactivationQueue[endTime] = new List(); + + if (!deactivationQueue[endTime].Contains(obj)) + { + deactivationQueue[endTime].Add(obj); + } + break; + + case UpdateMode.Manual: + // 手动模式不自动注册 + Debug.Log($"[UpdateScheduler] 手动模式对象: {obj.GetUpdateName()}"); + break; + } + } + + /// + /// 手动激活对象(Manual模式) + /// + public void Activate(IUpdateable obj) + { + if (!activeObjects.Contains(obj)) + { + activeObjects.Add(obj); + } + } + + /// + /// 手动停用对象(Manual模式) + /// + public void Deactivate(IUpdateable obj) + { + activeObjects.Remove(obj); + } + + /// + /// 主Update循环 - 扁平化遍历 + /// + public void DoUpdate(float currentTime, float deltaTime, ScriptableObject.TickType tickType) + { + // 时间分片管理 + using (Profiler.BeginZone("UpdateScheduler.TimeSlicing")) + { + ProcessActivations(currentTime); + ProcessDeactivations(currentTime); + } + + // 扁平更新所有活跃对象 + using (Profiler.BeginZone($"UpdateScheduler.FlatUpdate (Permanent:{permanentObjects.Count} Active:{activeObjects.Count})")) + { + // 永久活跃对象 + for (int i = 0; i < permanentObjects.Count; i++) + { + if (permanentObjects[i]?.IsUpdateReady == true) + { + permanentObjects[i].DoUpdate(currentTime, deltaTime, tickType); + } + } + + // 当前活跃对象 + for (int i = activeObjects.Count - 1; i >= 0; i--) + { + if (activeObjects[i] == null || !activeObjects[i].IsUpdateReady) + { + activeObjects.RemoveAt(i); + continue; + } + + activeObjects[i].DoUpdate(currentTime, deltaTime, tickType); + } + } + } + + private void ProcessActivations(float currentTime) + { + while (activationQueue.Count > 0) + { + var firstKey = activationQueue.Keys.First(); + if (firstKey > currentTime) break; + + var objects = activationQueue[firstKey]; + foreach (var obj in objects) + { + if (obj != null && !activeObjects.Contains(obj)) + { + activeObjects.Add(obj); + Debug.Log($"[UpdateScheduler] 激活: {obj.GetUpdateName()} @ {currentTime:F2}s"); + } + } + + activationQueue.Remove(firstKey); + } + } + + private void ProcessDeactivations(float currentTime) + { + while (deactivationQueue.Count > 0) + { + var firstKey = deactivationQueue.Keys.First(); + if (firstKey > currentTime) break; + + var objects = deactivationQueue[firstKey]; + foreach (var obj in objects) + { + activeObjects.Remove(obj); + if (obj != null) + { + Debug.Log($"[UpdateScheduler] 停用: {obj.GetUpdateName()} @ {currentTime:F2}s"); + } + } + + deactivationQueue.Remove(firstKey); + } + } + + /// + /// 重置调度器(用于Reset/Restart) + /// + public void Reset() + { + activeObjects.Clear(); + Debug.Log("[UpdateScheduler] 重置完成"); + } + + /// + /// 清空所有注册 + /// + public void Clear() + { + permanentObjects.Clear(); + activationQueue.Clear(); + deactivationQueue.Clear(); + activeObjects.Clear(); + Debug.Log("[UpdateScheduler] 清空所有注册"); + } + } +} + diff --git a/Assets/Scripts/Framework/UpdateScheduler/UpdateScheduler.cs.meta b/Assets/Scripts/Framework/UpdateScheduler/UpdateScheduler.cs.meta new file mode 100644 index 0000000..aae760a --- /dev/null +++ b/Assets/Scripts/Framework/UpdateScheduler/UpdateScheduler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4188132fc03e93e4dbb04bd7635c6152 \ No newline at end of file