Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Created November 14, 2023 14:22
Show Gist options
  • Save ufcpp/a8ab75458aa35bdb1ed600153da86962 to your computer and use it in GitHub Desktop.
Save ufcpp/a8ab75458aa35bdb1ed600153da86962 to your computer and use it in GitHub Desktop.
namespace TimeProviderExtension;
public class PausableTimeProvider(TimeProvider innerProvider) : TimeProvider
{
public PausableTimeProvider() : this(System) { }
private long _pausedTimestamp = innerProvider.GetTimestamp();
private long _offsetTimestamp = 0;
private DateTimeOffset _pausedUtcNow = innerProvider.GetUtcNow();
private TimeSpan _offsetUtcNow = default;
private bool _isPaused = false;
public bool IsPaused
{
get => _isPaused;
set => ChangePaused(value);
}
private event Action<bool>? PauseChanged;
private void ChangePaused(bool value)
{
if (_isPaused == value) return;
if (value)
{
_pausedTimestamp = innerProvider.GetTimestamp();
_pausedUtcNow = innerProvider.GetUtcNow();
}
else
{
var timestamp = innerProvider.GetTimestamp();
_offsetTimestamp += timestamp - _pausedTimestamp;
var now = innerProvider.GetUtcNow();
_offsetUtcNow += now - _pausedUtcNow;
}
_isPaused = value;
PauseChanged?.Invoke(value);
}
public override long GetTimestamp()
{
if(IsPaused) return _pausedTimestamp;
var timestamp = innerProvider.GetTimestamp();
return timestamp - _offsetTimestamp;
}
public override DateTimeOffset GetUtcNow()
{
var now = innerProvider.GetUtcNow();
return now - _offsetUtcNow;
}
private class XTimer : ITimer
{
private readonly PausableTimeProvider _parent;
private readonly ITimer _innerTimer;
private TimeSpan _period;
public XTimer(PausableTimeProvider parent, ITimer innerTimer, TimeSpan period)
{
_parent = parent;
_innerTimer = innerTimer;
_period = period;
parent.PauseChanged += OnPauseChanged;
}
private void OnPauseChanged(bool isPaused)
{
if (isPaused)
{
_innerTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}
else
{
_innerTimer.Change(default, _period); // now - _pausedUtcNow 受け取って due の調整する方がいいかも。
}
}
public bool Change(TimeSpan dueTime, TimeSpan period)
{
if (_parent.IsPaused)
{
_period = period;
return true;
}
else
{
return _innerTimer.Change(dueTime, period);
}
}
public void Dispose()
{
_parent.PauseChanged -= OnPauseChanged;
_innerTimer.Dispose();
}
public async ValueTask DisposeAsync()
{
_parent.PauseChanged -= OnPauseChanged;
await _innerTimer.DisposeAsync();
}
}
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
return new XTimer(this, innerProvider.CreateTimer(callback, state, dueTime, period), period);
}
public override TimeZoneInfo LocalTimeZone => innerProvider.LocalTimeZone;
public override long TimestampFrequency => innerProvider.TimestampFrequency;
}
using TimeProviderExtension;
var scaled = new ScaledTimeProvider() { Scale = 1 };
var pausable = new PausableTimeProvider(scaled);
static async Task LoopAsync(TimeProvider tp, CancellationToken ct)
{
var t = new PeriodicTimer(TimeSpan.FromMilliseconds(500), tp);
while (!ct.IsCancellationRequested)
{
await t.WaitForNextTickAsync(ct);
Console.WriteLine(tp.GetLocalNow());
}
}
var cts = new CancellationTokenSource();
var task = LoopAsync(pausable, cts.Token);
Console.Write("""
p: pause
r: resume
1-9: change scale
q: quit
""");
while (true)
{
var c = Console.ReadKey().KeyChar;
Console.WriteLine();
switch (c)
{
case 'p':
Console.WriteLine("paused");
pausable.IsPaused = true;
break;
case 'r':
Console.WriteLine("resumed");
pausable.IsPaused = false;
break;
case >= '1' and <= '9':
var x = c - '0';
Console.WriteLine($"scale changed to {x}");
scaled.Scale = x;
break;
case 'q':
goto END_LOOP;
}
}
END_LOOP:
cts.Cancel();
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
namespace TimeProviderExtension;
public class ScaledTimeProvider(TimeProvider innerProvider) : TimeProvider
{
public ScaledTimeProvider() : this(System) { }
private long _startTimestamp = innerProvider.GetTimestamp();
private long _offsetTimestamp = 0;
private DateTimeOffset _startUtcNow = innerProvider.GetUtcNow();
private TimeSpan _offsetUtcNow = default;
private double _scale = 1;
public double Scale
{
get => _scale;
set => ChangeScale(value);
}
private event Action<double>? ScaleChanged;
private void ChangeScale(double value)
{
if (_scale == value) return;
var s1 = GetTimestamp();
var s2 = _startTimestamp = innerProvider.GetTimestamp();
_offsetTimestamp = s1 - s2;
var n1 = GetUtcNow();
var n2 = _startUtcNow = innerProvider.GetUtcNow();
_offsetUtcNow = n1 - n2;
_scale = value;
ScaleChanged?.Invoke(value);
}
public override long GetTimestamp()
{
var timestamp = innerProvider.GetTimestamp();
return (long)(_startTimestamp + Scale * (timestamp - _startTimestamp)) + _offsetTimestamp;
}
public override DateTimeOffset GetUtcNow()
{
var now = innerProvider.GetUtcNow();
return _startUtcNow + Scale * (now - _startUtcNow) + _offsetUtcNow;
}
private class XTimer : ITimer
{
private readonly ScaledTimeProvider _parent;
private readonly ITimer _innerTimer;
private TimeSpan _period;
public XTimer(ScaledTimeProvider parent, ITimer innerTimer)
{
_parent = parent;
_innerTimer = innerTimer;
parent.ScaleChanged += OnScaleChanged;
}
private void OnScaleChanged(double scale)
{
var inv = 1 / scale;
_innerTimer.Change(inv * _period, inv * _period);
}
public bool Change(TimeSpan dueTime, TimeSpan period)
{
_period = period;
var inv = 1 / _parent.Scale;
return _innerTimer.Change(inv * dueTime, inv * period);
}
public void Dispose()
{
_parent.ScaleChanged -= OnScaleChanged;
_innerTimer.Dispose();
}
public async ValueTask DisposeAsync()
{
_parent.ScaleChanged -= OnScaleChanged;
await _innerTimer.DisposeAsync();
}
}
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
var t = new XTimer(this, innerProvider.CreateTimer(callback, state, default, default));
t.Change(dueTime, period);
return t;
}
public override TimeZoneInfo LocalTimeZone => innerProvider.LocalTimeZone;
public override long TimestampFrequency => innerProvider.TimestampFrequency;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment