diff --git a/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs b/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs index cc47484f6..3665bc492 100644 --- a/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs +++ b/managed/CounterStrikeSharp.Tests.Native/FrameSchedulingTests.cs @@ -1,3 +1,5 @@ +using System.Collections.Concurrent; +using System.Threading; using System.Threading.Tasks; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; @@ -95,4 +97,50 @@ public async Task NextWorldUpdate_HighLevelApi_ExecutesCallback() Assert.True(called, "NextWorldUpdate callback should have been called"); } + + [Fact] + public async Task NextFrameConcurrentQueueDrainsProperly() + { + int callCount = 0; + int targetCalls = 4096; + var callsByFrame = new ConcurrentDictionary(); + for (int i = 0; i < targetCalls; i++) + { + Server.NextFrame(() => + { + callsByFrame.AddOrUpdate(Server.TickCount, 1, (_, count) => count + 1); + Interlocked.Increment(ref callCount); + }); + } + + // All tasks should have been drained by latest NextFrameAsync + await Server.NextFrameAsync(() => { }).ConfigureAwait(false); + + // The task should be bucketed into multiple frames + Assert.All(callsByFrame.Values, count => Assert.Equal(1024, count)); + Assert.Equal(4096, callCount); + } + + [Fact] + public async Task NextWorldUpdateConcurrentQueueDrainsProperly() + { + int callCount = 0; + int targetCalls = 4096; + var callsByFrame = new ConcurrentDictionary(); + for (int i = 0; i < targetCalls; i++) + { + Server.NextWorldUpdate(() => + { + callsByFrame.AddOrUpdate(Server.TickCount, 1, (_, count) => count + 1); + Interlocked.Increment(ref callCount); + }); + } + + // All tasks should have been drained by latest NextFrameAsync + await Server.NextWorldUpdateAsync(() => { }).ConfigureAwait(false); + + // The task should be bucketed into multiple frames + Assert.All(callsByFrame.Values, count => Assert.Equal(1024, count)); + Assert.Equal(4096, callCount); + } } diff --git a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs index 99c2c0850..2e9a211dc 100644 --- a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs +++ b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs @@ -44,7 +44,7 @@ public override void Load(bool hotReload) { gameThreadId = Thread.CurrentThread.ManagedThreadId; // Loading blocks the game thread, so we use NextFrame to run our tests asynchronously. - Server.NextFrame(() => RunTests()); + Server.NextWorldUpdate(() => RunTests()); AddCommand("css_run_tests", "Runs the xUnit tests for the native plugin.", (player, info) => { RunTests(); }); } diff --git a/src/core/managers/server_manager.cpp b/src/core/managers/server_manager.cpp index b5a90ddfb..b399d8cf6 100644 --- a/src/core/managers/server_manager.cpp +++ b/src/core/managers/server_manager.cpp @@ -200,7 +200,11 @@ void ServerManager::PreWorldUpdate(bool bSimulating) void ServerManager::AddTaskForNextWorldUpdate(std::function&& task) { - m_nextWorldUpdateTasks.enqueue(std::forward(task)); + auto success = m_nextWorldUpdateTasks.enqueue(std::forward(task)); + if (!success) + { + CSSHARP_CORE_ERROR("Failed to enqueue task for next world update!"); + } } void ServerManager::OnPrecacheResources(IEntityResourceManifest* pResourceManifest) diff --git a/src/core/managers/server_manager.h b/src/core/managers/server_manager.h index 16dec55a7..11b956588 100644 --- a/src/core/managers/server_manager.h +++ b/src/core/managers/server_manager.h @@ -60,7 +60,7 @@ class ServerManager : public GlobalClass ScriptCallback* on_server_precache_resources; - moodycamel::ConcurrentQueue> m_nextWorldUpdateTasks; + moodycamel::ConcurrentQueue> m_nextWorldUpdateTasks{ 4096 }; }; } // namespace counterstrikesharp diff --git a/src/mm_plugin.cpp b/src/mm_plugin.cpp index 182ed648b..f30eee495 100644 --- a/src/mm_plugin.cpp +++ b/src/mm_plugin.cpp @@ -249,7 +249,11 @@ void CounterStrikeSharpMMPlugin::AllPluginsLoaded() void CounterStrikeSharpMMPlugin::AddTaskForNextFrame(std::function&& task) { - m_nextTasks.try_enqueue(std::forward(task)); + auto success = m_nextTasks.enqueue(std::forward(task)); + if (!success) + { + CSSHARP_CORE_ERROR("Failed to enqueue task for next frame!"); + } } void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick, bool bLastTick) diff --git a/src/mm_plugin.h b/src/mm_plugin.h index 27e9be155..925884b67 100644 --- a/src/mm_plugin.h +++ b/src/mm_plugin.h @@ -66,7 +66,7 @@ class CounterStrikeSharpMMPlugin : public ISmmPlugin, public IMetamodListener const char* GetLogTag() override; private: - moodycamel::ConcurrentQueue> m_nextTasks; + moodycamel::ConcurrentQueue> m_nextTasks{ 4096 }; }; static ScriptCallback* on_activate_callback;