From b6a9836e81ff2f38a94e15820120ff40ed5f1684 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 11 Mar 2021 17:56:41 +0900 Subject: [PATCH 1/4] WIP, playerlooptimer --- .../CancellationTokenSourceExtensions.cs | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs index 87dcbe7..70fb10b 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs @@ -4,6 +4,7 @@ using System.Threading; using UnityEngine; using Cysharp.Threading.Tasks.Triggers; using System; +using Cysharp.Threading.Tasks.Internal; namespace Cysharp.Threading.Tasks { @@ -46,6 +47,203 @@ namespace Cysharp.Threading.Tasks cts2.Cancel(); }, cts); } + + // TODO:delete this. + public static IDisposable CancelAfterSlim2(this CancellationTokenSource cts, TimeSpan delayTimeSpan) + { + var timer = PlayerLoopTimer.Create(delayTimeSpan, DelayType.DeltaTime, PlayerLoopTiming.Update, cts.Token, state => + { + var x = (CancellationTokenSource)state; + x.Cancel(); + }, cts); + timer.Restart(); + + return timer; + } + + abstract class PlayerLoopTimer : IDisposable, IPlayerLoopItem + { + readonly CancellationToken cancellationToken; + readonly Action timerCallback; + readonly object state; + readonly PlayerLoopTiming playerLoopTiming; + + bool isPlaying; + bool isDisposed; + + protected PlayerLoopTimer(PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + this.playerLoopTiming = playerLoopTiming; + this.cancellationToken = cancellationToken; + this.timerCallback = timerCallback; + this.state = state; + } + + public static PlayerLoopTimer Create(TimeSpan delayTimeSpan, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + switch (delayType) + { + case DelayType.UnscaledDeltaTime: + return new IgnoreTimeScalePlayerLoopTimer(delayTimeSpan, playerLoopTiming, cancellationToken, timerCallback, state); + case DelayType.Realtime: + return new RealtimePlayerLoopTimer(delayTimeSpan, playerLoopTiming, cancellationToken, timerCallback, state); + case DelayType.DeltaTime: + default: + return new DeltaTimePlayerLoopTimer(delayTimeSpan, playerLoopTiming, cancellationToken, timerCallback, state); + } + } + + /// + /// Restart(Reset and Start) timer. + /// + public void Restart() + { + if (isDisposed) throw new ObjectDisposedException(null); + if (isPlaying) return; + + ResetCore(); // init state + isPlaying = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + + /// + /// Stop timer. + /// + public void Stop() + { + isPlaying = false; + } + + protected abstract void ResetCore(); + + public void Dispose() + { + isDisposed = true; + } + + bool IPlayerLoopItem.MoveNext() + { + if (isDisposed) return false; + if (!isPlaying) return false; + if (cancellationToken.IsCancellationRequested) return false; + + if (!MoveNextCore()) + { + timerCallback(state); + return false; + } + + return true; + } + + protected abstract bool MoveNextCore(); + } + + sealed class DeltaTimePlayerLoopTimer : PlayerLoopTimer + { + int initialFrame; + float elapsed; + readonly float delayTimeSpan; + + public DeltaTimePlayerLoopTimer(TimeSpan delayTimeSpan, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(playerLoopTiming, cancellationToken, timerCallback, state) + { + this.elapsed = 0.0f; + this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; + } + + protected override bool MoveNextCore() + { + if (elapsed == 0.0f) + { + if (initialFrame == Time.frameCount) + { + return true; + } + } + + elapsed += Time.deltaTime; + if (elapsed >= delayTimeSpan) + { + return false; + } + + return true; + } + + protected override void ResetCore() + { + elapsed = 0.0f; + } + } + + sealed class IgnoreTimeScalePlayerLoopTimer : PlayerLoopTimer + { + int initialFrame; + float elapsed; + readonly float delayTimeSpan; + + public IgnoreTimeScalePlayerLoopTimer(TimeSpan delayTimeSpan, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(playerLoopTiming, cancellationToken, timerCallback, state) + { + this.elapsed = 0.0f; + this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; + } + + protected override bool MoveNextCore() + { + if (elapsed == 0.0f) + { + if (initialFrame == Time.frameCount) + { + return true; + } + } + + elapsed += Time.unscaledDeltaTime; + if (elapsed >= delayTimeSpan) + { + return false; + } + + return true; + } + + protected override void ResetCore() + { + elapsed = 0.0f; + } + } + + sealed class RealtimePlayerLoopTimer : PlayerLoopTimer + { + ValueStopwatch stopwatch; + readonly long delayTimeSpanTicks; + + public RealtimePlayerLoopTimer(TimeSpan delayTimeSpan, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(playerLoopTiming, cancellationToken, timerCallback, state) + { + this.stopwatch = ValueStopwatch.StartNew(); + this.delayTimeSpanTicks = delayTimeSpan.Ticks; + } + + protected override bool MoveNextCore() + { + if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) + { + return false; + } + + return true; + } + + protected override void ResetCore() + { + this.stopwatch = ValueStopwatch.StartNew(); + } + } } } From 62f6429b60161984dae6d946e79b4315935da449 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 12 Mar 2021 11:35:46 +0900 Subject: [PATCH 2/4] add PlayerLoopTimer --- .../CancellationTokenSourceExtensions.cs | 227 +----------------- .../UniTask/Runtime/PlayerLoopTimer.cs | 211 ++++++++++++++++ 2 files changed, 223 insertions(+), 215 deletions(-) create mode 100644 src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs index 70fb10b..ed15089 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs @@ -9,28 +9,24 @@ using Cysharp.Threading.Tasks.Internal; namespace Cysharp.Threading.Tasks { - public static class CancellationTokenSourceExtensions + public static partial class CancellationTokenSourceExtensions { - public static void CancelAfterSlim(this CancellationTokenSource cts, int millisecondsDelay, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) + readonly static Action CancelCancellationTokenSourceStateDelegate = new Action(CancelCancellationTokenSourceState); + + static void CancelCancellationTokenSourceState(object state) { - var delay = UniTask.Delay(millisecondsDelay, delayType, delayTiming, cts.Token); - CancelAfterCore(cts, delay).Forget(); + var cts = (CancellationTokenSource)state; + cts.Cancel(); } - public static void CancelAfterSlim(this CancellationTokenSource cts, TimeSpan delayTimeSpan, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) + public static IDisposable CancelAfterSlim(this CancellationTokenSource cts, int millisecondsDelay, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { - var delay = UniTask.Delay(delayTimeSpan, delayType, delayTiming, cts.Token); - CancelAfterCore(cts, delay).Forget(); + return CancelAfterSlim(cts, TimeSpan.FromMilliseconds(millisecondsDelay), delayType, delayTiming); } - static async UniTaskVoid CancelAfterCore(CancellationTokenSource cts, UniTask delayTask) + public static IDisposable CancelAfterSlim(this CancellationTokenSource cts, TimeSpan delayTimeSpan, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { - var alreadyCanceled = await delayTask.SuppressCancellationThrow(); - if (!alreadyCanceled) - { - cts.Cancel(); - cts.Dispose(); - } + return PlayerLoopTimer.StartNew(delayTimeSpan, false, delayType, delayTiming, cts.Token, CancelCancellationTokenSourceStateDelegate, cts); } public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, Component component) @@ -41,209 +37,10 @@ namespace Cysharp.Threading.Tasks public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, GameObject gameObject) { var trigger = gameObject.GetAsyncDestroyTrigger(); - trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(state => - { - var cts2 = (CancellationTokenSource)state; - cts2.Cancel(); - }, cts); + trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts); } - // TODO:delete this. - public static IDisposable CancelAfterSlim2(this CancellationTokenSource cts, TimeSpan delayTimeSpan) - { - var timer = PlayerLoopTimer.Create(delayTimeSpan, DelayType.DeltaTime, PlayerLoopTiming.Update, cts.Token, state => - { - var x = (CancellationTokenSource)state; - x.Cancel(); - }, cts); - timer.Restart(); - - return timer; - } - - abstract class PlayerLoopTimer : IDisposable, IPlayerLoopItem - { - readonly CancellationToken cancellationToken; - readonly Action timerCallback; - readonly object state; - readonly PlayerLoopTiming playerLoopTiming; - - bool isPlaying; - bool isDisposed; - - protected PlayerLoopTimer(PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) - { - this.playerLoopTiming = playerLoopTiming; - this.cancellationToken = cancellationToken; - this.timerCallback = timerCallback; - this.state = state; - } - - public static PlayerLoopTimer Create(TimeSpan delayTimeSpan, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) - { - switch (delayType) - { - case DelayType.UnscaledDeltaTime: - return new IgnoreTimeScalePlayerLoopTimer(delayTimeSpan, playerLoopTiming, cancellationToken, timerCallback, state); - case DelayType.Realtime: - return new RealtimePlayerLoopTimer(delayTimeSpan, playerLoopTiming, cancellationToken, timerCallback, state); - case DelayType.DeltaTime: - default: - return new DeltaTimePlayerLoopTimer(delayTimeSpan, playerLoopTiming, cancellationToken, timerCallback, state); - } - } - - /// - /// Restart(Reset and Start) timer. - /// - public void Restart() - { - if (isDisposed) throw new ObjectDisposedException(null); - if (isPlaying) return; - - ResetCore(); // init state - isPlaying = true; - PlayerLoopHelper.AddAction(playerLoopTiming, this); - } - - /// - /// Stop timer. - /// - public void Stop() - { - isPlaying = false; - } - - protected abstract void ResetCore(); - - public void Dispose() - { - isDisposed = true; - } - - bool IPlayerLoopItem.MoveNext() - { - if (isDisposed) return false; - if (!isPlaying) return false; - if (cancellationToken.IsCancellationRequested) return false; - - if (!MoveNextCore()) - { - timerCallback(state); - return false; - } - - return true; - } - - protected abstract bool MoveNextCore(); - } - - sealed class DeltaTimePlayerLoopTimer : PlayerLoopTimer - { - int initialFrame; - float elapsed; - readonly float delayTimeSpan; - - public DeltaTimePlayerLoopTimer(TimeSpan delayTimeSpan, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) - : base(playerLoopTiming, cancellationToken, timerCallback, state) - { - this.elapsed = 0.0f; - this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; - this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; - } - - protected override bool MoveNextCore() - { - if (elapsed == 0.0f) - { - if (initialFrame == Time.frameCount) - { - return true; - } - } - - elapsed += Time.deltaTime; - if (elapsed >= delayTimeSpan) - { - return false; - } - - return true; - } - - protected override void ResetCore() - { - elapsed = 0.0f; - } - } - - sealed class IgnoreTimeScalePlayerLoopTimer : PlayerLoopTimer - { - int initialFrame; - float elapsed; - readonly float delayTimeSpan; - - public IgnoreTimeScalePlayerLoopTimer(TimeSpan delayTimeSpan, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) - : base(playerLoopTiming, cancellationToken, timerCallback, state) - { - this.elapsed = 0.0f; - this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; - this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; - } - - protected override bool MoveNextCore() - { - if (elapsed == 0.0f) - { - if (initialFrame == Time.frameCount) - { - return true; - } - } - - elapsed += Time.unscaledDeltaTime; - if (elapsed >= delayTimeSpan) - { - return false; - } - - return true; - } - - protected override void ResetCore() - { - elapsed = 0.0f; - } - } - - sealed class RealtimePlayerLoopTimer : PlayerLoopTimer - { - ValueStopwatch stopwatch; - readonly long delayTimeSpanTicks; - - public RealtimePlayerLoopTimer(TimeSpan delayTimeSpan, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) - : base(playerLoopTiming, cancellationToken, timerCallback, state) - { - this.stopwatch = ValueStopwatch.StartNew(); - this.delayTimeSpanTicks = delayTimeSpan.Ticks; - } - - protected override bool MoveNextCore() - { - if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) - { - return false; - } - - return true; - } - - protected override void ResetCore() - { - this.stopwatch = ValueStopwatch.StartNew(); - } - } + } } diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs new file mode 100644 index 0000000..03dc117 --- /dev/null +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs @@ -0,0 +1,211 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +using System.Threading; +using System; +using Cysharp.Threading.Tasks.Internal; +using UnityEngine; + +namespace Cysharp.Threading.Tasks +{ + public abstract class PlayerLoopTimer : IDisposable, IPlayerLoopItem + { + readonly CancellationToken cancellationToken; + readonly Action timerCallback; + readonly object state; + readonly PlayerLoopTiming playerLoopTiming; + readonly bool periodic; + + bool isPlaying; + bool isDisposed; + + protected PlayerLoopTimer(bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + this.periodic = periodic; + this.playerLoopTiming = playerLoopTiming; + this.cancellationToken = cancellationToken; + this.timerCallback = timerCallback; + this.state = state; + } + + public static PlayerLoopTimer Create(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + switch (delayType) + { + case DelayType.UnscaledDeltaTime: + return new IgnoreTimeScalePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + case DelayType.Realtime: + return new RealtimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + case DelayType.DeltaTime: + default: + return new DeltaTimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + } + } + + public static PlayerLoopTimer StartNew(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + var timer = Create(delayTimeSpan, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state); + timer.Restart(); + return timer; + } + + /// + /// Restart(Reset and Start) timer. + /// + public void Restart() + { + if (isDisposed) throw new ObjectDisposedException(null); + + ResetCore(); // init state + isPlaying = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + + /// + /// Stop timer. + /// + public void Stop() + { + isPlaying = false; + } + + protected abstract void ResetCore(); + + public void Dispose() + { + isDisposed = true; + } + + bool IPlayerLoopItem.MoveNext() + { + if (isDisposed) return false; + if (!isPlaying) return false; + if (cancellationToken.IsCancellationRequested) return false; + + if (!MoveNextCore()) + { + timerCallback(state); + + if (periodic) + { + ResetCore(); + return true; + } + else + { + return false; + } + } + + return true; + } + + protected abstract bool MoveNextCore(); + } + + sealed class DeltaTimePlayerLoopTimer : PlayerLoopTimer + { + int initialFrame; + float elapsed; + readonly float delayTimeSpan; + + public DeltaTimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) + { + this.elapsed = 0.0f; + this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; + } + + protected override bool MoveNextCore() + { + if (elapsed == 0.0f) + { + if (initialFrame == Time.frameCount) + { + return true; + } + } + + elapsed += Time.deltaTime; + if (elapsed >= delayTimeSpan) + { + return false; + } + + return true; + } + + protected override void ResetCore() + { + elapsed = 0.0f; + } + } + + sealed class IgnoreTimeScalePlayerLoopTimer : PlayerLoopTimer + { + int initialFrame; + float elapsed; + readonly float delayTimeSpan; + + public IgnoreTimeScalePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) + { + this.elapsed = 0.0f; + this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; + } + + protected override bool MoveNextCore() + { + if (elapsed == 0.0f) + { + if (initialFrame == Time.frameCount) + { + return true; + } + } + + elapsed += Time.unscaledDeltaTime; + if (elapsed >= delayTimeSpan) + { + return false; + } + + return true; + } + + protected override void ResetCore() + { + elapsed = 0.0f; + } + } + + sealed class RealtimePlayerLoopTimer : PlayerLoopTimer + { + ValueStopwatch stopwatch; + readonly long delayTimeSpanTicks; + + public RealtimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) + { + this.stopwatch = ValueStopwatch.StartNew(); + this.delayTimeSpanTicks = delayTimeSpan.Ticks; + } + + protected override bool MoveNextCore() + { + if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) + { + return false; + } + + return true; + } + + protected override void ResetCore() + { + this.stopwatch = ValueStopwatch.StartNew(); + } + } +} + From 49ca9364f7511e1ac6d587580ce6501f9b0a3981 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 12 Mar 2021 13:01:46 +0900 Subject: [PATCH 3/4] TimeoutController uses PlayerLoopTimer(WIP, needs test) --- .../CancellationTokenSourceExtensions.cs | 2 - .../UniTask/Runtime/PlayerLoopTimer.cs | 85 ++++--- .../UniTask/Runtime/TimeoutController.cs | 230 ++++-------------- 3 files changed, 108 insertions(+), 209 deletions(-) diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs index ed15089..c519944 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs @@ -39,8 +39,6 @@ namespace Cysharp.Threading.Tasks var trigger = gameObject.GetAsyncDestroyTrigger(); trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts); } - - } } diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs index 03dc117..110c8cf 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs @@ -27,23 +27,31 @@ namespace Cysharp.Threading.Tasks this.state = state; } - public static PlayerLoopTimer Create(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + public static PlayerLoopTimer Create(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) { +#if UNITY_EDITOR + // force use Realtime. + if (PlayerLoopHelper.IsMainThread && !UnityEditor.EditorApplication.isPlaying) + { + delayType = DelayType.Realtime; + } +#endif + switch (delayType) { case DelayType.UnscaledDeltaTime: - return new IgnoreTimeScalePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + return new IgnoreTimeScalePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state); case DelayType.Realtime: - return new RealtimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + return new RealtimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state); case DelayType.DeltaTime: default: - return new DeltaTimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + return new DeltaTimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state); } } - public static PlayerLoopTimer StartNew(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + public static PlayerLoopTimer StartNew(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) { - var timer = Create(delayTimeSpan, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state); + var timer = Create(interval, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state); timer.Restart(); return timer; } @@ -55,7 +63,19 @@ namespace Cysharp.Threading.Tasks { if (isDisposed) throw new ObjectDisposedException(null); - ResetCore(); // init state + ResetCore(null); // init state + isPlaying = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + + /// + /// Restart(Reset and Start) and change interval. + /// + public void Restart(TimeSpan interval) + { + if (isDisposed) throw new ObjectDisposedException(null); + + ResetCore(interval); // init state isPlaying = true; PlayerLoopHelper.AddAction(playerLoopTiming, this); } @@ -68,7 +88,7 @@ namespace Cysharp.Threading.Tasks isPlaying = false; } - protected abstract void ResetCore(); + protected abstract void ResetCore(TimeSpan? newInterval); public void Dispose() { @@ -87,7 +107,7 @@ namespace Cysharp.Threading.Tasks if (periodic) { - ResetCore(); + ResetCore(null); return true; } else @@ -106,13 +126,12 @@ namespace Cysharp.Threading.Tasks { int initialFrame; float elapsed; - readonly float delayTimeSpan; + float interval; - public DeltaTimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + public DeltaTimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { - this.elapsed = 0.0f; - this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; + ResetCore(interval); this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; } @@ -127,7 +146,7 @@ namespace Cysharp.Threading.Tasks } elapsed += Time.deltaTime; - if (elapsed >= delayTimeSpan) + if (elapsed >= interval) { return false; } @@ -135,9 +154,13 @@ namespace Cysharp.Threading.Tasks return true; } - protected override void ResetCore() + protected override void ResetCore(TimeSpan? interval) { - elapsed = 0.0f; + this.elapsed = 0.0f; + if (interval != null) + { + this.interval = (float)interval.Value.TotalSeconds; + } } } @@ -145,13 +168,12 @@ namespace Cysharp.Threading.Tasks { int initialFrame; float elapsed; - readonly float delayTimeSpan; + float interval; - public IgnoreTimeScalePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + public IgnoreTimeScalePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { - this.elapsed = 0.0f; - this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds; + ResetCore(interval); this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; } @@ -166,7 +188,7 @@ namespace Cysharp.Threading.Tasks } elapsed += Time.unscaledDeltaTime; - if (elapsed >= delayTimeSpan) + if (elapsed >= interval) { return false; } @@ -174,27 +196,30 @@ namespace Cysharp.Threading.Tasks return true; } - protected override void ResetCore() + protected override void ResetCore(TimeSpan? interval) { elapsed = 0.0f; + if (interval != null) + { + this.interval = (float)interval.Value.TotalSeconds; + } } } sealed class RealtimePlayerLoopTimer : PlayerLoopTimer { ValueStopwatch stopwatch; - readonly long delayTimeSpanTicks; + long intervalTicks; - public RealtimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + public RealtimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { - this.stopwatch = ValueStopwatch.StartNew(); - this.delayTimeSpanTicks = delayTimeSpan.Ticks; + ResetCore(interval); } protected override bool MoveNextCore() { - if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) + if (stopwatch.ElapsedTicks >= intervalTicks) { return false; } @@ -202,9 +227,13 @@ namespace Cysharp.Threading.Tasks return true; } - protected override void ResetCore() + protected override void ResetCore(TimeSpan? interval) { this.stopwatch = ValueStopwatch.StartNew(); + if (interval != null) + { + this.intervalTicks = interval.Value.Ticks; + } } } } diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs index 6dc5517..e38d7b7 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs @@ -1,8 +1,7 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -using System.Threading; using System; -using Cysharp.Threading.Tasks.Internal; +using System.Threading; namespace Cysharp.Threading.Tasks { @@ -14,26 +13,44 @@ namespace Cysharp.Threading.Tasks public sealed class TimeoutController : IDisposable { + readonly static Action CancelCancellationTokenSourceStateDelegate = new Action(CancelCancellationTokenSourceState); + + static void CancelCancellationTokenSourceState(object state) + { + var cts = (CancellationTokenSource)state; + cts.Cancel(); + } + CancellationTokenSource timeoutSource; CancellationTokenSource linkedSource; - StoppableDelayRealtimePromise timeoutDelay; + PlayerLoopTimer timer; + bool isDisposed; + readonly DelayType delayType; + readonly PlayerLoopTiming delayTiming; readonly CancellationTokenSource originalLinkCancellationTokenSource; - public TimeoutController() + public TimeoutController(DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { this.timeoutSource = new CancellationTokenSource(); this.originalLinkCancellationTokenSource = null; this.linkedSource = null; - this.timeoutDelay = null; + this.delayType = delayType; + this.delayTiming = delayTiming; } - public TimeoutController(CancellationTokenSource linkCancellationTokenSource) + public TimeoutController(CancellationTokenSource linkCancellationTokenSource, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { this.timeoutSource = new CancellationTokenSource(); this.originalLinkCancellationTokenSource = linkCancellationTokenSource; this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, linkCancellationTokenSource.Token); - this.timeoutDelay = null; + this.delayType = delayType; + this.delayTiming = delayTiming; + } + + public CancellationToken Timeout(int millisecondsTimeout) + { + return Timeout(TimeSpan.FromMilliseconds(millisecondsTimeout)); } public CancellationToken Timeout(TimeSpan timeout) @@ -43,6 +60,7 @@ namespace Cysharp.Threading.Tasks return originalLinkCancellationTokenSource.Token; } + // Timeouted, create new source and timer. if (timeoutSource.IsCancellationRequested) { timeoutSource.Dispose(); @@ -53,18 +71,26 @@ namespace Cysharp.Threading.Tasks this.linkedSource.Dispose(); this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token); } + + timer?.Dispose(); + timer = null; } - if (timeoutDelay == null) + + var useSource = (linkedSource != null) ? linkedSource : timeoutSource; + var token = useSource.Token; + if (timer == null) { - RunDelayAsync(timeout).Forget(); // timeoutDelay = ... in RunDelayAsync(immediately, before await) + // Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled. + // (linked)token is canceled => stop timer + timer = PlayerLoopTimer.Create(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); } else { - timeoutDelay.RestartStopwatch(); // already running RunDelayAsync + timer.Restart(timeout); } - return (linkedSource != null) ? linkedSource.Token : timeoutSource.Token; + return token; } public bool IsTimeout() @@ -74,184 +100,30 @@ namespace Cysharp.Threading.Tasks public void Reset() { - if (timeoutDelay != null) - { - timeoutDelay.Stop(); // stop delay, will finish RunDelayAsync - timeoutDelay = null; - } - } - - async UniTaskVoid RunDelayAsync(TimeSpan timeout) - { - timeoutDelay = StoppableDelayRealtimePromise.Create(timeout, PlayerLoopTiming.Update, (linkedSource == null) ? CancellationToken.None : linkedSource.Token, out var version); - try - { - var reason = await new UniTask(timeoutDelay, version); - if (reason == DelayResult.DelayCompleted) - { - // UnityEngine.Debug.Log("DEBUG:Timeout Complete, try to call timeoutSource.Cancel"); - timeoutSource.Cancel(); - } - else if (reason == DelayResult.LinkedTokenCanceled) - { - // UnityEngine.Debug.Log("DEBUG:LinkedSource IsCancellationRequested"); - } - else if (reason == DelayResult.ExternalStopped) - { - // Reset(Promise.Stop) called, do nothing. - // UnityEngine.Debug.Log("DEBUG:Reset called"); - } - } - finally - { - timeoutDelay = null; - } + timer.Stop(); } public void Dispose() { - if (timeoutDelay != null) + if (isDisposed) return; + + try { - timeoutDelay.Stop(); - } - timeoutSource.Dispose(); - if (linkedSource != null) - { - linkedSource.Dispose(); - } - } + // stop timer. + timer.Dispose(); - enum DelayResult - { - LinkedTokenCanceled, - ExternalStopped, - DelayCompleted, // as Timeout. - } - - // Stop + SuppressCancellationThrow. - sealed class StoppableDelayRealtimePromise : IUniTaskSource, IPlayerLoopItem, ITaskPoolNode - { - static OperationCanceledException ExterenalStopException = new OperationCanceledException(); - - static TaskPool pool; - StoppableDelayRealtimePromise nextNode; - public ref StoppableDelayRealtimePromise NextNode => ref nextNode; - - static StoppableDelayRealtimePromise() - { - TaskPool.RegisterSizeGetter(typeof(StoppableDelayRealtimePromise), () => pool.Size); - } - - long delayTimeSpanTicks; - ValueStopwatch stopwatch; - CancellationToken cancellationToken; - bool externalStop; - - UniTaskCompletionSourceCore core; - - StoppableDelayRealtimePromise() - { - } - - public static StoppableDelayRealtimePromise Create(TimeSpan delayTimeSpan, PlayerLoopTiming timing, CancellationToken cancellationToken, out short token) - { - if (!pool.TryPop(out var result)) + // cancel and dispose. + timeoutSource.Cancel(); + timeoutSource.Dispose(); + if (linkedSource != null) { - result = new StoppableDelayRealtimePromise(); - } - - result.stopwatch = ValueStopwatch.StartNew(); - result.delayTimeSpanTicks = delayTimeSpan.Ticks; - result.cancellationToken = cancellationToken; - result.externalStop = false; - - TaskTracker.TrackActiveTask(result, 3); - - PlayerLoopHelper.AddAction(timing, result); - - token = result.core.Version; - return result; - } - - public void Stop() - { - externalStop = true; - } - - public void RestartStopwatch() - { - stopwatch = ValueStopwatch.StartNew(); - } - - public DelayResult GetResult(short token) - { - try - { - return core.GetResult(token); - } - finally - { - TryReturn(); + linkedSource.Cancel(); + linkedSource.Dispose(); } } - - void IUniTaskSource.GetResult(short token) + finally { - GetResult(token); - } - - public UniTaskStatus GetStatus(short token) - { - return core.GetStatus(token); - } - - public UniTaskStatus UnsafeGetStatus() - { - return core.UnsafeGetStatus(); - } - - public void OnCompleted(Action continuation, object state, short token) - { - core.OnCompleted(continuation, state, token); - } - - public bool MoveNext() - { - if (cancellationToken.IsCancellationRequested) - { - core.TrySetResult(DelayResult.LinkedTokenCanceled); - return false; - } - - if (externalStop) - { - core.TrySetResult(DelayResult.ExternalStopped); - return false; - } - - if (stopwatch.IsInvalid) - { - core.TrySetResult(DelayResult.DelayCompleted); - return false; - } - - if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) - { - core.TrySetResult(DelayResult.DelayCompleted); - return false; - } - - return true; - } - - bool TryReturn() - { - TaskTracker.RemoveTracking(this); - core.Reset(); - stopwatch = default; - cancellationToken = default; - externalStop = false; - return pool.TryPush(this); + isDisposed = true; } } } From 8b3c8d15c43bc919bfde777df6bf29e644da39f8 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 12 Mar 2021 16:06:42 +0900 Subject: [PATCH 4/4] PlayerLoopTimer unit test and fixes --- src/UniTask.NetCore/UniTask.NetCore.csproj | 1 + .../UniTask/Runtime/PlayerLoopTimer.cs | 46 +++-- .../UniTask/Runtime/PlayerLoopTimer.cs.meta | 11 ++ .../UniTask/Runtime/TimeoutController.cs | 3 +- .../Assets/Tests/PlayerLoopTimerTest.cs | 179 ++++++++++++++++++ .../Assets/Tests/PlayerLoopTimerTest.cs.meta | 11 ++ 6 files changed, 237 insertions(+), 14 deletions(-) create mode 100644 src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta create mode 100644 src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs create mode 100644 src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta diff --git a/src/UniTask.NetCore/UniTask.NetCore.csproj b/src/UniTask.NetCore/UniTask.NetCore.csproj index 80dd978..8a2f804 100644 --- a/src/UniTask.NetCore/UniTask.NetCore.csproj +++ b/src/UniTask.NetCore/UniTask.NetCore.csproj @@ -43,6 +43,7 @@ ..\UniTask\Assets\Plugins\UniTask\Runtime\EnumeratorAsyncExtensions.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\TimeoutController.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopHelper.cs; +..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopTimer.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Delay.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Run.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Bridge.cs; diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs index 110c8cf..f8a877a 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs @@ -15,7 +15,8 @@ namespace Cysharp.Threading.Tasks readonly PlayerLoopTiming playerLoopTiming; readonly bool periodic; - bool isPlaying; + bool isRunning; + bool tryStop; bool isDisposed; protected PlayerLoopTimer(bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) @@ -64,8 +65,12 @@ namespace Cysharp.Threading.Tasks if (isDisposed) throw new ObjectDisposedException(null); ResetCore(null); // init state - isPlaying = true; - PlayerLoopHelper.AddAction(playerLoopTiming, this); + if (!isRunning) + { + isRunning = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + tryStop = false; } /// @@ -76,8 +81,12 @@ namespace Cysharp.Threading.Tasks if (isDisposed) throw new ObjectDisposedException(null); ResetCore(interval); // init state - isPlaying = true; - PlayerLoopHelper.AddAction(playerLoopTiming, this); + if (!isRunning) + { + isRunning = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + tryStop = false; } /// @@ -85,7 +94,7 @@ namespace Cysharp.Threading.Tasks /// public void Stop() { - isPlaying = false; + tryStop = true; } protected abstract void ResetCore(TimeSpan? newInterval); @@ -97,9 +106,21 @@ namespace Cysharp.Threading.Tasks bool IPlayerLoopItem.MoveNext() { - if (isDisposed) return false; - if (!isPlaying) return false; - if (cancellationToken.IsCancellationRequested) return false; + if (isDisposed) + { + isRunning = false; + return false; + } + if (tryStop) + { + isRunning = false; + return false; + } + if (cancellationToken.IsCancellationRequested) + { + isRunning = false; + return false; + } if (!MoveNextCore()) { @@ -112,6 +133,7 @@ namespace Cysharp.Threading.Tasks } else { + isRunning = false; return false; } } @@ -132,7 +154,6 @@ namespace Cysharp.Threading.Tasks : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { ResetCore(interval); - this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; } protected override bool MoveNextCore() @@ -157,6 +178,7 @@ namespace Cysharp.Threading.Tasks protected override void ResetCore(TimeSpan? interval) { this.elapsed = 0.0f; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; if (interval != null) { this.interval = (float)interval.Value.TotalSeconds; @@ -174,7 +196,6 @@ namespace Cysharp.Threading.Tasks : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { ResetCore(interval); - this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; } protected override bool MoveNextCore() @@ -198,7 +219,8 @@ namespace Cysharp.Threading.Tasks protected override void ResetCore(TimeSpan? interval) { - elapsed = 0.0f; + this.elapsed = 0.0f; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; if (interval != null) { this.interval = (float)interval.Value.TotalSeconds; diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta new file mode 100644 index 0000000..eb2b50a --- /dev/null +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57095a17fdca7ee4380450910afc7f26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs index e38d7b7..ad95cb5 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs @@ -76,14 +76,13 @@ namespace Cysharp.Threading.Tasks timer = null; } - var useSource = (linkedSource != null) ? linkedSource : timeoutSource; var token = useSource.Token; if (timer == null) { // Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled. // (linked)token is canceled => stop timer - timer = PlayerLoopTimer.Create(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); + timer = PlayerLoopTimer.StartNew(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); } else { diff --git a/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs new file mode 100644 index 0000000..9a91613 --- /dev/null +++ b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs @@ -0,0 +1,179 @@ +using Cysharp.Threading.Tasks; +using FluentAssertions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine.TestTools; + +namespace Cysharp.Threading.TasksTests +{ + public class PlayerLoopTimerTest + { + void Between(TimeSpan l, TimeSpan data, TimeSpan r) + { + NUnit.Framework.Assert.AreEqual(l < data, true, "{0} < {1} failed.", l, data); + NUnit.Framework.Assert.AreEqual(data < r, true, "{0} < {1} failed.", data, r); + } + + [UnityTest] + public IEnumerator StandardTicks() => UniTask.ToCoroutine(async () => + { + foreach (var delay in new[] { DelayType.DeltaTime, DelayType.Realtime, DelayType.UnscaledDeltaTime }) + { + var raisedTimeout = new UniTaskCompletionSource(); + PlayerLoopTimer.StartNew(TimeSpan.FromSeconds(1), false, delay, PlayerLoopTiming.Update, CancellationToken.None, _ => + { + raisedTimeout.TrySetResult(); + }, null); + + + var sw = Stopwatch.StartNew(); + await raisedTimeout.Task; + sw.Stop(); + + Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1)); + } + }); + + + [UnityTest] + public IEnumerator Periodic() => UniTask.ToCoroutine(async () => + { + var raisedTime = new List(); + var count = 0; + var complete = new UniTaskCompletionSource(); + + PlayerLoopTimer timer = null; + timer = PlayerLoopTimer.StartNew(TimeSpan.FromSeconds(1), true, DelayType.DeltaTime, PlayerLoopTiming.Update, CancellationToken.None, _ => + { + raisedTime.Add(DateTime.UtcNow); + count++; + if (count == 3) + { + complete.TrySetResult(); + timer.Dispose(); + } + }, null); + + var start = DateTime.UtcNow; + await complete.Task; + + Between(TimeSpan.FromSeconds(0.9), raisedTime[0] - start, TimeSpan.FromSeconds(1.1)); + Between(TimeSpan.FromSeconds(1.9), raisedTime[1] - start, TimeSpan.FromSeconds(2.1)); + Between(TimeSpan.FromSeconds(2.9), raisedTime[2] - start, TimeSpan.FromSeconds(3.1)); + }); + + [UnityTest] + public IEnumerator CancelAfterSlimTest() => UniTask.ToCoroutine(async () => + { + var cts = new CancellationTokenSource(); + var complete = new UniTaskCompletionSource(); + cts.Token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); + }); + + cts.CancelAfterSlim(TimeSpan.FromSeconds(1)); + + var sw = Stopwatch.StartNew(); + await complete.Task; + + Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1)); + }); + + [UnityTest] + public IEnumerator CancelAfterSlimCancelTest() => UniTask.ToCoroutine(async () => + { + var cts = new CancellationTokenSource(); + var complete = new UniTaskCompletionSource(); + cts.Token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); + }); + + var d = cts.CancelAfterSlim(TimeSpan.FromSeconds(1)); + + var sw = Stopwatch.StartNew(); + + await UniTask.Delay(TimeSpan.FromMilliseconds(100)); + d.Dispose(); + + await UniTask.Delay(TimeSpan.FromSeconds(2)); + + complete.Task.Status.Should().Be(UniTaskStatus.Pending); + }); + + [UnityTest] + public IEnumerator TimeoutController() => UniTask.ToCoroutine(async () => + { + var controller = new TimeoutController(); + + var token = controller.Timeout(TimeSpan.FromSeconds(1)); + + var complete = new UniTaskCompletionSource(); + token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); + }); + + var sw = Stopwatch.StartNew(); + await complete.Task; + Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1)); + + controller.IsTimeout().Should().BeTrue(); + }); + + + [UnityTest] + public IEnumerator TimeoutReuse() => UniTask.ToCoroutine(async () => + { + var controller = new TimeoutController(DelayType.DeltaTime); + + var token = controller.Timeout(TimeSpan.FromSeconds(2)); + + var complete = new UniTaskCompletionSource(); + token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); // reuse, used same token? + }); + + await UniTask.Delay(TimeSpan.FromMilliseconds(100)); + controller.Reset(); + + controller.IsTimeout().Should().BeFalse(); + + var sw = Stopwatch.StartNew(); + + controller.Timeout(TimeSpan.FromSeconds(5)); + + await complete.Task; + + UnityEngine.Debug.Log(UnityEngine.Time.timeScale); + Between(TimeSpan.FromSeconds(4.9), sw.Elapsed, TimeSpan.FromSeconds(5.1)); + + controller.IsTimeout().Should().BeTrue(); + }); + + [UnityTest] + public IEnumerator LinkedTokenTest() => UniTask.ToCoroutine(async () => + { + var cts = new CancellationTokenSource(); + + var controller = new TimeoutController(cts); + var token = controller.Timeout(TimeSpan.FromSeconds(2)); + + await UniTask.DelayFrame(3); + + cts.Cancel(); + + token.IsCancellationRequested.Should().BeTrue(); + + controller.Dispose(); + }); + } +} diff --git a/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta new file mode 100644 index 0000000..dbaecbd --- /dev/null +++ b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0c49de697f829f44aa8709b4d1eff3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: