TimeoutController uses PlayerLoopTimer(WIP, needs test)

master
neuecc 2021-03-12 13:01:46 +09:00
parent 62f6429b60
commit 49ca9364f7
3 changed files with 108 additions and 209 deletions

View File

@ -39,8 +39,6 @@ namespace Cysharp.Threading.Tasks
var trigger = gameObject.GetAsyncDestroyTrigger(); var trigger = gameObject.GetAsyncDestroyTrigger();
trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts); trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts);
} }
} }
} }

View File

@ -27,23 +27,31 @@ namespace Cysharp.Threading.Tasks
this.state = state; this.state = state;
} }
public static PlayerLoopTimer Create(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state) public static PlayerLoopTimer Create(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
{ {
#if UNITY_EDITOR
// force use Realtime.
if (PlayerLoopHelper.IsMainThread && !UnityEditor.EditorApplication.isPlaying)
{
delayType = DelayType.Realtime;
}
#endif
switch (delayType) switch (delayType)
{ {
case DelayType.UnscaledDeltaTime: case DelayType.UnscaledDeltaTime:
return new IgnoreTimeScalePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); return new IgnoreTimeScalePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
case DelayType.Realtime: case DelayType.Realtime:
return new RealtimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state); return new RealtimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
case DelayType.DeltaTime: case DelayType.DeltaTime:
default: 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<object> timerCallback, object state) public static PlayerLoopTimer StartNew(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> 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(); timer.Restart();
return timer; return timer;
} }
@ -55,7 +63,19 @@ namespace Cysharp.Threading.Tasks
{ {
if (isDisposed) throw new ObjectDisposedException(null); if (isDisposed) throw new ObjectDisposedException(null);
ResetCore(); // init state ResetCore(null); // init state
isPlaying = true;
PlayerLoopHelper.AddAction(playerLoopTiming, this);
}
/// <summary>
/// Restart(Reset and Start) and change interval.
/// </summary>
public void Restart(TimeSpan interval)
{
if (isDisposed) throw new ObjectDisposedException(null);
ResetCore(interval); // init state
isPlaying = true; isPlaying = true;
PlayerLoopHelper.AddAction(playerLoopTiming, this); PlayerLoopHelper.AddAction(playerLoopTiming, this);
} }
@ -68,7 +88,7 @@ namespace Cysharp.Threading.Tasks
isPlaying = false; isPlaying = false;
} }
protected abstract void ResetCore(); protected abstract void ResetCore(TimeSpan? newInterval);
public void Dispose() public void Dispose()
{ {
@ -87,7 +107,7 @@ namespace Cysharp.Threading.Tasks
if (periodic) if (periodic)
{ {
ResetCore(); ResetCore(null);
return true; return true;
} }
else else
@ -106,13 +126,12 @@ namespace Cysharp.Threading.Tasks
{ {
int initialFrame; int initialFrame;
float elapsed; float elapsed;
readonly float delayTimeSpan; float interval;
public DeltaTimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state) public DeltaTimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
{ {
this.elapsed = 0.0f; ResetCore(interval);
this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds;
this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1;
} }
@ -127,7 +146,7 @@ namespace Cysharp.Threading.Tasks
} }
elapsed += Time.deltaTime; elapsed += Time.deltaTime;
if (elapsed >= delayTimeSpan) if (elapsed >= interval)
{ {
return false; return false;
} }
@ -135,9 +154,13 @@ namespace Cysharp.Threading.Tasks
return true; 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; int initialFrame;
float elapsed; float elapsed;
readonly float delayTimeSpan; float interval;
public IgnoreTimeScalePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state) public IgnoreTimeScalePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
{ {
this.elapsed = 0.0f; ResetCore(interval);
this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds;
this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1;
} }
@ -166,7 +188,7 @@ namespace Cysharp.Threading.Tasks
} }
elapsed += Time.unscaledDeltaTime; elapsed += Time.unscaledDeltaTime;
if (elapsed >= delayTimeSpan) if (elapsed >= interval)
{ {
return false; return false;
} }
@ -174,27 +196,30 @@ namespace Cysharp.Threading.Tasks
return true; return true;
} }
protected override void ResetCore() protected override void ResetCore(TimeSpan? interval)
{ {
elapsed = 0.0f; elapsed = 0.0f;
if (interval != null)
{
this.interval = (float)interval.Value.TotalSeconds;
}
} }
} }
sealed class RealtimePlayerLoopTimer : PlayerLoopTimer sealed class RealtimePlayerLoopTimer : PlayerLoopTimer
{ {
ValueStopwatch stopwatch; ValueStopwatch stopwatch;
readonly long delayTimeSpanTicks; long intervalTicks;
public RealtimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state) public RealtimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
{ {
this.stopwatch = ValueStopwatch.StartNew(); ResetCore(interval);
this.delayTimeSpanTicks = delayTimeSpan.Ticks;
} }
protected override bool MoveNextCore() protected override bool MoveNextCore()
{ {
if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) if (stopwatch.ElapsedTicks >= intervalTicks)
{ {
return false; return false;
} }
@ -202,9 +227,13 @@ namespace Cysharp.Threading.Tasks
return true; return true;
} }
protected override void ResetCore() protected override void ResetCore(TimeSpan? interval)
{ {
this.stopwatch = ValueStopwatch.StartNew(); this.stopwatch = ValueStopwatch.StartNew();
if (interval != null)
{
this.intervalTicks = interval.Value.Ticks;
}
} }
} }
} }

View File

@ -1,8 +1,7 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System.Threading;
using System; using System;
using Cysharp.Threading.Tasks.Internal; using System.Threading;
namespace Cysharp.Threading.Tasks namespace Cysharp.Threading.Tasks
{ {
@ -14,26 +13,44 @@ namespace Cysharp.Threading.Tasks
public sealed class TimeoutController : IDisposable public sealed class TimeoutController : IDisposable
{ {
readonly static Action<object> CancelCancellationTokenSourceStateDelegate = new Action<object>(CancelCancellationTokenSourceState);
static void CancelCancellationTokenSourceState(object state)
{
var cts = (CancellationTokenSource)state;
cts.Cancel();
}
CancellationTokenSource timeoutSource; CancellationTokenSource timeoutSource;
CancellationTokenSource linkedSource; CancellationTokenSource linkedSource;
StoppableDelayRealtimePromise timeoutDelay; PlayerLoopTimer timer;
bool isDisposed;
readonly DelayType delayType;
readonly PlayerLoopTiming delayTiming;
readonly CancellationTokenSource originalLinkCancellationTokenSource; readonly CancellationTokenSource originalLinkCancellationTokenSource;
public TimeoutController() public TimeoutController(DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
{ {
this.timeoutSource = new CancellationTokenSource(); this.timeoutSource = new CancellationTokenSource();
this.originalLinkCancellationTokenSource = null; this.originalLinkCancellationTokenSource = null;
this.linkedSource = 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.timeoutSource = new CancellationTokenSource();
this.originalLinkCancellationTokenSource = linkCancellationTokenSource; this.originalLinkCancellationTokenSource = linkCancellationTokenSource;
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, linkCancellationTokenSource.Token); 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) public CancellationToken Timeout(TimeSpan timeout)
@ -43,6 +60,7 @@ namespace Cysharp.Threading.Tasks
return originalLinkCancellationTokenSource.Token; return originalLinkCancellationTokenSource.Token;
} }
// Timeouted, create new source and timer.
if (timeoutSource.IsCancellationRequested) if (timeoutSource.IsCancellationRequested)
{ {
timeoutSource.Dispose(); timeoutSource.Dispose();
@ -53,18 +71,26 @@ namespace Cysharp.Threading.Tasks
this.linkedSource.Dispose(); this.linkedSource.Dispose();
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token); 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 else
{ {
timeoutDelay.RestartStopwatch(); // already running RunDelayAsync timer.Restart(timeout);
} }
return (linkedSource != null) ? linkedSource.Token : timeoutSource.Token; return token;
} }
public bool IsTimeout() public bool IsTimeout()
@ -74,184 +100,30 @@ namespace Cysharp.Threading.Tasks
public void Reset() public void Reset()
{ {
if (timeoutDelay != null) timer.Stop();
{
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<DelayResult>(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;
}
} }
public void Dispose() public void Dispose()
{ {
if (timeoutDelay != null) if (isDisposed) return;
try
{ {
timeoutDelay.Stop(); // stop timer.
} timer.Dispose();
timeoutSource.Dispose();
if (linkedSource != null)
{
linkedSource.Dispose();
}
}
enum DelayResult // cancel and dispose.
{ timeoutSource.Cancel();
LinkedTokenCanceled, timeoutSource.Dispose();
ExternalStopped, if (linkedSource != null)
DelayCompleted, // as Timeout.
}
// Stop + SuppressCancellationThrow.
sealed class StoppableDelayRealtimePromise : IUniTaskSource<DelayResult>, IPlayerLoopItem, ITaskPoolNode<StoppableDelayRealtimePromise>
{
static OperationCanceledException ExterenalStopException = new OperationCanceledException();
static TaskPool<StoppableDelayRealtimePromise> 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<DelayResult> core;
StoppableDelayRealtimePromise()
{
}
public static StoppableDelayRealtimePromise Create(TimeSpan delayTimeSpan, PlayerLoopTiming timing, CancellationToken cancellationToken, out short token)
{
if (!pool.TryPop(out var result))
{ {
result = new StoppableDelayRealtimePromise(); linkedSource.Cancel();
} linkedSource.Dispose();
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();
} }
} }
finally
void IUniTaskSource.GetResult(short token)
{ {
GetResult(token); isDisposed = true;
}
public UniTaskStatus GetStatus(short token)
{
return core.GetStatus(token);
}
public UniTaskStatus UnsafeGetStatus()
{
return core.UnsafeGetStatus();
}
public void OnCompleted(Action<object> 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);
} }
} }
} }