feat: use concurrent queue for next frame & world update tasks (#365)

This commit is contained in:
Michael Wilson
2024-03-06 21:08:22 +10:00
committed by GitHub
parent bd3c0c76e3
commit c3d44a87bc
10 changed files with 3914 additions and 135 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,7 @@ include_directories(
libraries/tl
libraries/funchook/include
libraries/DynoHook/src
libraries/moodycamel
libraries
)

View File

@@ -427,6 +427,12 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.FunctionReference.Create(System.Delegate)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.NativeAPI.QueueTaskForNextFrame(System.IntPtr)</Target>

View File

@@ -14,11 +14,9 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -30,140 +28,149 @@ namespace CounterStrikeSharp.API.Core
{
/// <summary>Delegate will be removed after the first invocation.</summary>
SingleUse,
/// <summary>Delegate will remain in memory for the lifetime of the application.</summary>
/// <summary>Delegate will remain in memory for the lifetime of the application (or until <see cref="FunctionReference.Remove"/> is called).</summary>
Permanent
}
/// <summary>
/// Represents a reference to a function that can be called from native code.
/// </summary>
public class FunctionReference
{
private readonly Delegate m_method;
public FunctionLifetime Lifetime { get; set; } = FunctionLifetime.Permanent;
public unsafe delegate void CallbackDelegate(fxScriptContext* context);
private CallbackDelegate s_callback;
private FunctionReference(Delegate method)
private static readonly ConcurrentDictionary<int, FunctionReference> IdToFunctionReferencesMap = new();
private static readonly ConcurrentDictionary<Delegate, FunctionReference> TargetMethodToFunctionReferencesMap = new();
private static readonly object ReferenceCounterLock = new();
private static int _referenceCounter;
private readonly Delegate _targetMethod;
private readonly CallbackDelegate _nativeCallback;
private readonly TaskCompletionSource _taskCompletionSource = new();
private FunctionReference(Delegate method, FunctionLifetime lifetime)
{
m_method = method;
unsafe
{
var dg = new CallbackDelegate((fxScriptContext* context) =>
{
try
{
var scriptContext = new ScriptContext(context);
if (method.Method.GetParameters().FirstOrDefault()?.ParameterType == typeof(ScriptContext))
{
var returnO = m_method.DynamicInvoke(scriptContext);
if (returnO != null)
{
scriptContext.SetResult(returnO, context);
}
return;
}
var paramsList = method.Method.GetParameters().Select((x, i) =>
{
var param = method.Method.GetParameters()[i];
object obj = null;
if (typeof(NativeObject).IsAssignableFrom(param.ParameterType))
{
obj = Activator.CreateInstance(param.ParameterType,
new[] { scriptContext.GetArgument(typeof(IntPtr), i) });
}
else
{
obj = scriptContext.GetArgument(param.ParameterType, i);
}
return obj;
}).ToArray();
var returnObj = m_method.DynamicInvoke(paramsList);
if (returnObj != null)
{
scriptContext.SetResult(returnObj, context);
}
}
catch (Exception e)
{
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally
{
if (Lifetime == FunctionLifetime.SingleUse)
{
Remove(Identifier);
if (references.ContainsKey(m_method))
references.Remove(m_method, out _);
}
}
});
s_callback = dg;
}
Lifetime = lifetime;
_targetMethod = method;
_nativeCallback = CreateWrappedCallback();
}
/// <summary>
/// <inheritdoc cref="FunctionLifetime"/>
/// </summary>
public FunctionLifetime Lifetime { get; }
/// <summary>
/// For <see cref="FunctionLifetime.SingleUse"/> function references, this task will complete when
/// the function has finished invoking.
/// </summary>
public Task CompletionTask => _taskCompletionSource.Task;
public int Identifier { get; private set; }
public static FunctionReference Create(Delegate method)
private unsafe CallbackDelegate CreateWrappedCallback()
{
if (references.TryGetValue(method, out var existingReference))
return context =>
{
try
{
var scriptContext = new ScriptContext(context);
// Allow for manual handling of the script context
if (_targetMethod.Method.GetParameters().FirstOrDefault()?.ParameterType == typeof(ScriptContext))
{
var returnValue = _targetMethod.DynamicInvoke(scriptContext);
if (returnValue != null)
{
scriptContext.SetResult(returnValue, context);
}
return;
}
var parameterList = _targetMethod.Method.GetParameters().Select((_, i) =>
{
var parameter = _targetMethod.Method.GetParameters()[i];
return scriptContext.GetArgument(parameter.ParameterType, i);
}).ToArray();
var returnObj = _targetMethod.DynamicInvoke(parameterList);
if (returnObj != null)
{
scriptContext.SetResult(returnObj, context);
}
}
catch (Exception e)
{
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally
{
if (Lifetime == FunctionLifetime.SingleUse)
{
RemoveSelf();
}
_taskCompletionSource.TrySetResult();
}
};
}
public static FunctionReference Create(Delegate method, FunctionLifetime lifetime = FunctionLifetime.Permanent)
{
// We always want to create a new reference if the lifetime is single use.
if (lifetime == FunctionLifetime.Permanent && TargetMethodToFunctionReferencesMap.TryGetValue(method, out var existingReference))
{
return existingReference;
}
var reference = new FunctionReference(method);
var reference = new FunctionReference(method, lifetime);
var referenceId = Register(reference);
reference.Identifier = referenceId;
return reference;
}
private static ConcurrentDictionary<int, FunctionReference> ms_references = new ConcurrentDictionary<int, FunctionReference>();
private static int ms_referenceId;
private static ConcurrentDictionary<Delegate, FunctionReference> references =
new ConcurrentDictionary<Delegate, FunctionReference>();
private static int Register(FunctionReference reference)
{
var thisRefId = ms_referenceId;
ms_references[thisRefId] = reference;
references[reference.m_method] = reference;
unchecked { ms_referenceId++; }
return thisRefId;
}
public static FunctionReference Get(int reference)
{
if (ms_references.ContainsKey(reference))
lock (ReferenceCounterLock)
{
return ms_references[reference];
}
var thisRefId = _referenceCounter;
IdToFunctionReferencesMap[thisRefId] = reference;
TargetMethodToFunctionReferencesMap[reference._targetMethod] = reference;
return null;
unchecked
{
_referenceCounter++;
}
return thisRefId;
}
}
public IntPtr GetFunctionPointer()
public IntPtr GetFunctionPointer() => Marshal.GetFunctionPointerForDelegate(_nativeCallback);
private void RemoveSelf()
{
IntPtr cb = Marshal.GetFunctionPointerForDelegate(s_callback);
return cb;
Remove(Identifier);
}
public static void Remove(int reference)
{
if (ms_references.TryGetValue(reference, out var funcRef))
if (IdToFunctionReferencesMap.TryGetValue(reference, out var functionReference))
{
ms_references.Remove(reference, out _);
if (TargetMethodToFunctionReferencesMap.ContainsKey(functionReference._targetMethod))
{
TargetMethodToFunctionReferencesMap.Remove(functionReference._targetMethod, out _);
}
IdToFunctionReferencesMap.Remove(reference, out _);
Application.Instance.Logger.LogDebug("Removing function/callback reference: {Reference}", reference);
}

View File

@@ -1,5 +1,7 @@
// Global using directives
global using System;
global using System.Linq;
global using System.IO;
global using System.Collections.Generic;
global using CounterStrikeSharp.API.Core;

View File

@@ -19,6 +19,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -41,15 +42,35 @@ namespace CounterStrikeSharp.API
public static double EngineTime => NativeAPI.GetEngineTime();
public static void PrecacheModel(string name) => NativeAPI.PrecacheModel(name);
/// <summary>
/// <inheritdoc cref="NextFrame"/>
/// Returns Task that completes once the synchronous task has been completed.
/// </summary>
public static Task NextFrameAsync(Action task)
{
var functionReference = FunctionReference.Create(task, FunctionLifetime.SingleUse);
NativeAPI.QueueTaskForNextFrame(functionReference);
return functionReference.CompletionTask;
}
/// <summary>
/// Queue a task to be executed on the next game frame.
/// <remarks>Does not execute if the server is hibernating.</remarks>
/// </summary>
public static void NextFrame(Action task)
{
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextFrame(functionReference);
NextFrameAsync(task);
}
/// <summary>
/// <inheritdoc cref="NextWorldUpdate"/>
/// Returns Task that completes once the synchronous task has been completed.
/// </summary>
public static Task NextWorldUpdateAsync(Action task)
{
var functionReference = FunctionReference.Create(task, FunctionLifetime.SingleUse);
NativeAPI.QueueTaskForNextWorldUpdate(functionReference);
return functionReference.CompletionTask;
}
/// <summary>
@@ -59,9 +80,7 @@ namespace CounterStrikeSharp.API
/// <param name="task"></param>
public static void NextWorldUpdate(Action task)
{
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextWorldUpdate(functionReference);
NextWorldUpdateAsync(task);
}
public static void PrintToChatAll(string message)

View File

@@ -20,6 +20,7 @@
#include "scripting/callback_manager.h"
#include "core/game_system.h"
#include <concurrentqueue.h>
SH_DECL_HOOK1_void(ISource2Server, ServerHibernationUpdate, SH_NOATTRIB, 0, bool);
SH_DECL_HOOK0_void(ISource2Server, GameServerSteamAPIActivated, SH_NOATTRIB, 0);
@@ -176,17 +177,17 @@ void ServerManager::UpdateWhenNotInGame(float flFrameTime)
void ServerManager::PreWorldUpdate(bool bSimulating)
{
std::lock_guard<std::mutex> lock(m_nextWorldUpdateTasksLock);
std::vector<std::function<void()>> out_list(1024);
if (!m_nextWorldUpdateTasks.empty()) {
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} at time {1}", m_nextWorldUpdateTasks.size(),
globals::getGlobalVars()->curtime);
auto size = m_nextWorldUpdateTasks.try_dequeue_bulk(out_list.begin(), 1024);
for (size_t i = 0; i < m_nextWorldUpdateTasks.size(); i++) {
m_nextWorldUpdateTasks[i]();
if (size > 0) {
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} at time {1}", size,
globals::getGlobalVars()->curtime);
for (size_t i = 0; i < size; i++) {
out_list[i]();
}
m_nextWorldUpdateTasks.clear();
}
auto callback = globals::serverManager.on_server_pre_world_update;
@@ -200,8 +201,7 @@ void ServerManager::PreWorldUpdate(bool bSimulating)
void ServerManager::AddTaskForNextWorldUpdate(std::function<void()>&& task)
{
std::lock_guard<std::mutex> lock(m_nextWorldUpdateTasksLock);
m_nextWorldUpdateTasks.push_back(std::forward<decltype(task)>(task));
m_nextWorldUpdateTasks.enqueue(std::forward<decltype(task)>(task));
}
void ServerManager::OnPrecacheResources(IEntityResourceManifest* pResourceManifest)

View File

@@ -19,6 +19,7 @@
#include "core/globals.h"
#include "core/global_listener.h"
#include "scripting/script_engine.h"
#include <concurrentqueue.h>
#include "core/game_system.h"
@@ -56,8 +57,7 @@ private:
ScriptCallback *on_server_precache_resources;
std::vector<std::function<void()>> m_nextWorldUpdateTasks;
std::mutex m_nextWorldUpdateTasksLock;
moodycamel::ConcurrentQueue<std::function<void()>> m_nextWorldUpdateTasks;
};
} // namespace counterstrikesharp

View File

@@ -193,9 +193,7 @@ void CounterStrikeSharpMMPlugin::AllPluginsLoaded()
void CounterStrikeSharpMMPlugin::AddTaskForNextFrame(std::function<void()>&& task)
{
std::lock_guard<std::mutex> lock(m_nextTasksLock);
m_nextTasks.push_back(std::forward<decltype(task)>(task));
m_nextTasks.try_enqueue(std::forward<decltype(task)>(task));
}
void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick, bool bLastTick)
@@ -208,19 +206,18 @@ void CounterStrikeSharpMMPlugin::Hook_GameFrame(bool simulating, bool bFirstTick
*/
globals::timerSystem.OnGameFrame(simulating);
std::lock_guard<std::mutex> lock(m_nextTasksLock);
std::vector<std::function<void()>> out_list(1024);
if (m_nextTasks.empty())
return;
auto size = m_nextTasks.try_dequeue_bulk(out_list.begin(), 1024);
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} on tick number {1}", m_nextTasks.size(),
globals::getGlobalVars()->tickcount);
if (size > 0) {
CSSHARP_CORE_TRACE("Executing queued tasks of size: {0} on tick number {1}", size,
globals::getGlobalVars()->tickcount);
for (size_t i = 0; i < m_nextTasks.size(); i++) {
m_nextTasks[i]();
for (size_t i = 0; i < size; i++) {
out_list[i]();
}
}
m_nextTasks.clear();
}
// Potentially might not work

View File

@@ -23,6 +23,7 @@
#include <sh_vector.h>
#include <vector>
#include "entitysystem.h"
#include "concurrentqueue.h"
namespace counterstrikesharp {
class ScriptCallback;
@@ -63,8 +64,7 @@ public:
const char *GetLogTag() override;
private:
std::vector<std::function<void()>> m_nextTasks;
std::mutex m_nextTasksLock;
moodycamel::ConcurrentQueue<std::function<void()>> m_nextTasks;
};
static ScriptCallback *on_activate_callback;