增加ui异步取消令牌,确保快速打开的稳定性
This commit is contained in:
parent
0a5ef9135c
commit
1dcf309b78
@ -81,10 +81,6 @@ namespace AlicizaX.UI.Runtime
|
||||
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)]
|
||||
private static bool TryReflectAndRegisterInternal(Type uiType, out UIMetaInfo info)
|
||||
{
|
||||
@ -93,7 +89,6 @@ namespace AlicizaX.UI.Runtime
|
||||
Type baseType = uiType;
|
||||
Type? holderType = null;
|
||||
|
||||
// Get holder type from generic arguments
|
||||
var genericArgs = baseType.GetGenericArguments();
|
||||
if (genericArgs.Length > 0)
|
||||
{
|
||||
@ -105,7 +100,6 @@ namespace AlicizaX.UI.Runtime
|
||||
int cacheTime = 0;
|
||||
bool needUpdate = false;
|
||||
|
||||
// Read attributes
|
||||
var windowAttribute = CustomAttributeData.GetCustomAttributes(uiType)
|
||||
.FirstOrDefault(a => a.AttributeType.Name == nameof(WindowAttribute));
|
||||
var uiUpdateAttribute = CustomAttributeData.GetCustomAttributes(uiType)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using AlicizaX;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
@ -13,6 +14,9 @@ namespace AlicizaX.UI.Runtime
|
||||
public readonly Type UILogicType;
|
||||
public bool InCache = false;
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
public CancellationToken CancellationToken => _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
|
||||
public UIState State
|
||||
{
|
||||
get
|
||||
@ -31,9 +35,17 @@ namespace AlicizaX.UI.Runtime
|
||||
return;
|
||||
|
||||
View = (UIBase)InstanceFactory.CreateInstanceOptimized(UILogicType);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelAsyncOperations()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAsync().Forget();
|
||||
@ -41,6 +53,8 @@ namespace AlicizaX.UI.Runtime
|
||||
|
||||
private async UniTask DisposeAsync()
|
||||
{
|
||||
CancelAsyncOperations();
|
||||
|
||||
if (State != UIState.Uninitialized && State != UIState.Destroying)
|
||||
{
|
||||
await View.InternalDestroy();
|
||||
|
||||
@ -15,7 +15,6 @@ namespace AlicizaX.UI.Runtime
|
||||
public static UIMetadataObject Create(UIMetadata target, RuntimeTypeHandle handle)
|
||||
{
|
||||
UIMetadataObject obj = MemoryPool.Acquire<UIMetadataObject>();
|
||||
// Use type handle hash code as name to avoid string allocation
|
||||
obj.Initialize(handle.GetHashCode().ToString(), target);
|
||||
return obj;
|
||||
}
|
||||
|
||||
@ -6,7 +6,19 @@ namespace AlicizaX.UI.Runtime
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -43,7 +55,7 @@ namespace AlicizaX.UI.Runtime
|
||||
}
|
||||
|
||||
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)
|
||||
@ -60,13 +72,13 @@ namespace AlicizaX.UI.Runtime
|
||||
|
||||
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);
|
||||
result.Item1.InCache = false;
|
||||
if (result.Item2 > 0)
|
||||
entry.Metadata.InCache = false;
|
||||
if (entry.TimerId > 0)
|
||||
{
|
||||
_timerModule.RemoveTimer(result.Item2);
|
||||
_timerModule.RemoveTimer(entry.TimerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using AlicizaX;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
@ -29,7 +30,7 @@ namespace AlicizaX.UI.Runtime
|
||||
CreateMetaUI(metaInfo);
|
||||
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
|
||||
FinalizeShow(metaInfo, userDatas);
|
||||
UpdateVisualState(metaInfo).Forget();
|
||||
await UpdateVisualState(metaInfo, metaInfo.CancellationToken);
|
||||
return metaInfo.View;
|
||||
}
|
||||
|
||||
@ -49,6 +50,7 @@ namespace AlicizaX.UI.Runtime
|
||||
return;
|
||||
}
|
||||
|
||||
meta.CancelAsyncOperations();
|
||||
await meta.View.InternalClose();
|
||||
Pop(meta);
|
||||
SortWindowVisible(meta.MetaInfo.UILayer);
|
||||
@ -167,16 +169,16 @@ namespace AlicizaX.UI.Runtime
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private async UniTask UpdateVisualState(UIMetadata meta)
|
||||
private async UniTask UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default)
|
||||
{
|
||||
SortWindowVisible(meta.MetaInfo.UILayer);
|
||||
SortWindowDepth(meta.MetaInfo.UILayer);
|
||||
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)
|
||||
|
||||
@ -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.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using AlicizaX;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
@ -70,7 +71,7 @@ namespace AlicizaX.UI.Runtime
|
||||
{
|
||||
metadata.CreateUI();
|
||||
await UIHolderFactory.CreateUIResourceAsync(metadata, parent, this);
|
||||
await ProcessWidget(metadata, visible);
|
||||
await ProcessWidget(metadata, visible, metadata.CancellationToken);
|
||||
return (UIBase)metadata.View;
|
||||
}
|
||||
|
||||
@ -105,7 +106,7 @@ namespace AlicizaX.UI.Runtime
|
||||
metadata.CreateUI();
|
||||
UIBase widget = (UIBase)metadata.View;
|
||||
widget.BindUIHolder(holder, this);
|
||||
await ProcessWidget(metadata, true);
|
||||
await ProcessWidget(metadata, true, metadata.CancellationToken);
|
||||
return (T)widget;
|
||||
}
|
||||
|
||||
@ -142,14 +143,14 @@ namespace AlicizaX.UI.Runtime
|
||||
#endregion
|
||||
|
||||
|
||||
private async UniTask ProcessWidget(UIMetadata meta, bool visible)
|
||||
private async UniTask ProcessWidget(UIMetadata meta, bool visible, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!AddWidget(meta)) return;
|
||||
await meta.View.InternalInitlized();
|
||||
await meta.View.InternalInitlized(cancellationToken);
|
||||
meta.View.Visible = 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))
|
||||
{
|
||||
meta.CancelAsyncOperations();
|
||||
await widget.InternalClose();
|
||||
|
||||
// Remove from updateable list if present
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using AlicizaX;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
@ -68,7 +69,7 @@ namespace AlicizaX.UI.Runtime
|
||||
/// <summary>
|
||||
/// 如果重写当前方法 则同步OnInitialize不会调用
|
||||
/// </summary>
|
||||
protected virtual UniTask OnInitializeAsync()
|
||||
protected virtual UniTask OnInitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
OnInitialize();
|
||||
return UniTask.CompletedTask;
|
||||
@ -77,7 +78,7 @@ namespace AlicizaX.UI.Runtime
|
||||
/// <summary>
|
||||
/// 如果重写当前方法 则同步OnOpen不会调用
|
||||
/// </summary>
|
||||
protected virtual UniTask OnOpenAsync()
|
||||
protected virtual UniTask OnOpenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
OnOpen();
|
||||
return UniTask.CompletedTask;
|
||||
@ -86,7 +87,7 @@ namespace AlicizaX.UI.Runtime
|
||||
/// <summary>
|
||||
/// 如果重写当前方法 则同步OnClose不会调用
|
||||
/// </summary>
|
||||
protected virtual UniTask OnCloseAsync()
|
||||
protected virtual UniTask OnCloseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
OnClose();
|
||||
return UniTask.CompletedTask;
|
||||
@ -203,18 +204,18 @@ namespace AlicizaX.UI.Runtime
|
||||
|
||||
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))
|
||||
return;
|
||||
|
||||
_state = UIState.Initialized;
|
||||
Holder.OnWindowInitEvent?.Invoke();
|
||||
await OnInitializeAsync();
|
||||
await OnInitializeAsync(cancellationToken);
|
||||
OnRegisterEvent(EventListenerProxy);
|
||||
}
|
||||
|
||||
internal async UniTask InternalOpen()
|
||||
internal async UniTask InternalOpen(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_state == UIState.Opened)
|
||||
return; // Already open
|
||||
@ -225,11 +226,11 @@ namespace AlicizaX.UI.Runtime
|
||||
_state = UIState.Opened;
|
||||
Visible = true;
|
||||
Holder.OnWindowBeforeShowEvent?.Invoke();
|
||||
await OnOpenAsync();
|
||||
await OnOpenAsync(cancellationToken);
|
||||
Holder.OnWindowAfterShowEvent?.Invoke();
|
||||
}
|
||||
|
||||
internal async UniTask InternalClose()
|
||||
internal async UniTask InternalClose(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_state != UIState.Opened)
|
||||
return; // Not open, nothing to close
|
||||
@ -238,7 +239,7 @@ namespace AlicizaX.UI.Runtime
|
||||
return;
|
||||
|
||||
Holder.OnWindowBeforeClosedEvent?.Invoke();
|
||||
await OnCloseAsync();
|
||||
await OnCloseAsync(cancellationToken);
|
||||
_state = UIState.Closed;
|
||||
Visible = false;
|
||||
Holder.OnWindowAfterClosedEvent?.Invoke();
|
||||
|
||||
@ -3,10 +3,7 @@ using System.Collections.Generic;
|
||||
|
||||
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
|
||||
{
|
||||
private static readonly Dictionary<UIState, HashSet<UIState>> _validTransitions = new()
|
||||
@ -21,24 +18,11 @@ namespace AlicizaX.UI.Runtime
|
||||
[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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (IsValidTransition(from, to))
|
||||
@ -48,11 +32,7 @@ namespace AlicizaX.UI.Runtime
|
||||
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)
|
||||
{
|
||||
return _validTransitions.TryGetValue(currentState, out var states)
|
||||
@ -60,9 +40,6 @@ namespace AlicizaX.UI.Runtime
|
||||
: new HashSet<UIState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable description of the state.
|
||||
/// </summary>
|
||||
public static string GetStateDescription(UIState state)
|
||||
{
|
||||
return state switch
|
||||
|
||||
Loading…
Reference in New Issue
Block a user