diff --git a/GFramework.Core.Abstractions/coroutine/CoroutinePriority.cs b/GFramework.Core.Abstractions/coroutine/CoroutinePriority.cs new file mode 100644 index 0000000..7467dcc --- /dev/null +++ b/GFramework.Core.Abstractions/coroutine/CoroutinePriority.cs @@ -0,0 +1,33 @@ +namespace GFramework.Core.Abstractions.coroutine; + +/// +/// 协程优先级枚举 +/// 定义协程的执行优先级,高优先级的协程会优先执行 +/// +public enum CoroutinePriority : byte +{ + /// + /// 最低优先级 + /// + Lowest = 0, + + /// + /// 低优先级 + /// + Low = 1, + + /// + /// 普通优先级(默认) + /// + Normal = 2, + + /// + /// 高优先级 + /// + High = 3, + + /// + /// 最高优先级 + /// + Highest = 4 +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/coroutine/ICoroutineStatistics.cs b/GFramework.Core.Abstractions/coroutine/ICoroutineStatistics.cs new file mode 100644 index 0000000..139b4e2 --- /dev/null +++ b/GFramework.Core.Abstractions/coroutine/ICoroutineStatistics.cs @@ -0,0 +1,68 @@ +namespace GFramework.Core.Abstractions.coroutine; + +/// +/// 协程统计信息接口 +/// 提供协程执行的性能统计数据 +/// +public interface ICoroutineStatistics +{ + /// + /// 获取总协程启动数量 + /// + long TotalStarted { get; } + + /// + /// 获取总协程完成数量 + /// + long TotalCompleted { get; } + + /// + /// 获取总协程失败数量 + /// + long TotalFailed { get; } + + /// + /// 获取当前活跃协程数量 + /// + int ActiveCount { get; } + + /// + /// 获取当前暂停协程数量 + /// + int PausedCount { get; } + + /// + /// 获取协程平均执行时间(毫秒) + /// + double AverageExecutionTimeMs { get; } + + /// + /// 获取协程最大执行时间(毫秒) + /// + double MaxExecutionTimeMs { get; } + + /// + /// 获取按优先级分组的协程数量 + /// + /// 协程优先级 + /// 指定优先级的协程数量 + int GetCountByPriority(CoroutinePriority priority); + + /// + /// 获取按标签分组的协程数量 + /// + /// 协程标签 + /// 指定标签的协程数量 + int GetCountByTag(string tag); + + /// + /// 重置统计数据 + /// + void Reset(); + + /// + /// 生成统计报告 + /// + /// 格式化的统计报告字符串 + string GenerateReport(); +} \ No newline at end of file diff --git a/GFramework.Core.Tests/GlobalUsings.cs b/GFramework.Core.Tests/GlobalUsings.cs index 4d27181..6f0bb16 100644 --- a/GFramework.Core.Tests/GlobalUsings.cs +++ b/GFramework.Core.Tests/GlobalUsings.cs @@ -15,4 +15,6 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using NUnit.Framework; +global using NUnit.Compatibility; \ No newline at end of file diff --git a/GFramework.Core.Tests/coroutine/CoroutineGroupTests.cs b/GFramework.Core.Tests/coroutine/CoroutineGroupTests.cs new file mode 100644 index 0000000..58b674f --- /dev/null +++ b/GFramework.Core.Tests/coroutine/CoroutineGroupTests.cs @@ -0,0 +1,197 @@ +using GFramework.Core.Abstractions.coroutine; +using GFramework.Core.coroutine; +using GFramework.Core.coroutine.instructions; + +namespace GFramework.Core.Tests.coroutine; + +/// +/// 协程分组管理测试 +/// +public sealed class CoroutineGroupTests +{ + [Test] + public void Run_WithGroup_ShouldAddToGroup() + { + // Arrange + var timeSource = new FakeTimeSource(); + var scheduler = new CoroutineScheduler(timeSource); + + IEnumerator 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 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 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 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 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 Group1Coroutine() + { + while (true) + { + group1Count++; + yield return new WaitOneFrame(); + } + } + + IEnumerator 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次) + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/coroutine/CoroutinePriorityTests.cs b/GFramework.Core.Tests/coroutine/CoroutinePriorityTests.cs new file mode 100644 index 0000000..f64a6fb --- /dev/null +++ b/GFramework.Core.Tests/coroutine/CoroutinePriorityTests.cs @@ -0,0 +1,133 @@ +using GFramework.Core.Abstractions.coroutine; +using GFramework.Core.coroutine; +using GFramework.Core.coroutine.instructions; + +namespace GFramework.Core.Tests.coroutine; + +/// +/// 协程优先级测试 +/// +public sealed class CoroutinePriorityTests +{ + [Test] + public void Run_WithPriority_ShouldExecuteInPriorityOrder() + { + // Arrange + var timeSource = new FakeTimeSource(); + var scheduler = new CoroutineScheduler(timeSource); + var executionOrder = new List(); + + IEnumerator 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(); + + IEnumerator 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 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(); + + IEnumerator 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")); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/coroutine/CoroutineStatisticsTests.cs b/GFramework.Core.Tests/coroutine/CoroutineStatisticsTests.cs new file mode 100644 index 0000000..bd24e1c --- /dev/null +++ b/GFramework.Core.Tests/coroutine/CoroutineStatisticsTests.cs @@ -0,0 +1,351 @@ +using GFramework.Core.Abstractions.coroutine; +using GFramework.Core.coroutine; +using GFramework.Core.coroutine.instructions; + +namespace GFramework.Core.Tests.coroutine; + +/// +/// 协程统计功能测试 +/// +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 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 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 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 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 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 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 ShortCoroutine() + { + yield return new WaitOneFrame(); + } + + IEnumerator 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 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 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 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 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 TestCoroutine() + { + yield break; + } + + // Act - 并发读取统计信息 + var tasks = new List(); + 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)); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/coroutine/FakeTimeSource.cs b/GFramework.Core.Tests/coroutine/FakeTimeSource.cs new file mode 100644 index 0000000..95518af --- /dev/null +++ b/GFramework.Core.Tests/coroutine/FakeTimeSource.cs @@ -0,0 +1,47 @@ +using GFramework.Core.Abstractions.coroutine; + +namespace GFramework.Core.Tests.coroutine; + +/// +/// 可控制的时间源,用于协程测试 +/// +public sealed class FakeTimeSource : ITimeSource +{ + /// + /// 获取当前累计时间 + /// + public double CurrentTime { get; private set; } + + /// + /// 获取上一帧的时间增量 + /// + public double DeltaTime { get; private set; } + + /// + /// 更新时间源 + /// + public void Update() + { + // 在测试中,Update 不做任何事情 + // 时间推进由 Advance 方法控制 + } + + /// + /// 前进指定的时间 + /// + /// 时间增量(秒) + public void Advance(double deltaTime) + { + DeltaTime = deltaTime; + CurrentTime += deltaTime; + } + + /// + /// 重置时间源到初始状态 + /// + public void Reset() + { + CurrentTime = 0; + DeltaTime = 0; + } +} \ No newline at end of file diff --git a/GFramework.Core/coroutine/CoroutineMetadata.cs b/GFramework.Core/coroutine/CoroutineMetadata.cs index 47e0fe4..19c0f78 100644 --- a/GFramework.Core/coroutine/CoroutineMetadata.cs +++ b/GFramework.Core/coroutine/CoroutineMetadata.cs @@ -7,11 +7,26 @@ namespace GFramework.Core.coroutine; /// internal class CoroutineMetadata { + /// + /// 协程的分组标识符,用于批量管理协程 + /// + public string? Group; + + /// + /// 协程的优先级 + /// + public CoroutinePriority Priority; + /// /// 协程在调度器中的槽位索引 /// public int SlotIndex; + /// + /// 协程开始执行的时间戳(毫秒) + /// + public double StartTime; + /// /// 协程当前的执行状态 /// diff --git a/GFramework.Core/coroutine/CoroutineScheduler.cs b/GFramework.Core/coroutine/CoroutineScheduler.cs index 6b9d7f2..698e8e3 100644 --- a/GFramework.Core/coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/coroutine/CoroutineScheduler.cs @@ -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; /// 时间源接口,提供时间相关数据 /// 实例ID,默认为1 /// 初始容量,默认为256 +/// 是否启用统计功能,默认为false public sealed class CoroutineScheduler( ITimeSource timeSource, byte instanceId = 1, - int initialCapacity = 256) + int initialCapacity = 256, + bool enableStatistics = false) { + private readonly Dictionary> _grouped = new(); private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler)); private readonly Dictionary _metadata = new(); + private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null; private readonly Dictionary> _tagged = new(); private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource)); private readonly Dictionary> _waiting = new(); @@ -36,6 +40,11 @@ public sealed class CoroutineScheduler( /// public int ActiveCoroutineCount { get; private set; } + /// + /// 获取协程统计信息(如果启用) + /// + public ICoroutineStatistics? Statistics => _statistics; + /// /// 协程异常处理回调,当协程执行过程中发生异常时触发 /// 注意:事件处理程序会在独立任务中异步调用,以避免阻塞调度器主循环 @@ -61,10 +70,14 @@ public sealed class CoroutineScheduler( /// /// 要运行的协程枚举器 /// 协程标签,可选 + /// 协程优先级,默认为Normal + /// 协程分组,可选 /// 协程句柄 public CoroutineHandle Run( IEnumerator? 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(_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 + + /// + /// 暂停指定分组的所有协程 + /// + /// 分组名称 + /// 被暂停的协程数量 + public int PauseGroup(string group) + { + if (!_grouped.TryGetValue(group, out var handles)) + return 0; + + return handles.Count(Pause); + } + + /// + /// 恢复指定分组的所有协程 + /// + /// 分组名称 + /// 被恢复的协程数量 + public int ResumeGroup(string group) + { + if (!_grouped.TryGetValue(group, out var handles)) + return 0; + + return handles.Count(Resume); + } + + /// + /// 终止指定分组的所有协程 + /// + /// 分组名称 + /// 被终止的协程数量 + public int KillGroup(string group) + { + if (!_grouped.TryGetValue(group, out var handles)) + return 0; + + return handles.Count(Kill); + } + + /// + /// 获取指定分组的协程数量 + /// + /// 分组名称 + /// 协程数量 + public int GetGroupCount(string group) + { + return _grouped.TryGetValue(group, out var handles) ? handles.Count : 0; + } + + #endregion + #region Wait / Tag / Clear /// @@ -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); } /// @@ -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; } + /// + /// 为协程添加分组 + /// + /// 分组名称 + /// 协程句柄 + private void AddGroup(string group, CoroutineHandle handle) + { + if (!_grouped.TryGetValue(group, out var set)) + { + set = new HashSet(); + _grouped[group] = set; + } + + set.Add(handle); + _metadata[handle].Group = group; + } + + /// + /// 移除协程分组 + /// + /// 协程句柄 + 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 } \ No newline at end of file diff --git a/GFramework.Core/coroutine/CoroutineSlot.cs b/GFramework.Core/coroutine/CoroutineSlot.cs index 208c73a..4086052 100644 --- a/GFramework.Core/coroutine/CoroutineSlot.cs +++ b/GFramework.Core/coroutine/CoroutineSlot.cs @@ -17,6 +17,16 @@ internal sealed class CoroutineSlot /// public CoroutineHandle Handle; + /// + /// 协程是否已经开始执行 + /// + public bool HasStarted; + + /// + /// 协程的优先级 + /// + public CoroutinePriority Priority; + /// /// 协程当前状态 /// diff --git a/GFramework.Core/coroutine/CoroutineStatistics.cs b/GFramework.Core/coroutine/CoroutineStatistics.cs new file mode 100644 index 0000000..1807283 --- /dev/null +++ b/GFramework.Core/coroutine/CoroutineStatistics.cs @@ -0,0 +1,205 @@ +using System.Text; +using GFramework.Core.Abstractions.coroutine; + +namespace GFramework.Core.coroutine; + +/// +/// 协程统计信息实现类 +/// 线程安全:使用 Interlocked 操作确保计数器的原子性 +/// +internal sealed class CoroutineStatistics : ICoroutineStatistics +{ + private readonly Dictionary _countByPriority = new(); + private readonly Dictionary _countByTag = new(); + private readonly object _lock = new(); + private double _maxExecutionTimeMs; + private long _totalCompleted; + private long _totalExecutionTimeMs; + private long _totalFailed; + private long _totalStarted; + + /// + public long TotalStarted => Interlocked.Read(ref _totalStarted); + + /// + public long TotalCompleted => Interlocked.Read(ref _totalCompleted); + + /// + public long TotalFailed => Interlocked.Read(ref _totalFailed); + + /// + public int ActiveCount { get; set; } + + /// + public int PausedCount { get; set; } + + /// + 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; + } + } + + /// + public double MaxExecutionTimeMs + { + get + { + lock (_lock) + { + return _maxExecutionTimeMs; + } + } + } + + /// + public int GetCountByPriority(CoroutinePriority priority) + { + lock (_lock) + { + return _countByPriority.TryGetValue(priority, out var count) ? count : 0; + } + } + + /// + public int GetCountByTag(string tag) + { + lock (_lock) + { + return _countByTag.TryGetValue(tag, out var count) ? count : 0; + } + } + + /// + 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; + } + } + + /// + 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(); + } + + /// + /// 记录协程启动 + /// + /// 协程优先级 + /// 协程标签 + 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; + } + } + } + + /// + /// 记录协程完成 + /// + /// 执行时间(毫秒) + /// 协程优先级 + /// 协程标签 + 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); + } + } + } + + /// + /// 记录协程失败 + /// + /// 协程优先级 + /// 协程标签 + 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); + } + } + } +} \ No newline at end of file