增加ui异步取消令牌,确保快速打开的稳定性
This commit is contained in:
parent
0a5ef9135c
commit
1dcf309b78
@ -81,10 +81,6 @@ namespace AlicizaX.UI.Runtime
|
|||||||
return TryReflectAndRegisterInternal(uiType, out info);
|
return TryReflectAndRegisterInternal(uiType, out info);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal method to reflect and register UI type without logging.
|
|
||||||
/// Used by both runtime fallback and pre-registration.
|
|
||||||
/// </summary>
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private static bool TryReflectAndRegisterInternal(Type uiType, out UIMetaInfo info)
|
private static bool TryReflectAndRegisterInternal(Type uiType, out UIMetaInfo info)
|
||||||
{
|
{
|
||||||
@ -93,7 +89,6 @@ namespace AlicizaX.UI.Runtime
|
|||||||
Type baseType = uiType;
|
Type baseType = uiType;
|
||||||
Type? holderType = null;
|
Type? holderType = null;
|
||||||
|
|
||||||
// Get holder type from generic arguments
|
|
||||||
var genericArgs = baseType.GetGenericArguments();
|
var genericArgs = baseType.GetGenericArguments();
|
||||||
if (genericArgs.Length > 0)
|
if (genericArgs.Length > 0)
|
||||||
{
|
{
|
||||||
@ -105,7 +100,6 @@ namespace AlicizaX.UI.Runtime
|
|||||||
int cacheTime = 0;
|
int cacheTime = 0;
|
||||||
bool needUpdate = false;
|
bool needUpdate = false;
|
||||||
|
|
||||||
// Read attributes
|
|
||||||
var windowAttribute = CustomAttributeData.GetCustomAttributes(uiType)
|
var windowAttribute = CustomAttributeData.GetCustomAttributes(uiType)
|
||||||
.FirstOrDefault(a => a.AttributeType.Name == nameof(WindowAttribute));
|
.FirstOrDefault(a => a.AttributeType.Name == nameof(WindowAttribute));
|
||||||
var uiUpdateAttribute = CustomAttributeData.GetCustomAttributes(uiType)
|
var uiUpdateAttribute = CustomAttributeData.GetCustomAttributes(uiType)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
using AlicizaX;
|
using AlicizaX;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
@ -13,6 +14,9 @@ namespace AlicizaX.UI.Runtime
|
|||||||
public readonly Type UILogicType;
|
public readonly Type UILogicType;
|
||||||
public bool InCache = false;
|
public bool InCache = false;
|
||||||
|
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
public CancellationToken CancellationToken => _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||||
|
|
||||||
public UIState State
|
public UIState State
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -31,9 +35,17 @@ namespace AlicizaX.UI.Runtime
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
View = (UIBase)InstanceFactory.CreateInstanceOptimized(UILogicType);
|
View = (UIBase)InstanceFactory.CreateInstanceOptimized(UILogicType);
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CancelAsyncOperations()
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
_cancellationTokenSource?.Dispose();
|
||||||
|
_cancellationTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposeAsync().Forget();
|
DisposeAsync().Forget();
|
||||||
@ -41,6 +53,8 @@ namespace AlicizaX.UI.Runtime
|
|||||||
|
|
||||||
private async UniTask DisposeAsync()
|
private async UniTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
CancelAsyncOperations();
|
||||||
|
|
||||||
if (State != UIState.Uninitialized && State != UIState.Destroying)
|
if (State != UIState.Uninitialized && State != UIState.Destroying)
|
||||||
{
|
{
|
||||||
await View.InternalDestroy();
|
await View.InternalDestroy();
|
||||||
|
|||||||
@ -15,7 +15,6 @@ namespace AlicizaX.UI.Runtime
|
|||||||
public static UIMetadataObject Create(UIMetadata target, RuntimeTypeHandle handle)
|
public static UIMetadataObject Create(UIMetadata target, RuntimeTypeHandle handle)
|
||||||
{
|
{
|
||||||
UIMetadataObject obj = MemoryPool.Acquire<UIMetadataObject>();
|
UIMetadataObject obj = MemoryPool.Acquire<UIMetadataObject>();
|
||||||
// Use type handle hash code as name to avoid string allocation
|
|
||||||
obj.Initialize(handle.GetHashCode().ToString(), target);
|
obj.Initialize(handle.GetHashCode().ToString(), target);
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,19 @@ namespace AlicizaX.UI.Runtime
|
|||||||
{
|
{
|
||||||
internal sealed partial class UIModule
|
internal sealed partial class UIModule
|
||||||
{
|
{
|
||||||
private readonly Dictionary<RuntimeTypeHandle, (UIMetadata, int)> m_CacheWindow = new();
|
private readonly struct CacheEntry
|
||||||
|
{
|
||||||
|
public readonly UIMetadata Metadata;
|
||||||
|
public readonly int TimerId;
|
||||||
|
|
||||||
|
public CacheEntry(UIMetadata metadata, int timerId)
|
||||||
|
{
|
||||||
|
Metadata = metadata;
|
||||||
|
TimerId = timerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<RuntimeTypeHandle, CacheEntry> m_CacheWindow = new();
|
||||||
|
|
||||||
private void CacheWindow(UIMetadata uiMetadata, bool force)
|
private void CacheWindow(UIMetadata uiMetadata, bool force)
|
||||||
{
|
{
|
||||||
@ -43,7 +55,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
}
|
}
|
||||||
|
|
||||||
uiMetadata.InCache = true;
|
uiMetadata.InCache = true;
|
||||||
m_CacheWindow.Add(uiMetadata.MetaInfo.RuntimeTypeHandle, (uiMetadata, timerId));
|
m_CacheWindow.Add(uiMetadata.MetaInfo.RuntimeTypeHandle, new CacheEntry(uiMetadata, timerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTimerDisposeWindow(object[] args)
|
private void OnTimerDisposeWindow(object[] args)
|
||||||
@ -60,13 +72,13 @@ namespace AlicizaX.UI.Runtime
|
|||||||
|
|
||||||
private void RemoveFromCache(RuntimeTypeHandle typeHandle)
|
private void RemoveFromCache(RuntimeTypeHandle typeHandle)
|
||||||
{
|
{
|
||||||
if (m_CacheWindow.TryGetValue(typeHandle, out var result))
|
if (m_CacheWindow.TryGetValue(typeHandle, out var entry))
|
||||||
{
|
{
|
||||||
m_CacheWindow.Remove(typeHandle);
|
m_CacheWindow.Remove(typeHandle);
|
||||||
result.Item1.InCache = false;
|
entry.Metadata.InCache = false;
|
||||||
if (result.Item2 > 0)
|
if (entry.TimerId > 0)
|
||||||
{
|
{
|
||||||
_timerModule.RemoveTimer(result.Item2);
|
_timerModule.RemoveTimer(entry.TimerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
using AlicizaX;
|
using AlicizaX;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
CreateMetaUI(metaInfo);
|
CreateMetaUI(metaInfo);
|
||||||
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
|
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
|
||||||
FinalizeShow(metaInfo, userDatas);
|
FinalizeShow(metaInfo, userDatas);
|
||||||
UpdateVisualState(metaInfo).Forget();
|
await UpdateVisualState(metaInfo, metaInfo.CancellationToken);
|
||||||
return metaInfo.View;
|
return metaInfo.View;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
meta.CancelAsyncOperations();
|
||||||
await meta.View.InternalClose();
|
await meta.View.InternalClose();
|
||||||
Pop(meta);
|
Pop(meta);
|
||||||
SortWindowVisible(meta.MetaInfo.UILayer);
|
SortWindowVisible(meta.MetaInfo.UILayer);
|
||||||
@ -167,16 +169,16 @@ namespace AlicizaX.UI.Runtime
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private async UniTask UpdateVisualState(UIMetadata meta)
|
private async UniTask UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
SortWindowVisible(meta.MetaInfo.UILayer);
|
SortWindowVisible(meta.MetaInfo.UILayer);
|
||||||
SortWindowDepth(meta.MetaInfo.UILayer);
|
SortWindowDepth(meta.MetaInfo.UILayer);
|
||||||
if (meta.State == UIState.Loaded)
|
if (meta.State == UIState.Loaded)
|
||||||
{
|
{
|
||||||
await meta.View.InternalInitlized();
|
await meta.View.InternalInitlized(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await meta.View.InternalOpen();
|
await meta.View.InternalOpen(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SortWindowVisible(int layer)
|
private void SortWindowVisible(int layer)
|
||||||
|
|||||||
@ -1,275 +0,0 @@
|
|||||||
# UI Module Optimization - Complete Summary
|
|
||||||
|
|
||||||
## Date: 2025-12-24
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
Successfully implemented comprehensive optimizations across 3 phases for the Aliciza X Unity UI module, resulting in significant performance improvements, reduced memory allocations, and better code quality.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overall Performance Improvements
|
|
||||||
|
|
||||||
### Memory Allocations Eliminated
|
|
||||||
|
|
||||||
| Operation | Before | After | Savings |
|
|
||||||
|-----------|--------|-------|---------|
|
|
||||||
| **First UI Open** | 5-10ms reflection | 0ms | **5-10ms** |
|
|
||||||
| **UI Open (GC)** | ~150 bytes | 0 bytes | **150 bytes** |
|
|
||||||
| **Widget Update (per frame)** | 56 bytes | 0 bytes | **56 bytes** |
|
|
||||||
| **Widget Creation** | 40 bytes | 0 bytes | **40 bytes** |
|
|
||||||
| **Window Switch** | O(n) + 1-2ms | O(1) + 0ms | **1-2ms** |
|
|
||||||
|
|
||||||
### Total Impact Per Session
|
|
||||||
- **Startup:** One-time 10-30ms cost for pre-registration (acceptable trade-off)
|
|
||||||
- **Per UI Open:** ~200 bytes GC eliminated
|
|
||||||
- **Per Frame (with widgets):** ~100 bytes GC eliminated
|
|
||||||
- **Window Operations:** 50-70% faster
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: Critical Optimizations ✅
|
|
||||||
|
|
||||||
### 1. Pre-Register All UI Types at Startup
|
|
||||||
- **Problem:** 5-10ms reflection overhead on first UI open
|
|
||||||
- **Solution:** Automatic pre-registration at startup using `[RuntimeInitializeOnLoadMethod]`
|
|
||||||
- **Impact:** Eliminate all runtime reflection overhead
|
|
||||||
- **Files:** `UIMetaRegistry.cs`, `UIResRegistry.cs`
|
|
||||||
|
|
||||||
### 2. Replace List.IndexOf with Index Tracking
|
|
||||||
- **Problem:** O(n) linear search in `MoveToTop()`
|
|
||||||
- **Solution:** Added `IndexMap` dictionary for O(1) lookups
|
|
||||||
- **Impact:** 0.5-2ms faster window switching
|
|
||||||
- **Files:** `UIModule.Open.cs`
|
|
||||||
|
|
||||||
### 3. Fix Async State Machine Allocations
|
|
||||||
- **Problem:** Unnecessary async state machine allocations
|
|
||||||
- **Solution:** Return `UniTask.CompletedTask` instead of `await`
|
|
||||||
- **Impact:** Eliminate ~150 bytes per UI open
|
|
||||||
- **Files:** `UIBase.cs`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2: High Priority Optimizations ✅
|
|
||||||
|
|
||||||
### 4. Widget Dictionary Enumeration
|
|
||||||
- **Problem:** 40-56 bytes allocation per frame from dictionary enumeration
|
|
||||||
- **Solution:** Cache updateable widgets in list, use struct enumerator
|
|
||||||
- **Impact:** Zero allocation per frame
|
|
||||||
- **Files:** `UIBase.Widget.cs`
|
|
||||||
|
|
||||||
### 5. UIMetadataFactory String Allocations
|
|
||||||
- **Problem:** String allocation on every widget creation
|
|
||||||
- **Solution:** Use `RuntimeTypeHandle` as key instead of string
|
|
||||||
- **Impact:** Eliminate 40+ bytes per widget
|
|
||||||
- **Files:** `UIMetadataFactory.cs`, `UIMetadataObject.cs`
|
|
||||||
|
|
||||||
### 6. SortWindowVisible Early Exit
|
|
||||||
- **Problem:** No early exit when fullscreen found
|
|
||||||
- **Solution:** Two-phase algorithm with early exit
|
|
||||||
- **Impact:** Faster visibility sorting
|
|
||||||
- **Files:** `UIModule.Open.cs`
|
|
||||||
|
|
||||||
### 7. Cache Timer Management
|
|
||||||
- **Problem:** Typos and poor error handling
|
|
||||||
- **Solution:** Fixed typos, added validation, better error messages
|
|
||||||
- **Impact:** Better code quality and reliability
|
|
||||||
- **Files:** `UIModule.Cache.cs`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3: Medium Priority Optimizations ✅
|
|
||||||
|
|
||||||
### 8. UI State Machine Validation
|
|
||||||
- **Problem:** No validation of state transitions
|
|
||||||
- **Solution:** Created `UIStateMachine` with full validation
|
|
||||||
- **Impact:** Better debugging, prevent crashes
|
|
||||||
- **Files:** `UIStateMachine.cs` (new), `UIBase.cs`, `UIMetadata.cs`
|
|
||||||
|
|
||||||
### 9. Depth Sorting Optimization
|
|
||||||
- **Problem:** Recalculate depth for all windows
|
|
||||||
- **Solution:** Only update changed windows, check before setting
|
|
||||||
- **Impact:** Fewer Canvas updates
|
|
||||||
- **Files:** `UIModule.Open.cs`
|
|
||||||
|
|
||||||
### 10. Remove Implicit Bool Operator
|
|
||||||
- **Problem:** Confusing implicit operator overload
|
|
||||||
- **Solution:** Explicit `IsValid()` method
|
|
||||||
- **Impact:** Clearer code, safer
|
|
||||||
- **Files:** `UIHolderObjectBase.cs`, `UIModule.Open.cs`
|
|
||||||
|
|
||||||
### 11. State Check Improvements
|
|
||||||
- **Problem:** Redundant state checks
|
|
||||||
- **Solution:** Early returns with validation
|
|
||||||
- **Impact:** Prevent duplicate calls
|
|
||||||
- **Files:** `UIBase.cs`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Modified Summary
|
|
||||||
|
|
||||||
### New Files Created (1)
|
|
||||||
- `UIStateMachine.cs` - State transition validation
|
|
||||||
|
|
||||||
### Files Modified (11)
|
|
||||||
1. `UIMetaRegistry.cs` - Pre-registration
|
|
||||||
2. `UIResRegistry.cs` - Pre-registration
|
|
||||||
3. `UIModule.Open.cs` - Index tracking, visibility/depth optimization
|
|
||||||
4. `UIBase.cs` - Async fixes, state validation
|
|
||||||
5. `UIBase.Widget.cs` - Widget enumeration optimization
|
|
||||||
6. `UIMetadataFactory.cs` - String allocation fix
|
|
||||||
7. `UIMetadataObject.cs` - RuntimeTypeHandle support
|
|
||||||
8. `UIModule.Cache.cs` - Timer management fixes
|
|
||||||
9. `UIMetadata.cs` - State validation
|
|
||||||
10. `UIHolderObjectBase.cs` - Remove implicit operator
|
|
||||||
11. `UIModule.Initlize.cs` - (if needed for initialization)
|
|
||||||
|
|
||||||
### Documentation Created (3)
|
|
||||||
- `PHASE1_OPTIMIZATIONS.md`
|
|
||||||
- `PHASE2_OPTIMIZATIONS.md`
|
|
||||||
- `PHASE3_OPTIMIZATIONS.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Optimization Categories
|
|
||||||
|
|
||||||
### Memory Optimizations
|
|
||||||
- ✅ Eliminate reflection allocations (Phase 1)
|
|
||||||
- ✅ Eliminate async state machine allocations (Phase 1)
|
|
||||||
- ✅ Eliminate widget enumeration allocations (Phase 2)
|
|
||||||
- ✅ Eliminate string allocations (Phase 2)
|
|
||||||
|
|
||||||
### Performance Optimizations
|
|
||||||
- ✅ O(n) → O(1) window lookup (Phase 1)
|
|
||||||
- ✅ Early exit in visibility sorting (Phase 2)
|
|
||||||
- ✅ Partial depth updates (Phase 3)
|
|
||||||
- ✅ Avoid unnecessary Canvas updates (Phase 3)
|
|
||||||
|
|
||||||
### Code Quality Improvements
|
|
||||||
- ✅ State machine validation (Phase 3)
|
|
||||||
- ✅ Fixed typos (Phase 2)
|
|
||||||
- ✅ Better error handling (Phase 2, 3)
|
|
||||||
- ✅ Clearer intent (Phase 3)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
```csharp
|
|
||||||
// State machine validation
|
|
||||||
TestInvalidStateTransition()
|
|
||||||
TestValidStateTransitions()
|
|
||||||
|
|
||||||
// Performance tests
|
|
||||||
TestUIOpenPerformance()
|
|
||||||
TestWindowSwitchPerformance()
|
|
||||||
TestWidgetUpdateAllocations()
|
|
||||||
|
|
||||||
// Memory tests
|
|
||||||
TestUIOpenAllocations()
|
|
||||||
TestWidgetCreationAllocations()
|
|
||||||
|
|
||||||
// Functionality tests
|
|
||||||
TestDepthSortingOptimization()
|
|
||||||
TestHolderIsValid()
|
|
||||||
TestDuplicateOpenPrevention()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- Open/close multiple windows
|
|
||||||
- Switch between windows rapidly
|
|
||||||
- Create/destroy many widgets
|
|
||||||
- Test cache expiration
|
|
||||||
- Test state transitions
|
|
||||||
|
|
||||||
### Performance Profiling
|
|
||||||
- Unity Profiler memory tracking
|
|
||||||
- GC.Alloc monitoring
|
|
||||||
- Frame time measurements
|
|
||||||
- Startup time measurement
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backward Compatibility
|
|
||||||
|
|
||||||
✅ **100% Backward Compatible**
|
|
||||||
- No breaking changes to public API
|
|
||||||
- Existing UI code requires no modifications
|
|
||||||
- All optimizations are transparent to users
|
|
||||||
- Fallback to runtime reflection if pre-registration fails
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. **Startup Time:** +10-30ms one-time cost for pre-registration
|
|
||||||
2. **Memory Overhead:** ~32 bytes per layer for IndexMap
|
|
||||||
3. **State Machine:** Small overhead for validation (negligible)
|
|
||||||
4. **Assembly Scanning:** May fail on some platforms (graceful fallback)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations for Usage
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
1. Let pre-registration run at startup (automatic)
|
|
||||||
2. Use async UI loading for smooth experience
|
|
||||||
3. Cache frequently used UI windows
|
|
||||||
4. Use widgets for reusable components
|
|
||||||
5. Monitor state transitions in debug builds
|
|
||||||
|
|
||||||
### Performance Tips
|
|
||||||
1. Minimize fullscreen windows (blocks lower windows)
|
|
||||||
2. Use `[UIUpdate]` attribute only when needed
|
|
||||||
3. Pool frequently created/destroyed UI
|
|
||||||
4. Preload critical UI during loading screens
|
|
||||||
5. Use appropriate cache times
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Enhancement Opportunities
|
|
||||||
|
|
||||||
### Phase 4 (Architectural)
|
|
||||||
1. **UI Preloading System**
|
|
||||||
- Preload critical UI during loading
|
|
||||||
- Batch loading for better performance
|
|
||||||
- Memory budget management
|
|
||||||
|
|
||||||
2. **UI Pooling**
|
|
||||||
- Pool frequently used windows (toasts, tooltips)
|
|
||||||
- Reduce allocation for transient UI
|
|
||||||
- Configurable pool sizes
|
|
||||||
|
|
||||||
3. **Performance Metrics**
|
|
||||||
- Track UI open/close times
|
|
||||||
- Memory usage per UI
|
|
||||||
- Frame time impact
|
|
||||||
- Analytics integration
|
|
||||||
|
|
||||||
4. **Advanced Optimizations**
|
|
||||||
- Separate update loop for visible windows only
|
|
||||||
- Batch Canvas updates
|
|
||||||
- Lazy initialization for widgets
|
|
||||||
- Virtual scrolling for lists
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The UI module optimizations have achieved:
|
|
||||||
|
|
||||||
✅ **30-50% faster UI operations**
|
|
||||||
✅ **50-70% less GC pressure**
|
|
||||||
✅ **Better scalability** with many windows
|
|
||||||
✅ **Improved debugging** with state validation
|
|
||||||
✅ **Higher code quality** with fixes and clarity
|
|
||||||
✅ **100% backward compatible**
|
|
||||||
|
|
||||||
The optimizations are production-ready and will significantly improve user experience, especially on lower-end devices and when many UI windows are active.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
All optimizations maintain the original architecture's elegance while adding performance and reliability improvements. The module is now more robust, faster, and easier to debug.
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: dcb06edbd88c426459ffbcce1ffc484f
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -1,236 +0,0 @@
|
|||||||
# Phase 1 UI Module Optimizations - Implementation Summary
|
|
||||||
|
|
||||||
## Date: 2025-12-24
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Successfully implemented Phase 1 critical optimizations for the UI module, targeting reflection overhead, lookup performance, and memory allocations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 1: Pre-Register All UI Types at Startup
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- Reflection overhead of 5-10ms on first UI open
|
|
||||||
- `CustomAttributeData.GetCustomAttributes()` called at runtime
|
|
||||||
- `GetGenericArguments()` called for every unregistered UI type
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**Files Modified:**
|
|
||||||
- `UIMetaRegistry.cs`
|
|
||||||
- `UIResRegistry.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Added `PreRegisterAllUITypes()` method with `[RuntimeInitializeOnLoadMethod]` attribute
|
|
||||||
2. Scans all assemblies at startup (skips system assemblies)
|
|
||||||
3. Pre-caches all UIBase-derived types and their metadata
|
|
||||||
4. Added `PreRegisterAllUIResources()` for UI holder resource paths
|
|
||||||
5. Split reflection logic into `TryReflectAndRegisterInternal()` for reuse
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Runs automatically at `SubsystemRegistration` phase
|
|
||||||
- Logs registration time and count
|
|
||||||
- Graceful error handling for assembly load failures
|
|
||||||
- One-time execution with `_isPreRegistered` flag
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Eliminate 5-10ms reflection overhead on first UI open
|
|
||||||
- ✅ All UI types cached at startup
|
|
||||||
- ✅ Predictable startup time (one-time cost)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 2: Replace List.IndexOf with Index Tracking
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- `List.IndexOf(meta)` is O(n) linear search
|
|
||||||
- Called in `MoveToTop()` every time a window is re-opened
|
|
||||||
- Performance degrades with more windows in a layer
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIModule.Open.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Added `IndexMap` dictionary to `LayerData` struct
|
|
||||||
2. Updated `Push()` to track index when adding windows
|
|
||||||
3. Updated `Pop()` to maintain indices after removal
|
|
||||||
4. Replaced `IndexOf()` with O(1) dictionary lookup in `MoveToTop()`
|
|
||||||
5. Update indices for shifted elements after removal/move
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
int currentIdx = layer.OrderList.IndexOf(meta); // O(n)
|
|
||||||
|
|
||||||
// After
|
|
||||||
if (!layer.IndexMap.TryGetValue(meta.MetaInfo.RuntimeTypeHandle, out int currentIdx)) // O(1)
|
|
||||||
return;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ O(n) → O(1) lookup performance
|
|
||||||
- ✅ ~0.5-2ms saved per window switch
|
|
||||||
- ✅ Scales better with more windows
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 3: Fix Async State Machine Allocations
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- Virtual async methods allocate state machines even when not overridden
|
|
||||||
- `await UniTask.CompletedTask` allocates 48-64 bytes per call
|
|
||||||
- Called 3 times per UI open (Initialize, Open, Close)
|
|
||||||
- Unnecessary GC pressure
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIBase.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Removed `async` keyword from virtual methods
|
|
||||||
2. Changed return pattern from `await UniTask.CompletedTask` to `return UniTask.CompletedTask`
|
|
||||||
3. Call synchronous methods first, then return completed task
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
protected virtual async UniTask OnInitializeAsync()
|
|
||||||
{
|
|
||||||
await UniTask.CompletedTask; // Allocates state machine
|
|
||||||
OnInitialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
protected virtual UniTask OnInitializeAsync()
|
|
||||||
{
|
|
||||||
OnInitialize();
|
|
||||||
return UniTask.CompletedTask; // No allocation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Eliminate ~100-200 bytes allocation per UI open
|
|
||||||
- ✅ Reduce GC pressure
|
|
||||||
- ✅ Faster execution (no state machine overhead)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Improvements Summary
|
|
||||||
|
|
||||||
| Metric | Before | After | Improvement |
|
|
||||||
|--------|--------|-------|-------------|
|
|
||||||
| First UI Open | 5-10ms reflection | 0ms (pre-cached) | **5-10ms faster** |
|
|
||||||
| Window Switch | O(n) IndexOf | O(1) lookup | **0.5-2ms faster** |
|
|
||||||
| GC per UI Open | ~150 bytes | ~0 bytes | **150 bytes saved** |
|
|
||||||
| Startup Time | 0ms | +10-30ms (one-time) | Acceptable trade-off |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
### 1. Startup Performance Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public void TestUIPreRegistration()
|
|
||||||
{
|
|
||||||
// Verify all UI types are registered at startup
|
|
||||||
var registeredCount = UIMetaRegistry.GetRegisteredCount();
|
|
||||||
Assert.Greater(registeredCount, 0);
|
|
||||||
|
|
||||||
// Verify no reflection warnings in logs
|
|
||||||
LogAssert.NoUnexpectedReceived();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Window Switch Performance Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestWindowSwitchPerformance()
|
|
||||||
{
|
|
||||||
await GameApp.UI.ShowUI<Window1>();
|
|
||||||
await GameApp.UI.ShowUI<Window2>();
|
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
await GameApp.UI.ShowUI<Window1>(); // Re-open (triggers MoveToTop)
|
|
||||||
stopwatch.Stop();
|
|
||||||
|
|
||||||
Assert.Less(stopwatch.ElapsedMilliseconds, 5); // Should be < 5ms
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Memory Allocation Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestUIOpenAllocations()
|
|
||||||
{
|
|
||||||
GC.Collect();
|
|
||||||
var beforeMemory = GC.GetTotalMemory(true);
|
|
||||||
|
|
||||||
await GameApp.UI.ShowUI<TestWindow>();
|
|
||||||
|
|
||||||
var afterMemory = GC.GetTotalMemory(false);
|
|
||||||
var allocated = afterMemory - beforeMemory;
|
|
||||||
|
|
||||||
// Should allocate less than before (no async state machines)
|
|
||||||
Assert.Less(allocated, 1000); // Adjust threshold as needed
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Index Map Consistency Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestIndexMapConsistency()
|
|
||||||
{
|
|
||||||
// Open multiple windows
|
|
||||||
await GameApp.UI.ShowUI<Window1>();
|
|
||||||
await GameApp.UI.ShowUI<Window2>();
|
|
||||||
await GameApp.UI.ShowUI<Window3>();
|
|
||||||
|
|
||||||
// Close middle window
|
|
||||||
GameApp.UI.CloseUI<Window2>();
|
|
||||||
|
|
||||||
// Verify indices are correct
|
|
||||||
// (Internal test - would need access to UIModule internals)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
- [x] Code compiles without errors
|
|
||||||
- [x] No breaking changes to public API
|
|
||||||
- [x] Backward compatible with existing UI code
|
|
||||||
- [ ] Run Unity Editor and verify no errors in console
|
|
||||||
- [ ] Test UI opening/closing in play mode
|
|
||||||
- [ ] Profile memory allocations with Unity Profiler
|
|
||||||
- [ ] Measure startup time increase (should be < 50ms)
|
|
||||||
- [ ] Test with multiple UI windows open simultaneously
|
|
||||||
- [ ] Verify UI switching performance improvement
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. **Startup Time Increase**: Pre-registration adds 10-30ms to startup (acceptable trade-off)
|
|
||||||
2. **Memory Overhead**: IndexMap adds ~32 bytes per layer (negligible)
|
|
||||||
3. **Assembly Scanning**: May fail on some platforms (graceful fallback to runtime reflection)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps (Phase 2)
|
|
||||||
|
|
||||||
1. Optimize widget enumeration allocations
|
|
||||||
2. Fix UIMetadataFactory string allocations
|
|
||||||
3. Optimize SortWindowVisible with early exit
|
|
||||||
4. Add state machine validation
|
|
||||||
5. Implement UI preloading system
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All changes maintain backward compatibility
|
|
||||||
- Existing UI code requires no modifications
|
|
||||||
- Optimizations are transparent to users
|
|
||||||
- Fallback to runtime reflection if pre-registration fails
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e65d5c1ad620ab945a8e79be86b53728
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -1,434 +0,0 @@
|
|||||||
# Phase 2 UI Module Optimizations - Implementation Summary
|
|
||||||
|
|
||||||
## Date: 2025-12-24
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Successfully implemented Phase 2 high-priority optimizations for the UI module, targeting widget enumeration, string allocations, visibility sorting, and cache management.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 1: Widget Dictionary Enumeration Allocations
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- `_children.Values` enumeration allocates 40-56 bytes per call
|
|
||||||
- Called every frame for windows with widgets
|
|
||||||
- `UpdateChildren()` and `ChildVisible()` both enumerate dictionary
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIBase.Widget.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Added `_updateableChildren` list to cache widgets that need updates
|
|
||||||
2. Changed `UpdateChildren()` to iterate cached list instead of dictionary
|
|
||||||
3. Changed `ChildVisible()` to use struct enumerator (`foreach (var kvp in _children)`)
|
|
||||||
4. Updated `AddWidget()` to populate updateable list
|
|
||||||
5. Updated `RemoveWidget()` to maintain updateable list
|
|
||||||
6. Updated `DestroyAllChildren()` to clear both collections
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
private void UpdateChildren()
|
|
||||||
{
|
|
||||||
var values = _children.Values; // Allocates enumerator
|
|
||||||
foreach (var meta in values)
|
|
||||||
{
|
|
||||||
if (meta.View.State == UIState.Opened && meta.MetaInfo.NeedUpdate)
|
|
||||||
{
|
|
||||||
meta.View.InternalUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
private readonly List<UIMetadata> _updateableChildren = new();
|
|
||||||
|
|
||||||
private void UpdateChildren()
|
|
||||||
{
|
|
||||||
// Use cached list - no allocation
|
|
||||||
for (int i = 0; i < _updateableChildren.Count; i++)
|
|
||||||
{
|
|
||||||
var meta = _updateableChildren[i];
|
|
||||||
if (meta.View.State == UIState.Opened)
|
|
||||||
{
|
|
||||||
meta.View.InternalUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Eliminate 40-56 bytes allocation per frame per window with widgets
|
|
||||||
- ✅ Faster iteration (direct list access vs dictionary enumeration)
|
|
||||||
- ✅ Only iterate widgets that actually need updates
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 2: UIMetadataFactory String Allocations
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- `type.FullName` allocates new string on every widget creation
|
|
||||||
- String used as dictionary key in object pool
|
|
||||||
- Unnecessary allocation for frequently created widgets
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**Files Modified:**
|
|
||||||
- `UIMetadataFactory.cs`
|
|
||||||
- `UIMetadataObject.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Replaced string-based pooling with `RuntimeTypeHandle`-based caching
|
|
||||||
2. Added `WidgetMetadataCache` dictionary using `RuntimeTypeHandle` as key
|
|
||||||
3. Changed `GetWidgetMetadata()` to accept `RuntimeTypeHandle` directly
|
|
||||||
4. Added overload to `UIMetadataObject.Create()` accepting `RuntimeTypeHandle`
|
|
||||||
5. Simplified `ReturnToPool()` (widgets are now cached, not pooled)
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
private static UIMetadata GetFromPool(Type type)
|
|
||||||
{
|
|
||||||
string typeHandleKey = type.FullName; // Allocates string
|
|
||||||
UIMetadataObject metadataObj = m_UIMetadataPool.Spawn(typeHandleKey);
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
private static readonly Dictionary<RuntimeTypeHandle, UIMetadataObject> WidgetMetadataCache = new();
|
|
||||||
|
|
||||||
private static UIMetadata GetFromPool(RuntimeTypeHandle handle)
|
|
||||||
{
|
|
||||||
// Use RuntimeTypeHandle directly - no string allocation
|
|
||||||
if (WidgetMetadataCache.TryGetValue(handle, out var metadataObj))
|
|
||||||
{
|
|
||||||
if (metadataObj != null && metadataObj.Target != null)
|
|
||||||
{
|
|
||||||
return (UIMetadata)metadataObj.Target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Eliminate 40+ bytes string allocation per widget creation
|
|
||||||
- ✅ Faster lookup (RuntimeTypeHandle comparison vs string comparison)
|
|
||||||
- ✅ Better memory locality
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 3: SortWindowVisible Early Exit
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- Original logic iterates all windows even after finding fullscreen
|
|
||||||
- Unclear logic with boolean flag
|
|
||||||
- No early exit optimization
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIModule.Open.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Split logic into two phases: find fullscreen, then set visibility
|
|
||||||
2. Added early exit when fullscreen window found
|
|
||||||
3. Clearer logic with explicit index tracking
|
|
||||||
4. Separate fast paths for "no fullscreen" and "has fullscreen" cases
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
private void SortWindowVisible(int layer)
|
|
||||||
{
|
|
||||||
var list = _openUI[layer].OrderList;
|
|
||||||
bool shouldHide = false;
|
|
||||||
|
|
||||||
for (int i = list.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var meta = list[i];
|
|
||||||
meta.View.Visible = !shouldHide;
|
|
||||||
shouldHide |= meta.MetaInfo.FullScreen && meta.State == UIState.Opened;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
private void SortWindowVisible(int layer)
|
|
||||||
{
|
|
||||||
var list = _openUI[layer].OrderList;
|
|
||||||
int count = list.Count;
|
|
||||||
|
|
||||||
// Find topmost fullscreen window (early exit)
|
|
||||||
int fullscreenIdx = -1;
|
|
||||||
for (int i = count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var meta = list[i];
|
|
||||||
if (meta.MetaInfo.FullScreen && meta.State == UIState.Opened)
|
|
||||||
{
|
|
||||||
fullscreenIdx = i;
|
|
||||||
break; // Early exit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set visibility based on fullscreen index
|
|
||||||
if (fullscreenIdx == -1)
|
|
||||||
{
|
|
||||||
// Fast path: no fullscreen, all visible
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
list[i].View.Visible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Hide below fullscreen, show from fullscreen onwards
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
list[i].View.Visible = (i >= fullscreenIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Early exit when fullscreen found (saves iterations)
|
|
||||||
- ✅ Clearer, more maintainable code
|
|
||||||
- ✅ Separate fast paths for common cases
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 4: Cache Timer Management
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- Typo: `tiemrId` instead of `timerId`
|
|
||||||
- Typo: `OnTimerDiposeWindow` instead of `OnTimerDisposeWindow`
|
|
||||||
- No validation if timer creation fails
|
|
||||||
- Poor error messages
|
|
||||||
- No null checks on timer callback args
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIModule.Cache.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Fixed typo: `tiemrId` → `timerId`
|
|
||||||
2. Fixed typo: `OnTimerDiposeWindow` → `OnTimerDisposeWindow`
|
|
||||||
3. Added validation for timer creation failure
|
|
||||||
4. Improved error messages with context
|
|
||||||
5. Added null checks in timer callback
|
|
||||||
6. Used named parameters for clarity
|
|
||||||
7. Better null-conditional operator usage
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
int tiemrId = -1;
|
|
||||||
tiemrId = _timerModule.AddTimer(OnTimerDiposeWindow, uiMetadata.MetaInfo.CacheTime, false, true, uiMetadata);
|
|
||||||
|
|
||||||
private void OnTimerDiposeWindow(object[] args)
|
|
||||||
{
|
|
||||||
UIMetadata meta = args[0] as UIMetadata;
|
|
||||||
meta?.Dispose();
|
|
||||||
RemoveFromCache(meta.MetaInfo.RuntimeTypeHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
int timerId = -1;
|
|
||||||
timerId = _timerModule.AddTimer(
|
|
||||||
OnTimerDisposeWindow,
|
|
||||||
uiMetadata.MetaInfo.CacheTime,
|
|
||||||
oneShot: true,
|
|
||||||
ignoreTimeScale: true,
|
|
||||||
uiMetadata
|
|
||||||
);
|
|
||||||
|
|
||||||
if (timerId <= 0)
|
|
||||||
{
|
|
||||||
Log.Warning($"Failed to create cache timer for {uiMetadata.UILogicType.Name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTimerDisposeWindow(object[] args)
|
|
||||||
{
|
|
||||||
if (args == null || args.Length == 0) return;
|
|
||||||
|
|
||||||
UIMetadata meta = args[0] as UIMetadata;
|
|
||||||
if (meta != null)
|
|
||||||
{
|
|
||||||
meta.Dispose();
|
|
||||||
RemoveFromCache(meta.MetaInfo.RuntimeTypeHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Fixed typos (better code quality)
|
|
||||||
- ✅ Better error detection and logging
|
|
||||||
- ✅ Prevent null reference exceptions
|
|
||||||
- ✅ More maintainable code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Improvements Summary
|
|
||||||
|
|
||||||
| Metric | Before | After | Improvement |
|
|
||||||
|--------|--------|-------|-------------|
|
|
||||||
| Widget Update (per frame) | 56 bytes | 0 bytes | **56 bytes saved** |
|
|
||||||
| Widget Creation | 40 bytes | 0 bytes | **40 bytes saved** |
|
|
||||||
| Visibility Sort | No early exit | Early exit | **Faster** |
|
|
||||||
| Code Quality | Typos present | Fixed | **Better** |
|
|
||||||
|
|
||||||
**Total Per-Frame Savings (per window with widgets):**
|
|
||||||
- **~100 bytes GC allocation eliminated**
|
|
||||||
- **Faster widget updates** (list iteration vs dictionary enumeration)
|
|
||||||
- **Clearer, more maintainable code**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
### 1. Widget Update Performance Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public void TestWidgetUpdateAllocations()
|
|
||||||
{
|
|
||||||
var window = await GameApp.UI.ShowUI<WindowWithWidgets>();
|
|
||||||
|
|
||||||
GC.Collect();
|
|
||||||
var beforeMemory = GC.GetTotalMemory(true);
|
|
||||||
|
|
||||||
// Simulate multiple frames
|
|
||||||
for (int i = 0; i < 100; i++)
|
|
||||||
{
|
|
||||||
window.InternalUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
var afterMemory = GC.GetTotalMemory(false);
|
|
||||||
var allocated = afterMemory - beforeMemory;
|
|
||||||
|
|
||||||
// Should allocate much less than before
|
|
||||||
Assert.Less(allocated, 1000); // Adjust threshold
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Widget Creation Allocation Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestWidgetCreationAllocations()
|
|
||||||
{
|
|
||||||
var window = await GameApp.UI.ShowUI<TestWindow>();
|
|
||||||
|
|
||||||
GC.Collect();
|
|
||||||
var beforeMemory = GC.GetTotalMemory(true);
|
|
||||||
|
|
||||||
// Create multiple widgets
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
await window.CreateWidgetAsync<TestWidget>(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
var afterMemory = GC.GetTotalMemory(false);
|
|
||||||
var allocated = afterMemory - beforeMemory;
|
|
||||||
|
|
||||||
// Should not allocate strings for type names
|
|
||||||
Assert.Less(allocated, 5000); // Adjust based on widget size
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Visibility Sort Performance Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestVisibilitySortPerformance()
|
|
||||||
{
|
|
||||||
// Open multiple windows
|
|
||||||
await GameApp.UI.ShowUI<Window1>();
|
|
||||||
await GameApp.UI.ShowUI<Window2>();
|
|
||||||
await GameApp.UI.ShowUI<FullscreenWindow>();
|
|
||||||
await GameApp.UI.ShowUI<Window4>();
|
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
// Trigger visibility sort
|
|
||||||
await GameApp.UI.ShowUI<Window1>(); // Re-open
|
|
||||||
|
|
||||||
stopwatch.Stop();
|
|
||||||
|
|
||||||
// Should be fast with early exit
|
|
||||||
Assert.Less(stopwatch.ElapsedMilliseconds, 2);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Cache Timer Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestCacheTimerManagement()
|
|
||||||
{
|
|
||||||
// Open window with cache time
|
|
||||||
await GameApp.UI.ShowUI<CachedWindow>();
|
|
||||||
GameApp.UI.CloseUI<CachedWindow>();
|
|
||||||
|
|
||||||
// Verify window is cached
|
|
||||||
// Wait for cache expiration
|
|
||||||
await UniTask.Delay(TimeSpan.FromSeconds(cacheTime + 1));
|
|
||||||
|
|
||||||
// Verify window was disposed
|
|
||||||
LogAssert.NoUnexpectedReceived();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
- [x] Code compiles without errors
|
|
||||||
- [x] No breaking changes to public API
|
|
||||||
- [x] Backward compatible with existing UI code
|
|
||||||
- [x] Fixed all typos
|
|
||||||
- [ ] Run Unity Editor and verify no errors
|
|
||||||
- [ ] Test widget creation and updates
|
|
||||||
- [ ] Profile memory allocations
|
|
||||||
- [ ] Test cache timer expiration
|
|
||||||
- [ ] Verify visibility sorting with fullscreen windows
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Quality Improvements
|
|
||||||
|
|
||||||
1. **Fixed Typos:**
|
|
||||||
- `tiemrId` → `timerId`
|
|
||||||
- `OnTimerDiposeWindow` → `OnTimerDisposeWindow`
|
|
||||||
|
|
||||||
2. **Better Error Handling:**
|
|
||||||
- Null checks in timer callbacks
|
|
||||||
- Validation of timer creation
|
|
||||||
- Improved error messages
|
|
||||||
|
|
||||||
3. **Clearer Logic:**
|
|
||||||
- Explicit early exit in visibility sorting
|
|
||||||
- Named parameters for timer creation
|
|
||||||
- Better comments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. **Widget Update List:** Requires manual maintenance (add/remove)
|
|
||||||
2. **Cache Dictionary:** Uses RuntimeTypeHandle which may have hash collisions (rare)
|
|
||||||
3. **Visibility Sort:** Two-pass algorithm (could be optimized further)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps (Phase 3)
|
|
||||||
|
|
||||||
1. Add state machine validation
|
|
||||||
2. Optimize depth sorting (only update changed windows)
|
|
||||||
3. Implement UI preloading system
|
|
||||||
4. Add UI pooling for frequent windows
|
|
||||||
5. Implement performance metrics
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All optimizations maintain backward compatibility
|
|
||||||
- No changes required to existing UI code
|
|
||||||
- Significant reduction in per-frame allocations
|
|
||||||
- Better code quality and maintainability
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8eecca0a49896024eb62845a03e74f11
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -1,431 +0,0 @@
|
|||||||
# Phase 3 UI Module Optimizations - Implementation Summary
|
|
||||||
|
|
||||||
## Date: 2025-12-24
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Successfully implemented Phase 3 medium-priority optimizations for the UI module, focusing on state validation, depth sorting optimization, and code quality improvements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 1: UI State Machine Validation
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- No validation of state transitions
|
|
||||||
- Invalid state transitions could cause bugs
|
|
||||||
- Difficult to debug lifecycle issues
|
|
||||||
- No clear documentation of valid state flows
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**Files Created:**
|
|
||||||
- `UIStateMachine.cs` (new file)
|
|
||||||
|
|
||||||
**Files Modified:**
|
|
||||||
- `UIBase.cs`
|
|
||||||
- `UIMetadata.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Created `UIStateMachine` class with state transition validation
|
|
||||||
2. Defined valid state transitions in dictionary
|
|
||||||
3. Added `ValidateTransition()` method with logging
|
|
||||||
4. Added `GetStateDescription()` for debugging
|
|
||||||
5. Integrated validation into all lifecycle methods:
|
|
||||||
- `CreateUI()` → CreatedUI
|
|
||||||
- `InternalInitlized()` → Initialized
|
|
||||||
- `InternalOpen()` → Opened
|
|
||||||
- `InternalClose()` → Closed
|
|
||||||
- `InternalDestroy()` → Destroying → Destroyed
|
|
||||||
|
|
||||||
**Valid State Transitions:**
|
|
||||||
```
|
|
||||||
Uninitialized → CreatedUI
|
|
||||||
CreatedUI → Loaded
|
|
||||||
Loaded → Initialized
|
|
||||||
Initialized → Opened
|
|
||||||
Opened → Closed | Destroying
|
|
||||||
Closed → Opened | Destroying
|
|
||||||
Destroying → Destroyed
|
|
||||||
Destroyed → (none)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Code Example:**
|
|
||||||
```csharp
|
|
||||||
internal async UniTask InternalOpen()
|
|
||||||
{
|
|
||||||
if (_state == UIState.Opened)
|
|
||||||
return; // Already open
|
|
||||||
|
|
||||||
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opened))
|
|
||||||
return; // Invalid transition, logged
|
|
||||||
|
|
||||||
_state = UIState.Opened;
|
|
||||||
// ... rest of logic
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Catch invalid state transitions early
|
|
||||||
- ✅ Better error messages with UI name
|
|
||||||
- ✅ Prevent crashes from invalid lifecycle calls
|
|
||||||
- ✅ Easier debugging of UI lifecycle issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 2: Depth Sorting Optimization
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- `SortWindowDepth()` recalculates depth for ALL windows in layer
|
|
||||||
- Called even when only one window changed
|
|
||||||
- Unnecessary Canvas.sortingOrder updates
|
|
||||||
- Performance degrades with more windows
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIModule.Open.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Added `startIndex` parameter to `SortWindowDepth()`
|
|
||||||
2. Only update windows from `startIndex` onwards
|
|
||||||
3. Check if depth changed before setting (avoid Canvas update)
|
|
||||||
4. Integrated with `MoveToTop()` to only update affected windows
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
private void SortWindowDepth(int layer)
|
|
||||||
{
|
|
||||||
var list = _openUI[layer].OrderList;
|
|
||||||
int baseDepth = layer * LAYER_DEEP;
|
|
||||||
|
|
||||||
for (int i = 0; i < list.Count; i++)
|
|
||||||
{
|
|
||||||
list[i].View.Depth = baseDepth + i * WINDOW_DEEP; // Always set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
private void SortWindowDepth(int layer, int startIndex = 0)
|
|
||||||
{
|
|
||||||
var list = _openUI[layer].OrderList;
|
|
||||||
int baseDepth = layer * LAYER_DEEP;
|
|
||||||
|
|
||||||
// Only update from startIndex onwards
|
|
||||||
for (int i = startIndex; i < list.Count; i++)
|
|
||||||
{
|
|
||||||
int newDepth = baseDepth + i * WINDOW_DEEP;
|
|
||||||
|
|
||||||
// Only set if changed
|
|
||||||
if (list[i].View.Depth != newDepth)
|
|
||||||
{
|
|
||||||
list[i].View.Depth = newDepth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveToTop now only updates affected windows
|
|
||||||
private void MoveToTop(UIMetadata meta)
|
|
||||||
{
|
|
||||||
// ... move logic ...
|
|
||||||
|
|
||||||
// Only update depth for affected windows
|
|
||||||
SortWindowDepth(meta.MetaInfo.UILayer, currentIdx);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Reduce unnecessary Canvas.sortingOrder updates
|
|
||||||
- ✅ Faster window switching (only update changed windows)
|
|
||||||
- ✅ Better performance with many windows in a layer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 3: Remove Implicit Bool Operator
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- `UIHolderObjectBase` had confusing implicit bool operator
|
|
||||||
- Overloaded Unity's null check behavior
|
|
||||||
- Could cause unexpected behavior
|
|
||||||
- Not clear intent when used
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIHolderObjectBase.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Removed `implicit operator bool()`
|
|
||||||
2. Added explicit `IsValid()` method
|
|
||||||
3. Updated usage sites to use `IsValid()`
|
|
||||||
4. Clearer intent and safer code
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
public static implicit operator bool(UIHolderObjectBase exists)
|
|
||||||
{
|
|
||||||
if (exists == null) return false;
|
|
||||||
return exists.IsAlive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
if (meta.View?.Holder) // Confusing
|
|
||||||
{
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
public bool IsValid()
|
|
||||||
{
|
|
||||||
return this != null && _isAlive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
if (meta.View?.Holder != null && meta.View.Holder.IsValid()) // Clear
|
|
||||||
{
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Clearer intent in code
|
|
||||||
- ✅ Avoid operator confusion
|
|
||||||
- ✅ Safer null checking
|
|
||||||
- ✅ Better maintainability
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Optimization 4: State Check Improvements
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- Redundant state checks in lifecycle methods
|
|
||||||
- No early returns for already-open windows
|
|
||||||
- Could call lifecycle methods multiple times
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
**File Modified:**
|
|
||||||
- `UIBase.cs`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
1. Added early return in `InternalOpen()` if already open
|
|
||||||
2. Added early return in `InternalClose()` if not open
|
|
||||||
3. Combined with state machine validation
|
|
||||||
4. Better error prevention
|
|
||||||
|
|
||||||
**Code Changes:**
|
|
||||||
```csharp
|
|
||||||
// Before
|
|
||||||
internal async UniTask InternalOpen()
|
|
||||||
{
|
|
||||||
if (_state != UIState.Opened)
|
|
||||||
{
|
|
||||||
_state = UIState.Opened;
|
|
||||||
// ... logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After
|
|
||||||
internal async UniTask InternalOpen()
|
|
||||||
{
|
|
||||||
if (_state == UIState.Opened)
|
|
||||||
return; // Already open, early exit
|
|
||||||
|
|
||||||
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opened))
|
|
||||||
return; // Invalid transition
|
|
||||||
|
|
||||||
_state = UIState.Opened;
|
|
||||||
// ... logic
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- ✅ Prevent duplicate lifecycle calls
|
|
||||||
- ✅ Better error detection
|
|
||||||
- ✅ Clearer logic flow
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Improvements Summary
|
|
||||||
|
|
||||||
| Metric | Before | After | Improvement |
|
|
||||||
|--------|--------|-------|-------------|
|
|
||||||
| **Depth Updates** | All windows | Only changed | **Faster** |
|
|
||||||
| **Canvas Updates** | Always set | Only if changed | **Fewer updates** |
|
|
||||||
| **State Validation** | None | Full validation | **Better debugging** |
|
|
||||||
| **Code Clarity** | Implicit operator | Explicit method | **Clearer** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
### 1. State Machine Validation Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestInvalidStateTransition()
|
|
||||||
{
|
|
||||||
var window = await GameApp.UI.ShowUI<TestWindow>();
|
|
||||||
|
|
||||||
// Try to open already-open window
|
|
||||||
await window.InternalOpen(); // Should log error and return
|
|
||||||
|
|
||||||
// Verify no crash
|
|
||||||
Assert.AreEqual(UIState.Opened, window.State);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task TestValidStateTransitions()
|
|
||||||
{
|
|
||||||
var window = new TestWindow();
|
|
||||||
|
|
||||||
// Uninitialized → CreatedUI
|
|
||||||
window.CreateUI();
|
|
||||||
Assert.AreEqual(UIState.CreatedUI, window.State);
|
|
||||||
|
|
||||||
// CreatedUI → Loaded → Initialized → Opened
|
|
||||||
await window.InternalInitlized();
|
|
||||||
await window.InternalOpen();
|
|
||||||
Assert.AreEqual(UIState.Opened, window.State);
|
|
||||||
|
|
||||||
// Opened → Closed
|
|
||||||
await window.InternalClose();
|
|
||||||
Assert.AreEqual(UIState.Closed, window.State);
|
|
||||||
|
|
||||||
// Closed → Destroying → Destroyed
|
|
||||||
await window.InternalDestroy();
|
|
||||||
Assert.AreEqual(UIState.Destroyed, window.State);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Depth Sorting Optimization Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestDepthSortingOptimization()
|
|
||||||
{
|
|
||||||
// Open multiple windows
|
|
||||||
var w1 = await GameApp.UI.ShowUI<Window1>();
|
|
||||||
var w2 = await GameApp.UI.ShowUI<Window2>();
|
|
||||||
var w3 = await GameApp.UI.ShowUI<Window3>();
|
|
||||||
|
|
||||||
int initialDepth1 = w1.Depth;
|
|
||||||
int initialDepth2 = w2.Depth;
|
|
||||||
int initialDepth3 = w3.Depth;
|
|
||||||
|
|
||||||
// Re-open w1 (should move to top)
|
|
||||||
await GameApp.UI.ShowUI<Window1>();
|
|
||||||
|
|
||||||
// w1 should have new depth, w2 and w3 should be updated
|
|
||||||
Assert.AreNotEqual(initialDepth1, w1.Depth);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. IsValid() Method Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public void TestHolderIsValid()
|
|
||||||
{
|
|
||||||
var holder = CreateTestHolder();
|
|
||||||
|
|
||||||
Assert.IsTrue(holder.IsValid());
|
|
||||||
|
|
||||||
Destroy(holder.gameObject);
|
|
||||||
|
|
||||||
// After destroy, should be invalid
|
|
||||||
Assert.IsFalse(holder.IsValid());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. State Check Test
|
|
||||||
```csharp
|
|
||||||
[Test]
|
|
||||||
public async Task TestDuplicateOpenPrevention()
|
|
||||||
{
|
|
||||||
var window = await GameApp.UI.ShowUI<TestWindow>();
|
|
||||||
|
|
||||||
int openCallCount = 0;
|
|
||||||
window.OnWindowBeforeShowEvent += () => openCallCount++;
|
|
||||||
|
|
||||||
// Try to open again
|
|
||||||
await window.InternalOpen();
|
|
||||||
|
|
||||||
// Should only be called once
|
|
||||||
Assert.AreEqual(1, openCallCount);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
- [x] Code compiles without errors
|
|
||||||
- [x] No breaking changes to public API
|
|
||||||
- [x] State machine validates all transitions
|
|
||||||
- [x] Depth sorting only updates changed windows
|
|
||||||
- [x] Implicit operator removed
|
|
||||||
- [ ] Run Unity Editor and verify no errors
|
|
||||||
- [ ] Test invalid state transitions
|
|
||||||
- [ ] Test depth sorting with multiple windows
|
|
||||||
- [ ] Verify IsValid() works correctly
|
|
||||||
- [ ] Test duplicate lifecycle calls
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Quality Improvements
|
|
||||||
|
|
||||||
1. **State Machine:**
|
|
||||||
- Clear documentation of valid transitions
|
|
||||||
- Better error messages with UI name
|
|
||||||
- Easier debugging
|
|
||||||
|
|
||||||
2. **Depth Sorting:**
|
|
||||||
- Partial updates instead of full recalculation
|
|
||||||
- Avoid unnecessary Canvas updates
|
|
||||||
- Better performance
|
|
||||||
|
|
||||||
3. **Implicit Operator:**
|
|
||||||
- Removed confusing operator overload
|
|
||||||
- Explicit `IsValid()` method
|
|
||||||
- Clearer intent
|
|
||||||
|
|
||||||
4. **State Checks:**
|
|
||||||
- Early returns for invalid states
|
|
||||||
- Combined with validation
|
|
||||||
- Better error prevention
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. **State Machine:** Adds small overhead to lifecycle calls (negligible)
|
|
||||||
2. **Depth Sorting:** Still O(n) but with smaller n
|
|
||||||
3. **IsValid():** Requires explicit call instead of implicit check
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps (Future Enhancements)
|
|
||||||
|
|
||||||
1. Implement UI preloading system
|
|
||||||
2. Add UI pooling for frequent windows
|
|
||||||
3. Implement performance metrics/profiling
|
|
||||||
4. Add UI analytics tracking
|
|
||||||
5. Optimize visibility sorting further
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Combined Results (Phase 1 + 2 + 3)
|
|
||||||
|
|
||||||
| Metric | Original | Optimized | Total Improvement |
|
|
||||||
|--------|----------|-----------|-------------------|
|
|
||||||
| **First UI Open** | 5-10ms | 0ms | **5-10ms faster** |
|
|
||||||
| **Window Switch** | O(n) | O(1) | **0.5-2ms faster** |
|
|
||||||
| **Per UI Open** | ~150 bytes | 0 bytes | **150 bytes saved** |
|
|
||||||
| **Per Frame (widgets)** | ~100 bytes | 0 bytes | **100 bytes saved** |
|
|
||||||
| **Widget Creation** | 40 bytes | 0 bytes | **40 bytes saved** |
|
|
||||||
| **Depth Updates** | All windows | Only changed | **Fewer Canvas updates** |
|
|
||||||
| **State Validation** | None | Full | **Better debugging** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All optimizations maintain backward compatibility
|
|
||||||
- State machine adds safety without performance cost
|
|
||||||
- Depth sorting optimization reduces Canvas updates
|
|
||||||
- Code is clearer and more maintainable
|
|
||||||
- Better error detection and debugging capabilities
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3cc945d19bf80634eb057f3a5c8653ca
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using AlicizaX;
|
using AlicizaX;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -70,7 +71,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
{
|
{
|
||||||
metadata.CreateUI();
|
metadata.CreateUI();
|
||||||
await UIHolderFactory.CreateUIResourceAsync(metadata, parent, this);
|
await UIHolderFactory.CreateUIResourceAsync(metadata, parent, this);
|
||||||
await ProcessWidget(metadata, visible);
|
await ProcessWidget(metadata, visible, metadata.CancellationToken);
|
||||||
return (UIBase)metadata.View;
|
return (UIBase)metadata.View;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
metadata.CreateUI();
|
metadata.CreateUI();
|
||||||
UIBase widget = (UIBase)metadata.View;
|
UIBase widget = (UIBase)metadata.View;
|
||||||
widget.BindUIHolder(holder, this);
|
widget.BindUIHolder(holder, this);
|
||||||
await ProcessWidget(metadata, true);
|
await ProcessWidget(metadata, true, metadata.CancellationToken);
|
||||||
return (T)widget;
|
return (T)widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,14 +143,14 @@ namespace AlicizaX.UI.Runtime
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
private async UniTask ProcessWidget(UIMetadata meta, bool visible)
|
private async UniTask ProcessWidget(UIMetadata meta, bool visible, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!AddWidget(meta)) return;
|
if (!AddWidget(meta)) return;
|
||||||
await meta.View.InternalInitlized();
|
await meta.View.InternalInitlized(cancellationToken);
|
||||||
meta.View.Visible = visible;
|
meta.View.Visible = visible;
|
||||||
if (meta.View.Visible)
|
if (meta.View.Visible)
|
||||||
{
|
{
|
||||||
await meta.View.InternalOpen();
|
await meta.View.InternalOpen(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +177,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
{
|
{
|
||||||
if (_children.Remove(widget, out var meta))
|
if (_children.Remove(widget, out var meta))
|
||||||
{
|
{
|
||||||
|
meta.CancelAsyncOperations();
|
||||||
await widget.InternalClose();
|
await widget.InternalClose();
|
||||||
|
|
||||||
// Remove from updateable list if present
|
// Remove from updateable list if present
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using AlicizaX;
|
using AlicizaX;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -68,7 +69,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 如果重写当前方法 则同步OnInitialize不会调用
|
/// 如果重写当前方法 则同步OnInitialize不会调用
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual UniTask OnInitializeAsync()
|
protected virtual UniTask OnInitializeAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
OnInitialize();
|
OnInitialize();
|
||||||
return UniTask.CompletedTask;
|
return UniTask.CompletedTask;
|
||||||
@ -77,7 +78,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 如果重写当前方法 则同步OnOpen不会调用
|
/// 如果重写当前方法 则同步OnOpen不会调用
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual UniTask OnOpenAsync()
|
protected virtual UniTask OnOpenAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
OnOpen();
|
OnOpen();
|
||||||
return UniTask.CompletedTask;
|
return UniTask.CompletedTask;
|
||||||
@ -86,7 +87,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 如果重写当前方法 则同步OnClose不会调用
|
/// 如果重写当前方法 则同步OnClose不会调用
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual UniTask OnCloseAsync()
|
protected virtual UniTask OnCloseAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
OnClose();
|
OnClose();
|
||||||
return UniTask.CompletedTask;
|
return UniTask.CompletedTask;
|
||||||
@ -203,18 +204,18 @@ namespace AlicizaX.UI.Runtime
|
|||||||
|
|
||||||
internal abstract void BindUIHolder(UIHolderObjectBase holder, UIBase owner);
|
internal abstract void BindUIHolder(UIHolderObjectBase holder, UIBase owner);
|
||||||
|
|
||||||
internal async UniTask InternalInitlized()
|
internal async UniTask InternalInitlized(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized))
|
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_state = UIState.Initialized;
|
_state = UIState.Initialized;
|
||||||
Holder.OnWindowInitEvent?.Invoke();
|
Holder.OnWindowInitEvent?.Invoke();
|
||||||
await OnInitializeAsync();
|
await OnInitializeAsync(cancellationToken);
|
||||||
OnRegisterEvent(EventListenerProxy);
|
OnRegisterEvent(EventListenerProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async UniTask InternalOpen()
|
internal async UniTask InternalOpen(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_state == UIState.Opened)
|
if (_state == UIState.Opened)
|
||||||
return; // Already open
|
return; // Already open
|
||||||
@ -225,11 +226,11 @@ namespace AlicizaX.UI.Runtime
|
|||||||
_state = UIState.Opened;
|
_state = UIState.Opened;
|
||||||
Visible = true;
|
Visible = true;
|
||||||
Holder.OnWindowBeforeShowEvent?.Invoke();
|
Holder.OnWindowBeforeShowEvent?.Invoke();
|
||||||
await OnOpenAsync();
|
await OnOpenAsync(cancellationToken);
|
||||||
Holder.OnWindowAfterShowEvent?.Invoke();
|
Holder.OnWindowAfterShowEvent?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async UniTask InternalClose()
|
internal async UniTask InternalClose(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_state != UIState.Opened)
|
if (_state != UIState.Opened)
|
||||||
return; // Not open, nothing to close
|
return; // Not open, nothing to close
|
||||||
@ -238,7 +239,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Holder.OnWindowBeforeClosedEvent?.Invoke();
|
Holder.OnWindowBeforeClosedEvent?.Invoke();
|
||||||
await OnCloseAsync();
|
await OnCloseAsync(cancellationToken);
|
||||||
_state = UIState.Closed;
|
_state = UIState.Closed;
|
||||||
Visible = false;
|
Visible = false;
|
||||||
Holder.OnWindowAfterClosedEvent?.Invoke();
|
Holder.OnWindowAfterClosedEvent?.Invoke();
|
||||||
|
|||||||
@ -3,10 +3,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace AlicizaX.UI.Runtime
|
namespace AlicizaX.UI.Runtime
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// UI State Machine - Validates state transitions for UI lifecycle.
|
|
||||||
/// Helps catch bugs early and ensures proper UI lifecycle management.
|
|
||||||
/// </summary>
|
|
||||||
internal static class UIStateMachine
|
internal static class UIStateMachine
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<UIState, HashSet<UIState>> _validTransitions = new()
|
private static readonly Dictionary<UIState, HashSet<UIState>> _validTransitions = new()
|
||||||
@ -21,24 +18,11 @@ namespace AlicizaX.UI.Runtime
|
|||||||
[UIState.Destroyed] = new() { }
|
[UIState.Destroyed] = new() { }
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if a state transition is valid.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="from">Current state</param>
|
|
||||||
/// <param name="to">Target state</param>
|
|
||||||
/// <returns>True if transition is valid</returns>
|
|
||||||
public static bool IsValidTransition(UIState from, UIState to)
|
public static bool IsValidTransition(UIState from, UIState to)
|
||||||
{
|
{
|
||||||
return _validTransitions.TryGetValue(from, out var validStates) && validStates.Contains(to);
|
return _validTransitions.TryGetValue(from, out var validStates) && validStates.Contains(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a state transition and logs error if invalid.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uiName">Name of the UI for logging</param>
|
|
||||||
/// <param name="from">Current state</param>
|
|
||||||
/// <param name="to">Target state</param>
|
|
||||||
/// <returns>True if transition is valid</returns>
|
|
||||||
public static bool ValidateTransition(string uiName, UIState from, UIState to)
|
public static bool ValidateTransition(string uiName, UIState from, UIState to)
|
||||||
{
|
{
|
||||||
if (IsValidTransition(from, to))
|
if (IsValidTransition(from, to))
|
||||||
@ -48,11 +32,7 @@ namespace AlicizaX.UI.Runtime
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all valid next states from the current state.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="currentState">Current state</param>
|
|
||||||
/// <returns>Set of valid next states</returns>
|
|
||||||
public static HashSet<UIState> GetValidNextStates(UIState currentState)
|
public static HashSet<UIState> GetValidNextStates(UIState currentState)
|
||||||
{
|
{
|
||||||
return _validTransitions.TryGetValue(currentState, out var states)
|
return _validTransitions.TryGetValue(currentState, out var states)
|
||||||
@ -60,9 +40,6 @@ namespace AlicizaX.UI.Runtime
|
|||||||
: new HashSet<UIState>();
|
: new HashSet<UIState>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a human-readable description of the state.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetStateDescription(UIState state)
|
public static string GetStateDescription(UIState state)
|
||||||
{
|
{
|
||||||
return state switch
|
return state switch
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user