From e5e3a1c0caaf719a2a9be71f4d2cfad268df91a9 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 6 Mar 2026 12:34:12 +0800
Subject: [PATCH] =?UTF-8?q?feat(coroutine):=20=E6=B7=BB=E5=8A=A0=E5=8D=8F?=
=?UTF-8?q?=E7=A8=8B=E5=88=86=E7=BB=84=E7=AE=A1=E7=90=86=E5=92=8C=E4=BC=98?=
=?UTF-8?q?=E5=85=88=E7=BA=A7=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现协程分组功能,支持批量暂停、恢复和终止协程
- 添加协程优先级系统,支持从最低到最高的5个优先级级别
- 引入协程统计功能,跟踪启动、完成、失败数量及执行时间
- 添加FakeTimeSource用于协程测试的时间控制
- 实现按优先级排序的协程执行机制
- 添加协程执行时间戳记录功能
- 实现完整的协程统计报告生成功能
---
.../coroutine/CoroutinePriority.cs | 33 ++
.../coroutine/ICoroutineStatistics.cs | 68 ++++
GFramework.Core.Tests/GlobalUsings.cs | 4 +-
.../coroutine/CoroutineGroupTests.cs | 197 ++++++++++
.../coroutine/CoroutinePriorityTests.cs | 133 +++++++
.../coroutine/CoroutineStatisticsTests.cs | 351 ++++++++++++++++++
.../coroutine/FakeTimeSource.cs | 47 +++
.../coroutine/CoroutineMetadata.cs | 15 +
.../coroutine/CoroutineScheduler.cs | 174 ++++++++-
GFramework.Core/coroutine/CoroutineSlot.cs | 10 +
.../coroutine/CoroutineStatistics.cs | 205 ++++++++++
11 files changed, 1225 insertions(+), 12 deletions(-)
create mode 100644 GFramework.Core.Abstractions/coroutine/CoroutinePriority.cs
create mode 100644 GFramework.Core.Abstractions/coroutine/ICoroutineStatistics.cs
create mode 100644 GFramework.Core.Tests/coroutine/CoroutineGroupTests.cs
create mode 100644 GFramework.Core.Tests/coroutine/CoroutinePriorityTests.cs
create mode 100644 GFramework.Core.Tests/coroutine/CoroutineStatisticsTests.cs
create mode 100644 GFramework.Core.Tests/coroutine/FakeTimeSource.cs
create mode 100644 GFramework.Core/coroutine/CoroutineStatistics.cs
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