Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(scripting/csharp): experimental SynchronizationContext (on serve…
…r) for `async void` event/refs

Enable using `clr_experimental_2021_12_31_use_sync_context 'on'` in fxmanifest.lua while in preview.

Please test this with a few third-party libs using Task stuff for networking.
  • Loading branch information
blattersturm committed Dec 31, 2021
1 parent d57cf36 commit f78dc31
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 76 deletions.
2 changes: 1 addition & 1 deletion code/client/clrcore/BaseScript.cs
Expand Up @@ -210,7 +210,7 @@ private void ScheduleTick(TickHandler callWrap)
/// <returns>An awaitable task.</returns>
public static Task Delay(int msecs)
{
return CitizenTaskScheduler.Factory.FromAsync(BeginDelay, EndDelay, msecs, CitizenTaskScheduler.Instance);
return CitizenTaskScheduler.Factory.FromAsync(BeginDelay, EndDelay, msecs, null);

This comment has been minimized.

Copy link
@bladecoding

bladecoding Dec 31, 2021

Contributor

The scheduler should only be null when clr_experimental_2021_12_31_use_sync_context is enabled otherwise this change breaks old code.

You can test it with the following code.

[Command("xttp")]
public void HelloServer()
{
    using (var hc = new HttpClient()) {
        try {
            var s = CitizenFX.Core.Native.API.GetGameTimer();
            Debug.WriteLine($"start thread: {CitizenFX.Core.Native.API.GetGameTimer() - s} {Thread.CurrentThread.ManagedThreadId}");
            Task.Run(async () => {
                Debug.WriteLine($"background: {CitizenFX.Core.Native.API.GetGameTimer() - s} {Thread.CurrentThread.ManagedThreadId}");
                await Delay(0);
                Debug.WriteLine($"background continued: {CitizenFX.Core.Native.API.GetGameTimer() - s} {Thread.CurrentThread.ManagedThreadId}");
            });
            Debug.WriteLine($"continued: {CitizenFX.Core.Native.API.GetGameTimer() - s} {Thread.CurrentThread.ManagedThreadId}");
        } catch (Exception e) {
            Debug.WriteLine(e.ToString());
        }
    }
}

5104 fxserver

[         script:meow] start thread: 0 3
[         script:meow] continued: 5 3
[         script:meow] background: 16 7
[         script:meow] background continued: 100 3

5149 fxserver clr_experimental_2021_12_31_use_sync_context 'no'

[         script:meow] start thread: 0 3
[         script:meow] continued: 5 3
[         script:meow] background: 18 6
[         script:meow] background continued: 104 7

This comment has been minimized.

Copy link
@blattersturm

blattersturm Jan 1, 2022

Author Collaborator

This argument wasn't ever setting the scheduler, this was misleading and actually was the state argument, which is typed object, so didn't lead to a compile-time error.

Specifying no doesn't make any difference either as it's the presence that counts: 'no' is the same as 'yes'.

}

[SecuritySafeCritical]
Expand Down
31 changes: 19 additions & 12 deletions code/client/clrcore/CitizenTaskScheduler.cs
Expand Up @@ -26,25 +26,32 @@ public static void Tick()
{
var flowBlock = CitizenTaskScheduler.SuppressFlow();

Action[] tasks;

lock (m_scheduledTasks)
try
{
tasks = m_scheduledTasks.ToArray();
m_scheduledTasks.Clear();
}
Action[] tasks;

foreach (var task in tasks)
{
try
lock (m_scheduledTasks)
{
task();
tasks = m_scheduledTasks.ToArray();
m_scheduledTasks.Clear();
}
catch (Exception e)

foreach (var task in tasks)
{
Debug.WriteLine($"Exception during executing Post callback: {e}");
try
{
task();
}
catch (Exception e)
{
InternalManager.PrintErrorInternal($"task continuation", e);
}
}
}
finally
{

}

flowBlock?.Undo();
}
Expand Down
173 changes: 110 additions & 63 deletions code/client/clrcore/InternalManager.cs
Expand Up @@ -19,6 +19,7 @@ class InternalManager : MarshalByRefObject, InternalManagerInterface
private static readonly List<BaseScript> ms_definedScripts = new List<BaseScript>();
private static readonly List<Tuple<DateTime, AsyncCallback, string>> ms_delays = new List<Tuple<DateTime, AsyncCallback, string>>();
private static int ms_instanceId;
private static bool ms_useSyncContext;

private string m_resourceName;

Expand Down Expand Up @@ -70,6 +71,11 @@ public void SetResourceName(string resourceName)
m_resourceName = resourceName;
}

public void SetConfiguration(bool useSyncContext)
{
ms_useSyncContext = useSyncContext;
}

[SecuritySafeCritical]
public void SetScriptHost(IScriptHost host, int instanceId)
{
Expand Down Expand Up @@ -366,60 +372,89 @@ public void Tick()
}
}

[SecuritySafeCritical]
public void TriggerEvent(string eventName, byte[] argsSerialized, string sourceString)
private class SyncContextScope : IDisposable
{
if (GameInterface.SnapshotStackBoundary(out var bo))
private static SynchronizationContext citizenContext = new CitizenSynchronizationContext();
private SynchronizationContext oldContext;

public SyncContextScope()
{
ScriptHost.SubmitBoundaryStart(bo, bo.Length);
if (ms_useSyncContext)
{
oldContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(citizenContext);
}
}

try
public void Dispose()
{
if (oldContext != null)
{
SynchronizationContext.SetSynchronizationContext(oldContext);
}
}
}

[SecuritySafeCritical]
public void TriggerEvent(string eventName, byte[] argsSerialized, string sourceString)
{
// not using using statements here as old Mono on Linux build doesn't know of these
#if IS_FXSERVER
var netSource = (sourceString.StartsWith("net") || sourceString.StartsWith("internal-net")) ? sourceString : null;
using (var syncContext = new SyncContextScope())
#endif
{
if (GameInterface.SnapshotStackBoundary(out var bo))
{
ScriptHost.SubmitBoundaryStart(bo, bo.Length);
}

try
{
#if IS_FXSERVER
var netSource = (sourceString.StartsWith("net") || sourceString.StartsWith("internal-net")) ? sourceString : null;
#else
var netSource = sourceString.StartsWith("net") ? sourceString : null;
var netSource = sourceString.StartsWith("net") ? sourceString : null;
#endif

var obj = MsgPackDeserializer.Deserialize(argsSerialized, netSource) as List<object> ?? (IEnumerable<object>)new object[0];
var obj = MsgPackDeserializer.Deserialize(argsSerialized, netSource) as List<object> ?? (IEnumerable<object>)new object[0];

var scripts = ms_definedScripts.ToArray();
var scripts = ms_definedScripts.ToArray();

var objArray = obj.ToArray();
var objArray = obj.ToArray();

NetworkFunctionManager.HandleEventTrigger(eventName, objArray, sourceString);
NetworkFunctionManager.HandleEventTrigger(eventName, objArray, sourceString);

foreach (var script in scripts)
{
Task.Factory.StartNew(() =>
foreach (var script in scripts)
{
BaseScript.CurrentName = $"eventHandler {script.GetType().Name} -> {eventName}";
var t = script.EventHandlers.Invoke(eventName, sourceString, objArray);
BaseScript.CurrentName = null;
Task.Factory.StartNew(() =>
{
BaseScript.CurrentName = $"eventHandler {script.GetType().Name} -> {eventName}";
var t = script.EventHandlers.Invoke(eventName, sourceString, objArray);
BaseScript.CurrentName = null;
return t;
}, CancellationToken.None, TaskCreationOptions.None, CitizenTaskScheduler.Instance).Unwrap().ContinueWith(a =>
{
if (a.IsFaulted)
return t;
}, CancellationToken.None, TaskCreationOptions.None, CitizenTaskScheduler.Instance).Unwrap().ContinueWith(a =>
{
Debug.WriteLine($"Error invoking event handlers for {eventName}: {a.Exception?.InnerExceptions.Aggregate("", (b, s) => s + b.ToString() + "\n")}");
}
});
}
if (a.IsFaulted)
{
Debug.WriteLine($"Error invoking event handlers for {eventName}: {a.Exception?.InnerExceptions.Aggregate("", (b, s) => s + b.ToString() + "\n")}");
}
});
}

ExportDictionary.Invoke(eventName, objArray);
ExportDictionary.Invoke(eventName, objArray);

// invoke a single task tick
CitizenTaskScheduler.Instance.Tick();
}
catch (Exception e)
{
PrintError($"event ({eventName})", e);
}
finally
{
ScriptHost.SubmitBoundaryStart(null, 0);
// invoke a single task tick
CitizenTaskScheduler.Instance.Tick();
}
catch (Exception e)
{
PrintError($"event ({eventName})", e);
}
finally
{
ScriptHost.SubmitBoundaryStart(null, 0);
}
}
}

Expand All @@ -440,46 +475,53 @@ public static string CanonicalizeRef(int refId)
[SecuritySafeCritical]
public void CallRef(int refIndex, byte[] argsSerialized, out IntPtr retvalSerialized, out int retvalSize)
{
if (GameInterface.SnapshotStackBoundary(out var b))
// not using using statements here as old Mono on Linux build doesn't know of these
#if IS_FXSERVER
using (var syncContext = new SyncContextScope())
#endif
{
ScriptHost.SubmitBoundaryStart(b, b.Length);
}

try
{
var retvalData = FunctionReference.Invoke(refIndex, argsSerialized);
if (GameInterface.SnapshotStackBoundary(out var b))
{
ScriptHost.SubmitBoundaryStart(b, b.Length);
}

if (retvalData != null)
try
{
if (m_retvalBuffer == IntPtr.Zero)
{
m_retvalBuffer = Marshal.AllocHGlobal(32768);
m_retvalBufferSize = 32768;
}
var retvalData = FunctionReference.Invoke(refIndex, argsSerialized);

if (m_retvalBufferSize < retvalData.Length)
if (retvalData != null)
{
m_retvalBuffer = Marshal.ReAllocHGlobal(m_retvalBuffer, new IntPtr(retvalData.Length));
m_retvalBufferSize = retvalData.Length;
}
if (m_retvalBuffer == IntPtr.Zero)
{
m_retvalBuffer = Marshal.AllocHGlobal(32768);
m_retvalBufferSize = 32768;
}

if (m_retvalBufferSize < retvalData.Length)
{
m_retvalBuffer = Marshal.ReAllocHGlobal(m_retvalBuffer, new IntPtr(retvalData.Length));
m_retvalBufferSize = retvalData.Length;
}

Marshal.Copy(retvalData, 0, m_retvalBuffer, retvalData.Length);
Marshal.Copy(retvalData, 0, m_retvalBuffer, retvalData.Length);

retvalSerialized = m_retvalBuffer;
retvalSize = retvalData.Length;
retvalSerialized = m_retvalBuffer;
retvalSize = retvalData.Length;
}
else
{
retvalSerialized = IntPtr.Zero;
retvalSize = 0;
}
}
else
catch (Exception e)
{
retvalSerialized = IntPtr.Zero;
retvalSize = 0;
}
}
catch (Exception e)
{
retvalSerialized = IntPtr.Zero;
retvalSize = 0;

PrintError($"reference call", e.InnerException ?? e);
PrintError($"reference call", e.InnerException ?? e);
}
}
}

Expand Down Expand Up @@ -521,6 +563,11 @@ public override object InitializeLifetimeService()
return null;
}

internal static void PrintErrorInternal(string where, Exception what)
{
GlobalManager?.PrintError(where, what);
}

[SecuritySafeCritical]
private void PrintError(string where, Exception what)
{
Expand Down
2 changes: 2 additions & 0 deletions code/client/clrcore/InternalManagerInterface.cs
Expand Up @@ -8,6 +8,8 @@ interface InternalManagerInterface

void CreateTaskScheduler();

void SetConfiguration(bool syncContext = false);

void Destroy();

void SetScriptHost(IScriptHost host, int instanceId);
Expand Down
4 changes: 4 additions & 0 deletions code/client/clrcore/MonoScriptRuntime.cs
Expand Up @@ -46,6 +46,7 @@ public void Create(IScriptHost host)
string resourceName = Marshal.PtrToStringAnsi(nameString);

bool useTaskScheduler = true;
bool useSyncContext = false;

#if IS_FXSERVER
string basePath = "";
Expand All @@ -56,6 +57,7 @@ public void Create(IScriptHost host)

basePath = Native.API.GetResourcePath(resourceName);
useTaskScheduler = Native.API.GetNumResourceMetadata(resourceName, "clr_disable_task_scheduler") == 0;
useSyncContext = Native.API.GetNumResourceMetadata(resourceName, "clr_experimental_2021_12_31_use_sync_context") > 0;

if (host is IScriptHostWithManifest manifestHost)
{
Expand Down Expand Up @@ -83,6 +85,8 @@ public void Create(IScriptHost host)
m_intManager.CreateTaskScheduler();
}

m_intManager.SetConfiguration(useSyncContext: useSyncContext);

m_intManager.SetResourceName(resourceName);

// TODO: figure out a cleaner solution to Mono JIT corruption so server doesn't have to be slower
Expand Down

0 comments on commit f78dc31

Please sign in to comment.