From db7ddba735b79829a0832340f0328c13134e938a Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 30 Jul 2020 08:11:07 +0900 Subject: [PATCH] Add UniTaskSynchronizationContext, PlayerLoopHelper.IsInjectedUniTaskPlayerLoop, DumpCurrentPlayerLoop --- README.md | 27 +++ .../UniTask/Runtime/PlayerLoopHelper.cs | 81 +++++++++ .../Runtime/UniTaskSynchronizationContext.cs | 158 ++++++++++++++++++ .../UniTaskSynchronizationContext.cs.meta | 11 ++ 4 files changed, 277 insertions(+) create mode 100644 src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs create mode 100644 src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs.meta diff --git a/README.md b/README.md index de9f4db..27ed469 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,16 @@ var playerLoop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop; PlayerLoopHelper.Initialize(ref playerLoop); ``` +You can diagnostic UniTask's player loop is ready by `PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()`. And also `PlayerLoopHelper.DumpCurrentPlayerLoop` shows current all playerloop to console. + +```csharp +void Start() +{ + UnityEngine.Debug.Log("UniTaskPlayerLoop ready? " + PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()); + PlayerLoopHelper.DumpCurrentPlayerLoop(); +} +``` + async void vs async UniTaskVoid --- `async void` is a standard C# task system so does not run on UniTask systems. It is better not to use. `async UniTaskVoid` is a lightweight version of `async UniTask` because it does not have awaitable completion and report error immediately to `UniTaskScheduler.UnobservedTaskException`. If you don't require to await it(fire and forget), use `UniTaskVoid` is better. Unfortunately to dismiss warning, require to using with `Forget()`. @@ -826,6 +836,23 @@ foreach (var (type, size) in TaskPool.GetCacheSizeInfo()) > In UnityEditor profiler shows allocation of compiler generated AsyncStateMachine but it only occurs in debug(development) build. C# Compiler generate AsyncStateMachine as class on Debug build and as struct on Release build. +UniTaskSynchronizationContext +--- +Unity's default SynchronizationContext(`UnitySynchronizationContext`) is poor implementation for performance. UniTask itself is bypass `SynchronizationContext`(and `ExecutionContext`) so does not use it but if exists in `async Task`, still used it. `UniTaskSynchronizationContext` is replacement of `UnitySynchronizationContext`, it is better for performance. + +```csharp +public class SyncContextInjecter +{ + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + public static void Inject() + { + SynchronizationContext.SetSynchronizationContext(new UniTaskSynchronizationContext()); + } +} +``` + +This is an optional choice and is not always recommended; `UniTaskSynchronizationContext` is less performance than `async UniTask` and is not a complete UniTask replacement. It also does not guarantee full behavioral compatibility with the `UnitySynchronizationContext`. + API References --- UniTask's API References is hosted at [cysharp.github.io/UniTask](https://cysharp.github.io/UniTask/api/Cysharp.Threading.Tasks.html) by [DocFX](https://dotnet.github.io/docfx/) and [Cysharp/DocfXTemplate](https://github.com/Cysharp/DocfxTemplate). diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopHelper.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopHelper.cs index 18fa074..eccdb62 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopHelper.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopHelper.cs @@ -175,6 +175,32 @@ namespace Cysharp.Threading.Tasks return dest; } + static PlayerLoopSystem[] InsertUniTaskSynchronizationContext(PlayerLoopSystem loopSystem) + { + var loop = new PlayerLoopSystem + { + type = typeof(UniTaskSynchronizationContext), + updateDelegate = UniTaskSynchronizationContext.Run + }; + + // Remove items from previous initializations. + var source = loopSystem.subSystemList + .Where(ls => ls.type != typeof(UniTaskSynchronizationContext)) + .ToArray(); + + var dest = new System.Collections.Generic.List(source); + + var index = dest.FindIndex(x => x.type.Name == "ScriptRunDelayedTasks"); + if (index == -1) + { + index = dest.FindIndex(x => x.type.Name == "UniTaskLoopRunnerUpdate"); + } + + dest.Insert(index + 1, loop); + + return dest.ToArray(); + } + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void Init() { @@ -246,6 +272,8 @@ namespace Cysharp.Threading.Tasks if (item != null) item.Run(); } } + + UniTaskSynchronizationContext.Run(); } #endif @@ -293,6 +321,9 @@ namespace Cysharp.Threading.Tasks typeof(UniTaskLoopRunners.UniTaskLoopRunnerPostLateUpdate), runners[12] = new PlayerLoopRunner(PlayerLoopTiming.PostLateUpdate), typeof(UniTaskLoopRunners.UniTaskLoopRunnerLastPostLateUpdate), runners[13] = new PlayerLoopRunner(PlayerLoopTiming.LastPostLateUpdate)); + // Insert UniTaskSynchronizationContext to Update loop + copyList[4].subSystemList = InsertUniTaskSynchronizationContext(copyList[4]); + playerLoop.subSystemList = copyList; PlayerLoop.SetPlayerLoop(playerLoop); } @@ -306,6 +337,56 @@ namespace Cysharp.Threading.Tasks { yielders[(int)timing].Enqueue(continuation); } + + // Diagnostics helper + +#if UNITY_2019_3_OR_NEWER + + public static void DumpCurrentPlayerLoop() + { + var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop(); + + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"PlayerLoop List"); + foreach (var header in playerLoop.subSystemList) + { + sb.AppendFormat("------{0}------", header.type.Name); + sb.AppendLine(); + foreach (var subSystem in header.subSystemList) + { + sb.AppendFormat("{0}", subSystem.type.Name); + sb.AppendLine(); + + if (subSystem.subSystemList != null) + { + UnityEngine.Debug.LogWarning("More Subsystem:" + subSystem.subSystemList.Length); + } + } + } + + UnityEngine.Debug.Log(sb.ToString()); + } + + public static bool IsInjectedUniTaskPlayerLoop() + { + var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop(); + + foreach (var header in playerLoop.subSystemList) + { + foreach (var subSystem in header.subSystemList) + { + if (subSystem.type == typeof(UniTaskLoopRunners.UniTaskLoopRunnerInitialization)) + { + return true; + } + } + } + + return false; + } + +#endif + } } diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs new file mode 100644 index 0000000..ed58701 --- /dev/null +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs @@ -0,0 +1,158 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Cysharp.Threading.Tasks +{ + public class UniTaskSynchronizationContext : SynchronizationContext + { + const int MaxArrayLength = 0X7FEFFFFF; + const int InitialSize = 16; + + static SpinLock gate = new SpinLock(); + static bool dequing = false; + + static int actionListCount = 0; + static Callback[] actionList = new Callback[InitialSize]; + + static int waitingListCount = 0; + static Callback[] waitingList = new Callback[InitialSize]; + + static int opCount; + + public override void Send(SendOrPostCallback d, object state) + { + d(state); + } + + public override void Post(SendOrPostCallback d, object state) + { + bool lockTaken = false; + try + { + gate.Enter(ref lockTaken); + + if (dequing) + { + // Ensure Capacity + if (waitingList.Length == waitingListCount) + { + var newLength = waitingListCount * 2; + if ((uint)newLength > MaxArrayLength) newLength = MaxArrayLength; + + var newArray = new Callback[newLength]; + Array.Copy(waitingList, newArray, waitingListCount); + waitingList = newArray; + } + waitingList[waitingListCount] = new Callback(d, state); + waitingListCount++; + } + else + { + // Ensure Capacity + if (actionList.Length == actionListCount) + { + var newLength = actionListCount * 2; + if ((uint)newLength > MaxArrayLength) newLength = MaxArrayLength; + + var newArray = new Callback[newLength]; + Array.Copy(actionList, newArray, actionListCount); + actionList = newArray; + } + actionList[actionListCount] = new Callback(d, state); + actionListCount++; + } + } + finally + { + if (lockTaken) gate.Exit(false); + } + } + + public override void OperationStarted() + { + Interlocked.Increment(ref opCount); + } + + public override void OperationCompleted() + { + Interlocked.Decrement(ref opCount); + } + + public override SynchronizationContext CreateCopy() + { + return this; + } + + // delegate entrypoint. + internal static void Run() + { + { + bool lockTaken = false; + try + { + gate.Enter(ref lockTaken); + if (actionListCount == 0) return; + dequing = true; + } + finally + { + if (lockTaken) gate.Exit(false); + } + } + + for (int i = 0; i < actionListCount; i++) + { + var action = actionList[i]; + actionList[i] = default; + action.Invoke(); + } + + { + bool lockTaken = false; + try + { + gate.Enter(ref lockTaken); + dequing = false; + + var swapTempActionList = actionList; + + actionListCount = waitingListCount; + actionList = waitingList; + + waitingListCount = 0; + waitingList = swapTempActionList; + } + finally + { + if (lockTaken) gate.Exit(false); + } + } + } + + [StructLayout(LayoutKind.Auto)] + readonly struct Callback + { + readonly SendOrPostCallback callback; + readonly object state; + + public Callback(SendOrPostCallback callback, object state) + { + this.callback = callback; + this.state = state; + } + + public void Invoke() + { + try + { + callback(state); + } + catch (Exception ex) + { + UnityEngine.Debug.LogException(ex); + } + } + } + } +} \ No newline at end of file diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs.meta b/src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs.meta new file mode 100644 index 0000000..9828c89 --- /dev/null +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/UniTaskSynchronizationContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abf3aae9813db2849bce518f8596e920 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: