mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(coroutine): 添加协程分组管理和优先级支持
- 实现协程分组功能,支持批量暂停、恢复和终止协程 - 添加协程优先级系统,支持从最低到最高的5个优先级级别 - 引入协程统计功能,跟踪启动、完成、失败数量及执行时间 - 添加FakeTimeSource用于协程测试的时间控制 - 实现按优先级排序的协程执行机制 - 添加协程执行时间戳记录功能 - 实现完整的协程统计报告生成功能
This commit is contained in:
parent
16d8cad4f3
commit
e5e3a1c0ca
33
GFramework.Core.Abstractions/coroutine/CoroutinePriority.cs
Normal file
33
GFramework.Core.Abstractions/coroutine/CoroutinePriority.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace GFramework.Core.Abstractions.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程优先级枚举
|
||||
/// 定义协程的执行优先级,高优先级的协程会优先执行
|
||||
/// </summary>
|
||||
public enum CoroutinePriority : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 最低优先级
|
||||
/// </summary>
|
||||
Lowest = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 低优先级
|
||||
/// </summary>
|
||||
Low = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 普通优先级(默认)
|
||||
/// </summary>
|
||||
Normal = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 高优先级
|
||||
/// </summary>
|
||||
High = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 最高优先级
|
||||
/// </summary>
|
||||
Highest = 4
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
namespace GFramework.Core.Abstractions.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程统计信息接口
|
||||
/// 提供协程执行的性能统计数据
|
||||
/// </summary>
|
||||
public interface ICoroutineStatistics
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取总协程启动数量
|
||||
/// </summary>
|
||||
long TotalStarted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取总协程完成数量
|
||||
/// </summary>
|
||||
long TotalCompleted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取总协程失败数量
|
||||
/// </summary>
|
||||
long TotalFailed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前活跃协程数量
|
||||
/// </summary>
|
||||
int ActiveCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前暂停协程数量
|
||||
/// </summary>
|
||||
int PausedCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取协程平均执行时间(毫秒)
|
||||
/// </summary>
|
||||
double AverageExecutionTimeMs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取协程最大执行时间(毫秒)
|
||||
/// </summary>
|
||||
double MaxExecutionTimeMs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取按优先级分组的协程数量
|
||||
/// </summary>
|
||||
/// <param name="priority">协程优先级</param>
|
||||
/// <returns>指定优先级的协程数量</returns>
|
||||
int GetCountByPriority(CoroutinePriority priority);
|
||||
|
||||
/// <summary>
|
||||
/// 获取按标签分组的协程数量
|
||||
/// </summary>
|
||||
/// <param name="tag">协程标签</param>
|
||||
/// <returns>指定标签的协程数量</returns>
|
||||
int GetCountByTag(string tag);
|
||||
|
||||
/// <summary>
|
||||
/// 重置统计数据
|
||||
/// </summary>
|
||||
void Reset();
|
||||
|
||||
/// <summary>
|
||||
/// 生成统计报告
|
||||
/// </summary>
|
||||
/// <returns>格式化的统计报告字符串</returns>
|
||||
string GenerateReport();
|
||||
}
|
||||
@ -16,3 +16,5 @@ global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using NUnit.Framework;
|
||||
global using NUnit.Compatibility;
|
||||
197
GFramework.Core.Tests/coroutine/CoroutineGroupTests.cs
Normal file
197
GFramework.Core.Tests/coroutine/CoroutineGroupTests.cs
Normal file
@ -0,0 +1,197 @@
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace GFramework.Core.Tests.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程分组管理测试
|
||||
/// </summary>
|
||||
public sealed class CoroutineGroupTests
|
||||
{
|
||||
[Test]
|
||||
public void Run_WithGroup_ShouldAddToGroup()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "OtherGroup");
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(2));
|
||||
Assert.That(scheduler.GetGroupCount("OtherGroup"), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PauseGroup_ShouldPauseAllCoroutinesInGroup()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
var executionCount = 0;
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
executionCount++;
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
|
||||
scheduler.Update(); // 第一次更新,执行计数应为 4(每个协程执行2次)
|
||||
var pausedCount = scheduler.PauseGroup("TestGroup");
|
||||
scheduler.Update(); // 暂停后更新,执行计数不应增加
|
||||
|
||||
// Assert
|
||||
Assert.That(pausedCount, Is.EqualTo(2));
|
||||
Assert.That(executionCount, Is.EqualTo(4)); // 第一次更新时每个协程执行了2次
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResumeGroup_ShouldResumeAllCoroutinesInGroup()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
var executionCount = 0;
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
executionCount++;
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
|
||||
scheduler.Update(); // 第一次更新
|
||||
scheduler.PauseGroup("TestGroup");
|
||||
scheduler.Update(); // 暂停期间
|
||||
var resumedCount = scheduler.ResumeGroup("TestGroup");
|
||||
scheduler.Update(); // 恢复后更新
|
||||
|
||||
// Assert
|
||||
Assert.That(resumedCount, Is.EqualTo(2));
|
||||
Assert.That(executionCount, Is.EqualTo(6)); // 第一次 4,恢复后 2
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void KillGroup_ShouldKillAllCoroutinesInGroup()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "OtherGroup");
|
||||
|
||||
var initialCount = scheduler.ActiveCoroutineCount;
|
||||
var killedCount = scheduler.KillGroup("TestGroup");
|
||||
|
||||
// Assert
|
||||
Assert.That(initialCount, Is.EqualTo(3));
|
||||
Assert.That(killedCount, Is.EqualTo(2));
|
||||
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
|
||||
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetGroupCount_WithNonExistentGroup_ShouldReturnZero()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(scheduler.GetGroupCount("NonExistent"), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Complete_ShouldRemoveFromGroup()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame(); // 等待一帧后完成
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
scheduler.Run(TestCoroutine(), group: "TestGroup");
|
||||
|
||||
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(2));
|
||||
|
||||
scheduler.Update(); // 协程完成
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PauseGroup_WithMixedGroups_ShouldOnlyAffectTargetGroup()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
var group1Count = 0;
|
||||
var group2Count = 0;
|
||||
|
||||
IEnumerator<IYieldInstruction> Group1Coroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
group1Count++;
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator<IYieldInstruction> Group2Coroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
group2Count++;
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(Group1Coroutine(), group: "Group1");
|
||||
scheduler.Run(Group2Coroutine(), group: "Group2");
|
||||
|
||||
scheduler.Update(); // 第一次更新
|
||||
scheduler.PauseGroup("Group1");
|
||||
scheduler.Update(); // Group1 暂停,Group2 继续
|
||||
|
||||
// Assert
|
||||
Assert.That(group1Count, Is.EqualTo(2)); // Group1 执行了2次(第一次更新)
|
||||
Assert.That(group2Count, Is.EqualTo(3)); // Group2 执行了3次(第一次更新2次,第二次更新1次)
|
||||
}
|
||||
}
|
||||
133
GFramework.Core.Tests/coroutine/CoroutinePriorityTests.cs
Normal file
133
GFramework.Core.Tests/coroutine/CoroutinePriorityTests.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace GFramework.Core.Tests.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程优先级测试
|
||||
/// </summary>
|
||||
public sealed class CoroutinePriorityTests
|
||||
{
|
||||
[Test]
|
||||
public void Run_WithPriority_ShouldExecuteInPriorityOrder()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
IEnumerator<IYieldInstruction> CreateCoroutine(string name)
|
||||
{
|
||||
yield return new WaitOneFrame(); // 先等待一帧
|
||||
executionOrder.Add(name); // 在第一次 Update 时才记录
|
||||
}
|
||||
|
||||
// Act - 以不同优先级启动协程
|
||||
scheduler.Run(CreateCoroutine("Low"), priority: CoroutinePriority.Low);
|
||||
scheduler.Run(CreateCoroutine("High"), priority: CoroutinePriority.High);
|
||||
scheduler.Run(CreateCoroutine("Normal"), priority: CoroutinePriority.Normal);
|
||||
scheduler.Run(CreateCoroutine("Highest"), priority: CoroutinePriority.Highest);
|
||||
scheduler.Run(CreateCoroutine("Lowest"), priority: CoroutinePriority.Lowest);
|
||||
|
||||
scheduler.Update();
|
||||
|
||||
// Assert - 高优先级应该先执行
|
||||
Assert.That(executionOrder[0], Is.EqualTo("Highest"));
|
||||
Assert.That(executionOrder[1], Is.EqualTo("High"));
|
||||
Assert.That(executionOrder[2], Is.EqualTo("Normal"));
|
||||
Assert.That(executionOrder[3], Is.EqualTo("Low"));
|
||||
Assert.That(executionOrder[4], Is.EqualTo("Lowest"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Run_WithSamePriority_ShouldExecuteInStartOrder()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
IEnumerator<IYieldInstruction> CreateCoroutine(string name)
|
||||
{
|
||||
executionOrder.Add(name);
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act - 以相同优先级启动协程
|
||||
scheduler.Run(CreateCoroutine("First"), priority: CoroutinePriority.Normal);
|
||||
scheduler.Run(CreateCoroutine("Second"), priority: CoroutinePriority.Normal);
|
||||
scheduler.Run(CreateCoroutine("Third"), priority: CoroutinePriority.Normal);
|
||||
|
||||
scheduler.Update();
|
||||
|
||||
// Assert - 相同优先级按启动顺序执行
|
||||
Assert.That(executionOrder[0], Is.EqualTo("First"));
|
||||
Assert.That(executionOrder[1], Is.EqualTo("Second"));
|
||||
Assert.That(executionOrder[2], Is.EqualTo("Third"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Run_WithDefaultPriority_ShouldUseNormal()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
var executed = false;
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
executed = true;
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine());
|
||||
|
||||
// Assert - 在 Update 之前检查统计
|
||||
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.Normal), Is.EqualTo(1));
|
||||
|
||||
scheduler.Update();
|
||||
Assert.That(executed, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_WithMixedPriorities_ShouldRespectPriorityDuringExecution()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
IEnumerator<IYieldInstruction> CreateMultiStepCoroutine(string name)
|
||||
{
|
||||
yield return new WaitOneFrame(); // 先等待,避免在 Prewarm 时执行
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
executionOrder.Add($"{name}-{i}");
|
||||
yield return new Delay(0.02); // 等待稍长的时间,确保不会在同一帧完成
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(CreateMultiStepCoroutine("Low"), priority: CoroutinePriority.Low);
|
||||
scheduler.Run(CreateMultiStepCoroutine("High"), priority: CoroutinePriority.High);
|
||||
|
||||
// 执行多帧,每帧推进 0.016 秒
|
||||
// 第一帧:WaitOneFrame 完成
|
||||
// 之后每 2 帧(0.032秒)完成一次 Delay(0.02)
|
||||
for (var frame = 0; frame < 8; frame++)
|
||||
{
|
||||
timeSource.Advance(0.016);
|
||||
scheduler.Update();
|
||||
}
|
||||
|
||||
// Assert - 每帧都应该先执行高优先级
|
||||
Assert.That(executionOrder[0], Is.EqualTo("High-0"));
|
||||
Assert.That(executionOrder[1], Is.EqualTo("Low-0"));
|
||||
Assert.That(executionOrder[2], Is.EqualTo("High-1"));
|
||||
Assert.That(executionOrder[3], Is.EqualTo("Low-1"));
|
||||
Assert.That(executionOrder[4], Is.EqualTo("High-2"));
|
||||
Assert.That(executionOrder[5], Is.EqualTo("Low-2"));
|
||||
}
|
||||
}
|
||||
351
GFramework.Core.Tests/coroutine/CoroutineStatisticsTests.cs
Normal file
351
GFramework.Core.Tests/coroutine/CoroutineStatisticsTests.cs
Normal file
@ -0,0 +1,351 @@
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace GFramework.Core.Tests.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程统计功能测试
|
||||
/// </summary>
|
||||
public sealed class CoroutineStatisticsTests
|
||||
{
|
||||
[Test]
|
||||
public void Statistics_WhenDisabled_ShouldBeNull()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: false);
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(scheduler.Statistics, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Statistics_WhenEnabled_ShouldNotBeNull()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(scheduler.Statistics, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TotalStarted_ShouldTrackStartedCoroutines()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Run(TestCoroutine());
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.TotalStarted, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TotalCompleted_ShouldTrackCompletedCoroutines()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield break; // 立即完成
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Update();
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.TotalCompleted, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TotalFailed_ShouldTrackFailedCoroutines()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> FailingCoroutine()
|
||||
{
|
||||
throw new InvalidOperationException("Test exception");
|
||||
#pragma warning disable CS0162 // 检测到无法访问的代码
|
||||
yield break;
|
||||
#pragma warning restore CS0162 // 检测到无法访问的代码
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(FailingCoroutine());
|
||||
scheduler.Run(FailingCoroutine());
|
||||
scheduler.Update();
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.TotalFailed, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ActiveCount_ShouldReflectCurrentActiveCoroutines()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> LongRunningCoroutine()
|
||||
{
|
||||
for (var i = 0; i < 10; i++)
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(LongRunningCoroutine());
|
||||
scheduler.Run(LongRunningCoroutine());
|
||||
scheduler.Update();
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.ActiveCount, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PausedCount_ShouldReflectCurrentPausedCoroutines()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
var handle1 = scheduler.Run(TestCoroutine());
|
||||
var handle2 = scheduler.Run(TestCoroutine());
|
||||
scheduler.Pause(handle1);
|
||||
scheduler.Pause(handle2);
|
||||
scheduler.Update();
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.PausedCount, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AverageExecutionTimeMs_ShouldCalculateCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Run(TestCoroutine());
|
||||
|
||||
// 执行两帧,每帧 16ms
|
||||
timeSource.Advance(0.016);
|
||||
scheduler.Update();
|
||||
timeSource.Advance(0.016);
|
||||
scheduler.Update();
|
||||
|
||||
// Assert
|
||||
var avgTime = scheduler.Statistics!.AverageExecutionTimeMs;
|
||||
Assert.That(avgTime > 0, Is.True);
|
||||
Assert.That(avgTime <= 32, Is.True); // 最多 32ms
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MaxExecutionTimeMs_ShouldTrackLongestExecution()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> ShortCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
IEnumerator<IYieldInstruction> LongCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
yield return new WaitOneFrame();
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(ShortCoroutine());
|
||||
scheduler.Run(LongCoroutine());
|
||||
|
||||
// 执行足够的帧
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
timeSource.Advance(0.016);
|
||||
scheduler.Update();
|
||||
}
|
||||
|
||||
// Assert
|
||||
var maxTime = scheduler.Statistics!.MaxExecutionTimeMs;
|
||||
Assert.That(maxTime > 0, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetCountByPriority_ShouldReturnCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), priority: CoroutinePriority.High);
|
||||
scheduler.Run(TestCoroutine(), priority: CoroutinePriority.High);
|
||||
scheduler.Run(TestCoroutine(), priority: CoroutinePriority.Low);
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.High), Is.EqualTo(2));
|
||||
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.Low), Is.EqualTo(1));
|
||||
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.Normal), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetCountByTag_ShouldReturnCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield return new WaitOneFrame();
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), tag: "AI");
|
||||
scheduler.Run(TestCoroutine(), tag: "AI");
|
||||
scheduler.Run(TestCoroutine(), tag: "Physics");
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics!.GetCountByTag("AI"), Is.EqualTo(2));
|
||||
Assert.That(scheduler.Statistics!.GetCountByTag("Physics"), Is.EqualTo(1));
|
||||
Assert.That(scheduler.Statistics!.GetCountByTag("Graphics"), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reset_ShouldClearAllStatistics()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Run(TestCoroutine());
|
||||
scheduler.Update();
|
||||
|
||||
scheduler.Statistics!.Reset();
|
||||
|
||||
// Assert
|
||||
Assert.That(scheduler.Statistics.TotalStarted, Is.EqualTo(0));
|
||||
Assert.That(scheduler.Statistics.TotalCompleted, Is.EqualTo(0));
|
||||
Assert.That(scheduler.Statistics.TotalFailed, Is.EqualTo(0));
|
||||
Assert.That(scheduler.Statistics.ActiveCount, Is.EqualTo(0));
|
||||
Assert.That(scheduler.Statistics.PausedCount, Is.EqualTo(0));
|
||||
Assert.That(scheduler.Statistics.AverageExecutionTimeMs, Is.EqualTo(0));
|
||||
Assert.That(scheduler.Statistics.MaxExecutionTimeMs, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateReport_ShouldReturnFormattedString()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Act
|
||||
scheduler.Run(TestCoroutine(), tag: "Test", priority: CoroutinePriority.High);
|
||||
scheduler.Update();
|
||||
|
||||
var report = scheduler.Statistics!.GenerateReport();
|
||||
|
||||
// Assert
|
||||
Assert.That(report, Is.Not.Null);
|
||||
Assert.That(report, Does.Contain("协程统计报告"));
|
||||
Assert.That(report, Does.Contain("总启动数"));
|
||||
Assert.That(report, Does.Contain("总完成数"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Statistics_ShouldBeThreadSafe()
|
||||
{
|
||||
// Arrange
|
||||
var timeSource = new FakeTimeSource();
|
||||
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
|
||||
|
||||
IEnumerator<IYieldInstruction> TestCoroutine()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Act - 并发读取统计信息
|
||||
var tasks = new List<Task>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
for (var j = 0; j < 100; j++)
|
||||
{
|
||||
_ = scheduler.Statistics!.TotalStarted;
|
||||
_ = scheduler.Statistics.TotalCompleted;
|
||||
_ = scheduler.Statistics.AverageExecutionTimeMs;
|
||||
_ = scheduler.Statistics.GenerateReport();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 同时启动和完成协程
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
scheduler.Run(TestCoroutine());
|
||||
}
|
||||
|
||||
scheduler.Update();
|
||||
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
|
||||
// Assert - 不应该抛出异常
|
||||
Assert.That(scheduler.Statistics!.TotalStarted, Is.EqualTo(100));
|
||||
Assert.That(scheduler.Statistics.TotalCompleted, Is.EqualTo(100));
|
||||
}
|
||||
}
|
||||
47
GFramework.Core.Tests/coroutine/FakeTimeSource.cs
Normal file
47
GFramework.Core.Tests/coroutine/FakeTimeSource.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
|
||||
namespace GFramework.Core.Tests.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 可控制的时间源,用于协程测试
|
||||
/// </summary>
|
||||
public sealed class FakeTimeSource : ITimeSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前累计时间
|
||||
/// </summary>
|
||||
public double CurrentTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取上一帧的时间增量
|
||||
/// </summary>
|
||||
public double DeltaTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间源
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
// 在测试中,Update 不做任何事情
|
||||
// 时间推进由 Advance 方法控制
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前进指定的时间
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">时间增量(秒)</param>
|
||||
public void Advance(double deltaTime)
|
||||
{
|
||||
DeltaTime = deltaTime;
|
||||
CurrentTime += deltaTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置时间源到初始状态
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
CurrentTime = 0;
|
||||
DeltaTime = 0;
|
||||
}
|
||||
}
|
||||
@ -7,11 +7,26 @@ namespace GFramework.Core.coroutine;
|
||||
/// </summary>
|
||||
internal class CoroutineMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// 协程的分组标识符,用于批量管理协程
|
||||
/// </summary>
|
||||
public string? Group;
|
||||
|
||||
/// <summary>
|
||||
/// 协程的优先级
|
||||
/// </summary>
|
||||
public CoroutinePriority Priority;
|
||||
|
||||
/// <summary>
|
||||
/// 协程在调度器中的槽位索引
|
||||
/// </summary>
|
||||
public int SlotIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 协程开始执行的时间戳(毫秒)
|
||||
/// </summary>
|
||||
public double StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// 协程当前的执行状态
|
||||
/// </summary>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
using GFramework.Core.logging;
|
||||
@ -12,13 +12,17 @@ namespace GFramework.Core.coroutine;
|
||||
/// <param name="timeSource">时间源接口,提供时间相关数据</param>
|
||||
/// <param name="instanceId">实例ID,默认为1</param>
|
||||
/// <param name="initialCapacity">初始容量,默认为256</param>
|
||||
/// <param name="enableStatistics">是否启用统计功能,默认为false</param>
|
||||
public sealed class CoroutineScheduler(
|
||||
ITimeSource timeSource,
|
||||
byte instanceId = 1,
|
||||
int initialCapacity = 256)
|
||||
int initialCapacity = 256,
|
||||
bool enableStatistics = false)
|
||||
{
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new();
|
||||
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
|
||||
private readonly Dictionary<CoroutineHandle, CoroutineMetadata> _metadata = new();
|
||||
private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null;
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new();
|
||||
private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource));
|
||||
private readonly Dictionary<CoroutineHandle, HashSet<CoroutineHandle>> _waiting = new();
|
||||
@ -36,6 +40,11 @@ public sealed class CoroutineScheduler(
|
||||
/// </summary>
|
||||
public int ActiveCoroutineCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取协程统计信息(如果启用)
|
||||
/// </summary>
|
||||
public ICoroutineStatistics? Statistics => _statistics;
|
||||
|
||||
/// <summary>
|
||||
/// 协程异常处理回调,当协程执行过程中发生异常时触发
|
||||
/// 注意:事件处理程序会在独立任务中异步调用,以避免阻塞调度器主循环
|
||||
@ -61,10 +70,14 @@ public sealed class CoroutineScheduler(
|
||||
/// </summary>
|
||||
/// <param name="coroutine">要运行的协程枚举器</param>
|
||||
/// <param name="tag">协程标签,可选</param>
|
||||
/// <param name="priority">协程优先级,默认为Normal</param>
|
||||
/// <param name="group">协程分组,可选</param>
|
||||
/// <returns>协程句柄</returns>
|
||||
public CoroutineHandle Run(
|
||||
IEnumerator<IYieldInstruction>? coroutine,
|
||||
string? tag = null)
|
||||
string? tag = null,
|
||||
CoroutinePriority priority = CoroutinePriority.Normal,
|
||||
string? group = null)
|
||||
{
|
||||
if (coroutine == null)
|
||||
return default;
|
||||
@ -79,7 +92,8 @@ public sealed class CoroutineScheduler(
|
||||
{
|
||||
Enumerator = coroutine,
|
||||
State = CoroutineState.Running,
|
||||
Handle = handle
|
||||
Handle = handle,
|
||||
Priority = priority
|
||||
};
|
||||
|
||||
_slots[slotIndex] = slot;
|
||||
@ -87,12 +101,20 @@ public sealed class CoroutineScheduler(
|
||||
{
|
||||
SlotIndex = slotIndex,
|
||||
State = CoroutineState.Running,
|
||||
Tag = tag
|
||||
Tag = tag,
|
||||
Priority = priority,
|
||||
Group = group,
|
||||
StartTime = _timeSource.CurrentTime * 1000 // 转换为毫秒
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
AddTag(tag, handle);
|
||||
|
||||
if (!string.IsNullOrEmpty(group))
|
||||
AddGroup(group, handle);
|
||||
|
||||
_statistics?.RecordStart(priority, tag);
|
||||
|
||||
Prewarm(slotIndex);
|
||||
ActiveCoroutineCount++;
|
||||
|
||||
@ -107,8 +129,34 @@ public sealed class CoroutineScheduler(
|
||||
_timeSource.Update();
|
||||
var delta = _timeSource.DeltaTime;
|
||||
|
||||
// 遍历所有槽位并更新协程状态
|
||||
// 更新统计信息
|
||||
if (_statistics != null)
|
||||
{
|
||||
_statistics.ActiveCount = ActiveCoroutineCount;
|
||||
_statistics.PausedCount = _metadata.Count(m => m.Value.State == CoroutineState.Paused);
|
||||
}
|
||||
|
||||
// 按优先级排序槽位索引(高优先级优先执行)
|
||||
var sortedIndices = new List<int>(_nextSlot);
|
||||
for (var i = 0; i < _nextSlot; i++)
|
||||
{
|
||||
var slot = _slots[i];
|
||||
if (slot is { State: CoroutineState.Running })
|
||||
sortedIndices.Add(i);
|
||||
}
|
||||
|
||||
// 按优先级降序排序
|
||||
sortedIndices.Sort((a, b) =>
|
||||
{
|
||||
var slotA = _slots[a];
|
||||
var slotB = _slots[b];
|
||||
if (slotA == null || slotB == null)
|
||||
return 0;
|
||||
return slotB.Priority.CompareTo(slotA.Priority);
|
||||
});
|
||||
|
||||
// 遍历所有槽位并更新协程状态
|
||||
foreach (var i in sortedIndices)
|
||||
{
|
||||
var slot = _slots[i];
|
||||
if (slot is not { State: CoroutineState.Running })
|
||||
@ -254,6 +302,59 @@ public sealed class CoroutineScheduler(
|
||||
|
||||
#endregion
|
||||
|
||||
#region Group Management
|
||||
|
||||
/// <summary>
|
||||
/// 暂停指定分组的所有协程
|
||||
/// </summary>
|
||||
/// <param name="group">分组名称</param>
|
||||
/// <returns>被暂停的协程数量</returns>
|
||||
public int PauseGroup(string group)
|
||||
{
|
||||
if (!_grouped.TryGetValue(group, out var handles))
|
||||
return 0;
|
||||
|
||||
return handles.Count(Pause);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复指定分组的所有协程
|
||||
/// </summary>
|
||||
/// <param name="group">分组名称</param>
|
||||
/// <returns>被恢复的协程数量</returns>
|
||||
public int ResumeGroup(string group)
|
||||
{
|
||||
if (!_grouped.TryGetValue(group, out var handles))
|
||||
return 0;
|
||||
|
||||
return handles.Count(Resume);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 终止指定分组的所有协程
|
||||
/// </summary>
|
||||
/// <param name="group">分组名称</param>
|
||||
/// <returns>被终止的协程数量</returns>
|
||||
public int KillGroup(string group)
|
||||
{
|
||||
if (!_grouped.TryGetValue(group, out var handles))
|
||||
return 0;
|
||||
|
||||
return handles.Count(Kill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定分组的协程数量
|
||||
/// </summary>
|
||||
/// <param name="group">分组名称</param>
|
||||
/// <returns>协程数量</returns>
|
||||
public int GetGroupCount(string group)
|
||||
{
|
||||
return _grouped.TryGetValue(group, out var handles) ? handles.Count : 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Wait / Tag / Clear
|
||||
|
||||
/// <summary>
|
||||
@ -289,8 +390,8 @@ public sealed class CoroutineScheduler(
|
||||
{
|
||||
if (!_tagged.TryGetValue(tag, out var handles))
|
||||
return 0;
|
||||
var copy = handles.ToArray();
|
||||
return copy.Count(Kill);
|
||||
|
||||
return handles.Count(Kill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -303,6 +404,7 @@ public sealed class CoroutineScheduler(
|
||||
Array.Clear(_slots);
|
||||
_metadata.Clear();
|
||||
_tagged.Clear();
|
||||
_grouped.Clear();
|
||||
_waiting.Clear();
|
||||
|
||||
_nextSlot = 0;
|
||||
@ -352,18 +454,26 @@ public sealed class CoroutineScheduler(
|
||||
if (!handle.IsValid)
|
||||
return;
|
||||
|
||||
// 记录统计信息
|
||||
if (_metadata.TryGetValue(handle, out var meta))
|
||||
{
|
||||
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
|
||||
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
|
||||
}
|
||||
|
||||
_slots[slotIndex] = null;
|
||||
ActiveCoroutineCount--;
|
||||
|
||||
RemoveTag(handle);
|
||||
RemoveGroup(handle);
|
||||
_metadata.Remove(handle);
|
||||
|
||||
// 唤醒等待者
|
||||
if (!_waiting.TryGetValue(handle, out var waiters)) return;
|
||||
foreach (var waiter in waiters)
|
||||
{
|
||||
if (!_metadata.TryGetValue(waiter, out var meta)) continue;
|
||||
var s = _slots[meta.SlotIndex];
|
||||
if (!_metadata.TryGetValue(waiter, out var waiterMeta)) continue;
|
||||
var s = _slots[waiterMeta.SlotIndex];
|
||||
if (s == null) continue;
|
||||
switch (s.Waiting)
|
||||
{
|
||||
@ -374,7 +484,7 @@ public sealed class CoroutineScheduler(
|
||||
}
|
||||
|
||||
s.State = CoroutineState.Running;
|
||||
meta.State = CoroutineState.Running;
|
||||
waiterMeta.State = CoroutineState.Running;
|
||||
}
|
||||
|
||||
_waiting.Remove(handle);
|
||||
@ -390,6 +500,12 @@ public sealed class CoroutineScheduler(
|
||||
var slot = _slots[slotIndex];
|
||||
var handle = slot?.Handle ?? default;
|
||||
|
||||
// 记录统计信息
|
||||
if (handle.IsValid && _metadata.TryGetValue(handle, out var meta))
|
||||
{
|
||||
_statistics?.RecordFailure(meta.Priority, meta.Tag);
|
||||
}
|
||||
|
||||
// 将异常回调派发到线程池,避免阻塞调度器主循环
|
||||
var handler = OnCoroutineException;
|
||||
if (handler != null)
|
||||
@ -459,5 +575,41 @@ public sealed class CoroutineScheduler(
|
||||
meta.Tag = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为协程添加分组
|
||||
/// </summary>
|
||||
/// <param name="group">分组名称</param>
|
||||
/// <param name="handle">协程句柄</param>
|
||||
private void AddGroup(string group, CoroutineHandle handle)
|
||||
{
|
||||
if (!_grouped.TryGetValue(group, out var set))
|
||||
{
|
||||
set = new HashSet<CoroutineHandle>();
|
||||
_grouped[group] = set;
|
||||
}
|
||||
|
||||
set.Add(handle);
|
||||
_metadata[handle].Group = group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除协程分组
|
||||
/// </summary>
|
||||
/// <param name="handle">协程句柄</param>
|
||||
private void RemoveGroup(CoroutineHandle handle)
|
||||
{
|
||||
if (!_metadata.TryGetValue(handle, out var meta) || meta.Group == null)
|
||||
return;
|
||||
|
||||
if (_grouped.TryGetValue(meta.Group, out var set))
|
||||
{
|
||||
set.Remove(handle);
|
||||
if (set.Count == 0)
|
||||
_grouped.Remove(meta.Group);
|
||||
}
|
||||
|
||||
meta.Group = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -17,6 +17,16 @@ internal sealed class CoroutineSlot
|
||||
/// </summary>
|
||||
public CoroutineHandle Handle;
|
||||
|
||||
/// <summary>
|
||||
/// 协程是否已经开始执行
|
||||
/// </summary>
|
||||
public bool HasStarted;
|
||||
|
||||
/// <summary>
|
||||
/// 协程的优先级
|
||||
/// </summary>
|
||||
public CoroutinePriority Priority;
|
||||
|
||||
/// <summary>
|
||||
/// 协程当前状态
|
||||
/// </summary>
|
||||
|
||||
205
GFramework.Core/coroutine/CoroutineStatistics.cs
Normal file
205
GFramework.Core/coroutine/CoroutineStatistics.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using System.Text;
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
|
||||
namespace GFramework.Core.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程统计信息实现类
|
||||
/// 线程安全:使用 Interlocked 操作确保计数器的原子性
|
||||
/// </summary>
|
||||
internal sealed class CoroutineStatistics : ICoroutineStatistics
|
||||
{
|
||||
private readonly Dictionary<CoroutinePriority, int> _countByPriority = new();
|
||||
private readonly Dictionary<string, int> _countByTag = new();
|
||||
private readonly object _lock = new();
|
||||
private double _maxExecutionTimeMs;
|
||||
private long _totalCompleted;
|
||||
private long _totalExecutionTimeMs;
|
||||
private long _totalFailed;
|
||||
private long _totalStarted;
|
||||
|
||||
/// <inheritdoc />
|
||||
public long TotalStarted => Interlocked.Read(ref _totalStarted);
|
||||
|
||||
/// <inheritdoc />
|
||||
public long TotalCompleted => Interlocked.Read(ref _totalCompleted);
|
||||
|
||||
/// <inheritdoc />
|
||||
public long TotalFailed => Interlocked.Read(ref _totalFailed);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int ActiveCount { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int PausedCount { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public double AverageExecutionTimeMs
|
||||
{
|
||||
get
|
||||
{
|
||||
var completed = Interlocked.Read(ref _totalCompleted);
|
||||
if (completed == 0)
|
||||
return 0;
|
||||
|
||||
var totalTime = Interlocked.Read(ref _totalExecutionTimeMs);
|
||||
return (double)totalTime / completed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double MaxExecutionTimeMs
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _maxExecutionTimeMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetCountByPriority(CoroutinePriority priority)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _countByPriority.TryGetValue(priority, out var count) ? count : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetCountByTag(string tag)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _countByTag.TryGetValue(tag, out var count) ? count : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
Interlocked.Exchange(ref _totalStarted, 0);
|
||||
Interlocked.Exchange(ref _totalCompleted, 0);
|
||||
Interlocked.Exchange(ref _totalFailed, 0);
|
||||
Interlocked.Exchange(ref _totalExecutionTimeMs, 0);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_maxExecutionTimeMs = 0;
|
||||
_countByPriority.Clear();
|
||||
_countByTag.Clear();
|
||||
ActiveCount = 0;
|
||||
PausedCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GenerateReport()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== 协程统计报告 ===");
|
||||
sb.AppendLine($"总启动数: {TotalStarted}");
|
||||
sb.AppendLine($"总完成数: {TotalCompleted}");
|
||||
sb.AppendLine($"总失败数: {TotalFailed}");
|
||||
sb.AppendLine($"当前活跃: {ActiveCount}");
|
||||
sb.AppendLine($"当前暂停: {PausedCount}");
|
||||
sb.AppendLine($"平均执行时间: {AverageExecutionTimeMs:F2} ms");
|
||||
sb.AppendLine($"最大执行时间: {MaxExecutionTimeMs:F2} ms");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_countByPriority.Count > 0)
|
||||
{
|
||||
sb.AppendLine("\n按优先级统计:");
|
||||
foreach (var kvp in _countByPriority.OrderByDescending(x => x.Key))
|
||||
sb.AppendLine($" {kvp.Key}: {kvp.Value}");
|
||||
}
|
||||
|
||||
if (_countByTag.Count > 0)
|
||||
{
|
||||
sb.AppendLine("\n按标签统计:");
|
||||
foreach (var kvp in _countByTag.OrderByDescending(x => x.Value))
|
||||
sb.AppendLine($" {kvp.Key}: {kvp.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录协程启动
|
||||
/// </summary>
|
||||
/// <param name="priority">协程优先级</param>
|
||||
/// <param name="tag">协程标签</param>
|
||||
public void RecordStart(CoroutinePriority priority, string? tag)
|
||||
{
|
||||
Interlocked.Increment(ref _totalStarted);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_countByPriority.TryGetValue(priority, out var count);
|
||||
_countByPriority[priority] = count + 1;
|
||||
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
_countByTag.TryGetValue(tag, out var tagCount);
|
||||
_countByTag[tag] = tagCount + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录协程完成
|
||||
/// </summary>
|
||||
/// <param name="executionTimeMs">执行时间(毫秒)</param>
|
||||
/// <param name="priority">协程优先级</param>
|
||||
/// <param name="tag">协程标签</param>
|
||||
public void RecordComplete(double executionTimeMs, CoroutinePriority priority, string? tag)
|
||||
{
|
||||
Interlocked.Increment(ref _totalCompleted);
|
||||
Interlocked.Add(ref _totalExecutionTimeMs, (long)executionTimeMs);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (executionTimeMs > _maxExecutionTimeMs)
|
||||
_maxExecutionTimeMs = executionTimeMs;
|
||||
|
||||
_countByPriority.TryGetValue(priority, out var count);
|
||||
_countByPriority[priority] = Math.Max(0, count - 1);
|
||||
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
_countByTag.TryGetValue(tag, out var tagCount);
|
||||
_countByTag[tag] = Math.Max(0, tagCount - 1);
|
||||
if (_countByTag[tag] == 0)
|
||||
_countByTag.Remove(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录协程失败
|
||||
/// </summary>
|
||||
/// <param name="priority">协程优先级</param>
|
||||
/// <param name="tag">协程标签</param>
|
||||
public void RecordFailure(CoroutinePriority priority, string? tag)
|
||||
{
|
||||
Interlocked.Increment(ref _totalFailed);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_countByPriority.TryGetValue(priority, out var count);
|
||||
_countByPriority[priority] = Math.Max(0, count - 1);
|
||||
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
_countByTag.TryGetValue(tag, out var tagCount);
|
||||
_countByTag[tag] = Math.Max(0, tagCount - 1);
|
||||
if (_countByTag[tag] == 0)
|
||||
_countByTag.Remove(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user