From 8dfe389b116c287c450afa5dced178888269d2d0 Mon Sep 17 00:00:00 2001 From: Alex Yakunin Date: Tue, 18 Aug 2020 03:57:53 -0700 Subject: [PATCH] refactor: KeepAlive is now handled properly. --- .../Interception/ReplicaClientComputed.cs | 1 + src/Stl.Fusion/Bridge/ReplicaRegistry.cs | 47 ++++--- src/Stl.Fusion/Computed.Static.cs | 1 - src/Stl.Fusion/Computed.cs | 23 +--- src/Stl.Fusion/ComputedEx.cs | 25 ++++ src/Stl.Fusion/ComputedOptions.cs | 2 +- src/Stl.Fusion/ComputedRegistry.cs | 119 ++++++------------ src/Stl.Fusion/Internal/ComputedEx.cs | 3 + src/Stl.Fusion/Internal/RefHolder.cs | 58 +++++++++ tests/Stl.Fusion.Tests/KeepAliveTest.cs | 109 ++++++++++++++++ tests/Stl.Fusion.Tests/PerformanceTest.cs | 4 +- tests/Stl.Fusion.Tests/PruneTest.cs | 71 ----------- .../ScreenshotServiceClientTest.cs | 2 +- .../Services/IScreenshotServiceClient.cs | 2 +- .../Services/ScreenshotService.cs | 2 +- .../Stl.Fusion.Tests/Services/UserService.cs | 7 +- tests/Stl.Fusion.Tests/UserProviderTest.cs | 4 +- 17 files changed, 269 insertions(+), 211 deletions(-) create mode 100644 src/Stl.Fusion/Internal/RefHolder.cs create mode 100644 tests/Stl.Fusion.Tests/KeepAliveTest.cs delete mode 100644 tests/Stl.Fusion.Tests/PruneTest.cs diff --git a/src/Stl.Fusion/Bridge/Interception/ReplicaClientComputed.cs b/src/Stl.Fusion/Bridge/Interception/ReplicaClientComputed.cs index 22963d8d8..66dd14062 100644 --- a/src/Stl.Fusion/Bridge/Interception/ReplicaClientComputed.cs +++ b/src/Stl.Fusion/Bridge/Interception/ReplicaClientComputed.cs @@ -49,6 +49,7 @@ protected override void OnInvalidated() // We intentionally suppress ComputedRegistry.Unregister here, // otherwise it won't be possible to find IReplica using // old IComputed. + this.CancelKeepAlive(); } } } diff --git a/src/Stl.Fusion/Bridge/ReplicaRegistry.cs b/src/Stl.Fusion/Bridge/ReplicaRegistry.cs index 7a9ce94f8..832a3acb0 100644 --- a/src/Stl.Fusion/Bridge/ReplicaRegistry.cs +++ b/src/Stl.Fusion/Bridge/ReplicaRegistry.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -126,20 +125,19 @@ public virtual bool Remove(IReplica replica) return true; } - // Private members - - private void UpdatePruneCounterThreshold() + public Task PruneAsync() { lock (Lock) { - // Should be called inside Lock - var capacity = (long) _handles.GetCapacity(); - var nextThreshold = (int) Math.Min(int.MaxValue >> 1, capacity); - _pruneCounterThreshold = nextThreshold; + if (_pruneTask == null || _pruneTask.IsCompleted) + _pruneTask = Task.Run(PruneInternal); + return _pruneTask; } } + // Protected members + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OnOperation(int random) + protected void OnOperation(int random) { if (!_opCounter.Increment(random, out var opCounterValue)) return; @@ -147,39 +145,36 @@ private void OnOperation(int random) TryPrune(); } - private void TryPrune() + protected void TryPrune() { lock (Lock) { // Double check locking if (_opCounter.ApproximateValue <= _pruneCounterThreshold) return; _opCounter.ApproximateValue = 0; - Prune(); + PruneAsync(); } } - private void Prune() + protected virtual void PruneInternal() { + foreach (var (key, gcHandle) in _handles) { + if (gcHandle.Target == null && _handles.TryRemove(key, gcHandle)) + _gcHandlePool.Release(gcHandle, key.PublicationId.HashCode); + } lock (Lock) { - if (_pruneTask == null || _pruneTask.IsCompleted) - _pruneTask = Task.Run(PruneInternal); + UpdatePruneCounterThreshold(); + _opCounter.ApproximateValue = 0; } } - private void PruneInternal() + protected void UpdatePruneCounterThreshold() { - foreach (var (key, gcHandle) in _handles) { - if (gcHandle.Target != null) - continue; - if (!_handles.TryRemove(key, gcHandle)) - continue; - var random = key.PublicationId.HashCode; - _gcHandlePool.Release(gcHandle, random); - } - lock (Lock) { - UpdatePruneCounterThreshold(); - _opCounter.ApproximateValue = 0; + // Should be called inside Lock + var capacity = (long) _handles.GetCapacity(); + var nextThreshold = (int) Math.Min(int.MaxValue >> 1, capacity); + _pruneCounterThreshold = nextThreshold; } } } diff --git a/src/Stl.Fusion/Computed.Static.cs b/src/Stl.Fusion/Computed.Static.cs index 770293c49..df2e10ba0 100644 --- a/src/Stl.Fusion/Computed.Static.cs +++ b/src/Stl.Fusion/Computed.Static.cs @@ -9,7 +9,6 @@ namespace Stl.Fusion { public static class Computed { - public static readonly TimeSpan DefaultKeepAliveTime = TimeSpan.FromSeconds(1); private static readonly AsyncLocal CurrentLocal = new AsyncLocal(); // GetCurrent & ChangeCurrent diff --git a/src/Stl.Fusion/Computed.cs b/src/Stl.Fusion/Computed.cs index 86bead7dc..4c341c2eb 100644 --- a/src/Stl.Fusion/Computed.cs +++ b/src/Stl.Fusion/Computed.cs @@ -8,7 +8,6 @@ using Stl.Collections.Slim; using Stl.Frozen; using Stl.Fusion.Internal; -using Stl.Time; namespace Stl.Fusion { @@ -29,10 +28,8 @@ public interface IComputed : IResult ComputedState State { get; } bool IsConsistent { get; } event Action Invalidated; - Moment LastAccessTime { get; } bool Invalidate(); - void Touch(); TResult Apply(IComputedApplyHandler handler, TArg arg); ValueTask UpdateAsync(bool addDependency, CancellationToken cancellationToken = default); @@ -70,7 +67,6 @@ public class Computed : IComputed, IComputedImpl // ReSharper disable once InconsistentNaming private event Action? _invalidated; private bool _invalidateOnSetOutput; - private long _lastAccessTimeTicks; private object Lock => this; public ComputedOptions Options { @@ -87,13 +83,6 @@ public class Computed : IComputed, IComputedImpl public IFunction Function => (IFunction) Input.Function; public LTag Version { get; } - public Moment LastAccessTime { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new Moment(Volatile.Read(ref _lastAccessTimeTicks)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected set => Volatile.Write(ref _lastAccessTimeTicks, value.EpochOffset.Ticks); - } - public Type OutputType => typeof(TOut); public Result Output { get { @@ -140,7 +129,6 @@ public Computed(ComputedOptions options, TIn input, LTag version) _options = options; Input = input; Version = version; - LastAccessTime = CoarseCpuClock.Now; ComputedRegistry.Instance.Register(this); } @@ -153,7 +141,6 @@ public Computed(ComputedOptions options, TIn input, Result output, LTag ve _state = (int) (isConsistent ? ComputedState.Consistent : ComputedState.Invalidated); _output = output; Version = version; - LastAccessTime = CoarseCpuClock.Now; if (isConsistent) ComputedRegistry.Instance.Register(this); } @@ -290,7 +277,10 @@ public bool Invalidate() } protected virtual void OnInvalidated() - => ComputedRegistry.Instance.Unregister(this); + { + ComputedRegistry.Instance.Unregister(this); + this.CancelKeepAlive(); + } // UpdateAsync @@ -316,11 +306,6 @@ public async ValueTask UseAsync(CancellationToken cancellationToken = defa return computed.Value; } - // Touch - - public void Touch() - => LastAccessTime = CoarseCpuClock.Now; - // Apply public TResult Apply(IComputedApplyHandler handler, TArg arg) diff --git a/src/Stl.Fusion/ComputedEx.cs b/src/Stl.Fusion/ComputedEx.cs index 0a911afc6..ea69e3913 100644 --- a/src/Stl.Fusion/ComputedEx.cs +++ b/src/Stl.Fusion/ComputedEx.cs @@ -1,13 +1,17 @@ +using System; using System.Reactive; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Stl.Async; +using Stl.Fusion.Internal; namespace Stl.Fusion { public static partial class ComputedEx { + private static readonly TimeSpan CancelKeepAliveThreshold = TimeSpan.FromSeconds(1.1); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static T Strip(this IComputed? computed) => computed != null ? computed.Value : default!; @@ -20,5 +24,26 @@ public static Task WhenInvalidatedAsync(this IComputed computed, Cancellat computed.Invalidated += c => ts.SetResult(default); return ts.Task.WithFakeCancellation(cancellationToken); } + + public static void KeepAlive(this IComputed computed) + { + var keepAliveTime = computed.Options.KeepAliveTime; + if (keepAliveTime > TimeSpan.Zero && computed.State != ComputedState.Invalidated) + RefHolder.Hold(computed, keepAliveTime); + } + + public static void CancelKeepAlive(this IComputed computed, TimeSpan threshold) + { + var keepAliveTime = computed.Options.KeepAliveTime; + if (keepAliveTime >= threshold) + RefHolder.Release(computed); + } + + public static void CancelKeepAlive(this IComputed computed) + { + var keepAliveTime = computed.Options.KeepAliveTime; + if (keepAliveTime >= CancelKeepAliveThreshold) + RefHolder.Release(computed); + } } } diff --git a/src/Stl.Fusion/ComputedOptions.cs b/src/Stl.Fusion/ComputedOptions.cs index 6a063f0cc..4d094186c 100644 --- a/src/Stl.Fusion/ComputedOptions.cs +++ b/src/Stl.Fusion/ComputedOptions.cs @@ -6,7 +6,7 @@ namespace Stl.Fusion [Serializable] public class ComputedOptions { - public static readonly TimeSpan DefaultKeepAliveTime = TimeSpan.FromSeconds(1); + public static readonly TimeSpan DefaultKeepAliveTime = TimeSpan.Zero; public static readonly TimeSpan DefaultErrorAutoInvalidateTime = TimeSpan.FromSeconds(1); public static readonly TimeSpan DefaultAutoInvalidateTime = TimeSpan.MaxValue; // No auto invalidation diff --git a/src/Stl.Fusion/ComputedRegistry.cs b/src/Stl.Fusion/ComputedRegistry.cs index 1b65c5a38..730f9b864 100644 --- a/src/Stl.Fusion/ComputedRegistry.cs +++ b/src/Stl.Fusion/ComputedRegistry.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -25,7 +24,7 @@ public sealed class Options public static int DefaultInitialCapacity { get; } public int InitialCapacity { get; set; } = DefaultInitialCapacity; - public int ConcurrencyLevel { get; set; } = HardwareInfo.ProcessorCount; + public int ConcurrencyLevel { get; set; } = HardwareInfo.ProcessorCount << 5; public Func>? LocksProvider { get; set; } = null; public GCHandlePool? GCHandlePool { get; set; } = null; public IMomentClock Clock { get; set; } = CoarseCpuClock.Instance; @@ -41,7 +40,7 @@ static Options() } } - private readonly ConcurrentDictionary _storage; + private readonly ConcurrentDictionary _storage; private readonly Func> _locksProvider; private readonly GCHandlePool _gcHandlePool; private readonly StochasticCounter _opCounter; @@ -53,7 +52,7 @@ static Options() public ComputedRegistry(Options? options = null) { options ??= new Options(); - _storage = new ConcurrentDictionary(options.ConcurrencyLevel, options.InitialCapacity); + _storage = new ConcurrentDictionary(options.ConcurrencyLevel, options.InitialCapacity); var locksProvider = options.LocksProvider; if (locksProvider == null) { var locks = new AsyncLockSet(ReentryMode.CheckedFail); @@ -82,21 +81,11 @@ protected virtual void Dispose(bool disposing) { var random = Randomize(key.HashCode); OnOperation(random); - if (_storage.TryGetValue(key, out var entry)) { - var value = entry.Computed; - if (value != null) { - value.Touch(); + if (_storage.TryGetValue(key, out var handle)) { + var value = (IComputed?) handle.Target; + if (value != null) return value; - } - - var handle = entry.Handle; - value = (IComputed?) handle.Target; - if (value != null) { - value.Touch(); - _storage.TryUpdate(key, new Entry(value, handle), entry); - return value; - } - if (_storage.TryRemove(key, entry)) + if (_storage.TryRemove(key, handle)) _gcHandlePool.Release(handle, random); } return null; @@ -110,15 +99,14 @@ public virtual void Register(IComputed computed) OnOperation(random); var spinWait = new SpinWait(); - Entry? newEntry = null; + GCHandle? newHandle = null; while (computed.State != ComputedState.Invalidated) { - if (_storage.TryGetValue(key, out var entry)) { - var handle = entry.Handle; + if (_storage.TryGetValue(key, out var handle)) { var target = (IComputed?) handle.Target; if (target == computed) break; if (target == null || target.State == ComputedState.Invalidated) { - if (_storage.TryRemove(key, entry)) + if (_storage.TryRemove(key, handle)) _gcHandlePool.Release(handle, random); } else { @@ -128,11 +116,11 @@ public virtual void Register(IComputed computed) } } else { - newEntry ??= new Entry(computed, _gcHandlePool.Acquire(computed, random)); - if (_storage.TryAdd(key, newEntry.GetValueOrDefault())) { + newHandle ??= _gcHandlePool.Acquire(computed, random); + if (_storage.TryAdd(key, newHandle.GetValueOrDefault())) { if (computed.State == ComputedState.Invalidated) { - if (_storage.TryRemove(key, entry)) - _gcHandlePool.Release(entry.Handle, random); + if (_storage.TryRemove(key, handle)) + _gcHandlePool.Release(handle, random); } break; } @@ -152,15 +140,14 @@ public virtual bool Unregister(IComputed computed) var key = computed.Input; var random = Randomize(key.HashCode); OnOperation(random); - if (!_storage.TryGetValue(key, out var entry)) + if (!_storage.TryGetValue(key, out var handle)) return false; - var handle = entry.Handle; var target = handle.Target; if (target != null && !ReferenceEquals(target, computed)) return false; // gcHandle.Target == null (is gone, i.e. to be pruned) // or pointing to the right computation object - if (!_storage.TryRemove(key, entry)) + if (!_storage.TryRemove(key, handle)) // If another thread removed the entry, it also released the handle return false; _gcHandlePool.Release(handle, random); @@ -170,14 +157,19 @@ public virtual bool Unregister(IComputed computed) public virtual IAsyncLockSet GetLocksFor(IFunction function) => _locksProvider.Invoke(function); - // Private members + public Task PruneAsync() + { + lock (Lock) { + if (_pruneTask == null || _pruneTask.IsCompleted) + _pruneTask = Task.Run(PruneInternal); + return _pruneTask; + } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int Randomize(int random) - => random + CoarseStopwatch.RandomInt32; + // Protected members [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OnOperation(int random) + protected void OnOperation(int random) { if (!_opCounter.Increment(random, out var opCounterValue)) return; @@ -185,55 +177,32 @@ private void OnOperation(int random) TryPrune(); } - private void TryPrune() + protected void TryPrune() { lock (Lock) { // Double check locking if (_opCounter.ApproximateValue <= _pruneCounterThreshold) return; _opCounter.ApproximateValue = 0; - Prune(); - } - } - - private void Prune() - { - lock (Lock) { - if (_pruneTask == null || _pruneTask.IsCompleted) - _pruneTask = Task.Run(PruneInternal); + PruneAsync(); } } - private void PruneInternal() + protected virtual void PruneInternal() { // Debug.WriteLine(nameof(PruneInternal)); - var now = _clock.Now; var randomOffset = Randomize(Thread.CurrentThread.ManagedThreadId); - foreach (var (key, entry) in _storage) { - var handle = entry.Handle; - if (handle.Target == null) { - if (_storage.TryRemove(key, entry)) { - var random = key.HashCode + randomOffset; - _gcHandlePool.Release(handle, random); - } - continue; - } - var computed = entry.Computed; - if (computed == null) - continue; - var expirationTime = computed.LastAccessTime + computed.Options.KeepAliveTime; - if (expirationTime >= now) - continue; - _storage.TryUpdate(key, new Entry(null, handle), entry); + foreach (var (key, handle) in _storage) { + if (handle.Target == null && _storage.TryRemove(key, handle)) + _gcHandlePool.Release(handle, key.HashCode + randomOffset); } - lock (Lock) { UpdatePruneCounterThreshold(); _opCounter.ApproximateValue = 0; } } - private void UpdatePruneCounterThreshold() + protected void UpdatePruneCounterThreshold() { lock (Lock) { // Should be called inside Lock @@ -243,24 +212,8 @@ private void UpdatePruneCounterThreshold() } } - private readonly struct Entry : IEquatable - { - public readonly IComputed? Computed; - public readonly GCHandle Handle; - - public Entry(IComputed? computed, GCHandle handle) - { - Computed = computed; - Handle = handle; - } - - public bool Equals(Entry other) - => Computed == other.Computed && Handle == other.Handle; - public override bool Equals(object? obj) - => obj is Entry other && Equals(other); - public override int GetHashCode() => HashCode.Combine(Computed, Handle); - public static bool operator ==(Entry left, Entry right) => left.Equals(right); - public static bool operator !=(Entry left, Entry right) => !left.Equals(right); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int Randomize(int random) + => random + CoarseStopwatch.RandomInt32; } } diff --git a/src/Stl.Fusion/Internal/ComputedEx.cs b/src/Stl.Fusion/Internal/ComputedEx.cs index d0a1614b8..ac4803133 100644 --- a/src/Stl.Fusion/Internal/ComputedEx.cs +++ b/src/Stl.Fusion/Internal/ComputedEx.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.CompilerServices; using System.Threading; @@ -26,6 +27,7 @@ internal static bool TryUseCached(this IComputed? cached, ComputeContext context ((IComputedImpl?) usedBy)?.AddUsed((IComputedImpl) cached!); if ((callOptions & CallOptions.Capture) != 0) Interlocked.Exchange(ref context.CapturedComputed, cached); + cached.KeepAlive(); return true; } @@ -35,6 +37,7 @@ internal static void UseNew(this IComputed computed, ComputeContext context, ICo ((IComputedImpl?) usedBy)?.AddUsed((IComputedImpl) computed); if ((context.CallOptions & CallOptions.Capture) != 0) Interlocked.Exchange(ref context.CapturedComputed, computed); + computed.KeepAlive(); } } } diff --git a/src/Stl.Fusion/Internal/RefHolder.cs b/src/Stl.Fusion/Internal/RefHolder.cs new file mode 100644 index 000000000..d68d45cb5 --- /dev/null +++ b/src/Stl.Fusion/Internal/RefHolder.cs @@ -0,0 +1,58 @@ +using System; +using Stl.OS; +using Stl.Time; + +namespace Stl.Fusion.Internal +{ + public interface INotifyKeepAliveEnded + { + void KeepAliveEnded(); + } + + public static class RefHolder + { + private readonly static ConcurrentTimerSet SimpleTimers; + private readonly static ConcurrentTimerSet NotifyingTimers; + private readonly static IMomentClock Clock; + + static RefHolder() + { + Clock = CoarseCpuClock.Instance; + var quanta = TimeSpan.FromMilliseconds(250); + var concurrencyLevel = HardwareInfo.ProcessorCountPo2 << 5; + SimpleTimers = new ConcurrentTimerSet( + new ConcurrentTimerSet.Options() { + Quanta = quanta, + ConcurrencyLevel = concurrencyLevel, + Clock = Clock, + }); + NotifyingTimers = new ConcurrentTimerSet( + new ConcurrentTimerSet.Options() { + Quanta = quanta, + ConcurrencyLevel = concurrencyLevel, + Clock = Clock, + FireHandler = t => t.KeepAliveEnded(), + }); + } + + public static void Hold(object target, int durationMs) + => Hold(target, Clock.Now + TimeSpan.FromMilliseconds(durationMs)); + public static void Hold(object target, TimeSpan duration) + => Hold(target, Clock.Now + duration); + public static void Hold(object target, Moment until) + { + if (target is INotifyKeepAliveEnded notify) + NotifyingTimers.AddOrUpdateToLater(notify, until); + else + SimpleTimers.AddOrUpdateToLater(target, until); + } + + public static void Release(object target) + { + if (target is INotifyKeepAliveEnded notify) + NotifyingTimers.Remove(notify); + else + SimpleTimers.Remove(target); + } + } +} diff --git a/tests/Stl.Fusion.Tests/KeepAliveTest.cs b/tests/Stl.Fusion.Tests/KeepAliveTest.cs new file mode 100644 index 000000000..b2e74bdb4 --- /dev/null +++ b/tests/Stl.Fusion.Tests/KeepAliveTest.cs @@ -0,0 +1,109 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Stl.Testing; +using Stl.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace Stl.Fusion.Tests +{ + [Collection(nameof(TimeSensitiveTests)), Trait("Category", nameof(TimeSensitiveTests))] + public class KeepAliveTest : TestBase + { + public class Calculator + { + public int CallCount { get; set; } + + [ComputeMethod(KeepAliveTime = 0.5)] + public virtual async Task SumAsync(double a, double b) + { + await Task.Yield(); + CallCount++; + return a + b; + } + + [ComputeMethod] + public virtual async Task MulAsync(double a, double b) + { + await Task.Yield(); + CallCount++; + return a * b; + } + } + + public KeepAliveTest(ITestOutputHelper @out) : base(@out) { } + + public static IServiceProvider CreateProviderFor() + where TService : class + { + ComputedRegistry.Instance = new ComputedRegistry(new ComputedRegistry.Options() { + InitialCapacity = 16, + }); + var services = new ServiceCollection(); + services.AddFusionCore(); + services.AddComputeService(); + return services.BuildServiceProvider(); + } + + [Fact] + public async void TestNoKeepAlive() + { + if (TestRunnerInfo.GitHub.IsActionRunning) + // TODO: Fix intermittent failures on GitHub + return; + + var services = CreateProviderFor(); + var r = ComputedRegistry.Instance; + var c = services.GetRequiredService(); + + c.CallCount = 0; + await c.MulAsync(1, 1); + c.CallCount.Should().Be(1); + await c.MulAsync(1, 1); + c.CallCount.Should().Be(1); + + await GCCollectAsync(); + await c.MulAsync(1, 1); + c.CallCount.Should().Be(2); + } + + [Fact] + public async void TestKeepAlive() + { + if (TestRunnerInfo.GitHub.IsActionRunning) + // TODO: Fix intermittent failures on GitHub + return; + + var services = CreateProviderFor(); + var r = ComputedRegistry.Instance; + var c = services.GetRequiredService(); + + c.CallCount = 0; + await c.SumAsync(1, 1); + c.CallCount.Should().Be(1); + await c.SumAsync(1, 1); + c.CallCount.Should().Be(1); + + await Task.Delay(250); + await GCCollectAsync(); + await c.SumAsync(1, 1); + c.CallCount.Should().Be(1); + + await Task.Delay(1000); + await GCCollectAsync(); + await c.SumAsync(1, 1); + c.CallCount.Should().Be(2); + } + + private async Task GCCollectAsync() + { + GC.Collect(); + await Task.Delay(10); + GC.Collect(); + await Task.Delay(10); + GC.Collect(); // To collect what has finalizers + } + } +} diff --git a/tests/Stl.Fusion.Tests/PerformanceTest.cs b/tests/Stl.Fusion.Tests/PerformanceTest.cs index a041af3a6..2b133104a 100644 --- a/tests/Stl.Fusion.Tests/PerformanceTest.cs +++ b/tests/Stl.Fusion.Tests/PerformanceTest.cs @@ -43,8 +43,8 @@ public override async Task InitializeAsync() await Task.WhenAll(tasks); } - [Fact] - // [Fact(Skip = "Performance")] + // [Fact] + [Fact(Skip = "Performance")] public async Task ComputedPerformanceTest() { if (TestRunnerInfo.IsBuildAgent()) diff --git a/tests/Stl.Fusion.Tests/PruneTest.cs b/tests/Stl.Fusion.Tests/PruneTest.cs deleted file mode 100644 index f50f099e6..000000000 --- a/tests/Stl.Fusion.Tests/PruneTest.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Stl.OS; -using Stl.Testing; -using Stl.Tests; -using Xunit; -using Xunit.Abstractions; - -namespace Stl.Fusion.Tests -{ - [Collection(nameof(TimeSensitiveTests)), Trait("Category", nameof(TimeSensitiveTests))] - public class PruneTest : TestBase - { - public class Calculator - { - public Action? OnSumAsync; - - [ComputeMethod] - public virtual async Task SumAsync(double a, double b) - { - OnSumAsync?.Invoke(a, b); - await Task.Delay(100).ConfigureAwait(false); - return a + b; - } - } - - public PruneTest(ITestOutputHelper @out) : base(@out) { } - - public static IServiceProvider CreateProviderFor() - where TService : class - { - ComputedRegistry.Instance = new ComputedRegistry(new ComputedRegistry.Options() { - InitialCapacity = 16, - }); - var services = new ServiceCollection(); - services.AddFusionCore(); - services.AddComputeService(); - return services.BuildServiceProvider(); - } - - [Fact] - public async void Test() - { - if (TestRunnerInfo.GitHub.IsActionRunning) - // TODO: Fix intermittent failures on GitHub - return; - - var services = CreateProviderFor(); - var r = ComputedRegistry.Instance; - var c = services.GetRequiredService(); - - await c.SumAsync(1, 1); - - await Task.Delay(2000); - var tasks = new List(); - for (var i = 0; i < 20_000 * HardwareInfo.ProcessorCountPo2; i++) - tasks.Add(c.SumAsync(2, i)); - await Task.WhenAll(tasks); - GC.Collect(); - await Task.Delay(1000); - - var failed = true; - c.OnSumAsync = (a, b) => failed = false; - await c.SumAsync(1, 1); - failed.Should().BeFalse(); - } - } -} diff --git a/tests/Stl.Fusion.Tests/ScreenshotServiceClientTest.cs b/tests/Stl.Fusion.Tests/ScreenshotServiceClientTest.cs index ef0fb98aa..f543ae52b 100644 --- a/tests/Stl.Fusion.Tests/ScreenshotServiceClientTest.cs +++ b/tests/Stl.Fusion.Tests/ScreenshotServiceClientTest.cs @@ -28,7 +28,7 @@ public async Task BasicTest() (DateTime.Now - screenshot.CapturedAt).Should().BeLessThan(epsilon); await Task.Delay(TimeSpan.FromSeconds(0.1)); } - ScreenshotController.CallCount.Should().Be(1); + ScreenshotController.CallCount.Should().BeLessThan(5); } } } diff --git a/tests/Stl.Fusion.Tests/Services/IScreenshotServiceClient.cs b/tests/Stl.Fusion.Tests/Services/IScreenshotServiceClient.cs index 10980c171..1eff12a4f 100644 --- a/tests/Stl.Fusion.Tests/Services/IScreenshotServiceClient.cs +++ b/tests/Stl.Fusion.Tests/Services/IScreenshotServiceClient.cs @@ -10,7 +10,7 @@ namespace Stl.Fusion.Tests.Services [BasePath("screenshot")] public interface IScreenshotServiceClient : IRestEaseReplicaClient { - [Get("getScreenshot"), ComputeMethod] + [Get("getScreenshot"), ComputeMethod(KeepAliveTime = 0.3)] Task GetScreenshotAsync(int width, CancellationToken cancellationToken = default); } } diff --git a/tests/Stl.Fusion.Tests/Services/ScreenshotService.cs b/tests/Stl.Fusion.Tests/Services/ScreenshotService.cs index c524db525..a9c99b99a 100644 --- a/tests/Stl.Fusion.Tests/Services/ScreenshotService.cs +++ b/tests/Stl.Fusion.Tests/Services/ScreenshotService.cs @@ -30,7 +30,7 @@ public Screenshot(int width, int height, DateTime capturedAt, string base64Conte public interface IScreenshotService { - [ComputeMethod] + [ComputeMethod(KeepAliveTime = 0.3)] Task GetScreenshotAsync(int width, CancellationToken cancellationToken = default); } diff --git a/tests/Stl.Fusion.Tests/Services/UserService.cs b/tests/Stl.Fusion.Tests/Services/UserService.cs index b49dccc43..88d9189ab 100644 --- a/tests/Stl.Fusion.Tests/Services/UserService.cs +++ b/tests/Stl.Fusion.Tests/Services/UserService.cs @@ -18,7 +18,10 @@ public interface IUserService Task CreateAsync(User user, bool orUpdate = false, CancellationToken cancellationToken = default); Task UpdateAsync(User user, CancellationToken cancellationToken = default); Task DeleteAsync(User user, CancellationToken cancellationToken = default); + + [ComputeMethod(KeepAliveTime = 1)] Task TryGetAsync(long userId, CancellationToken cancellationToken = default); + [ComputeMethod(KeepAliveTime = 1)] Task CountAsync(CancellationToken cancellationToken = default); void Invalidate(); } @@ -87,7 +90,6 @@ public virtual async Task DeleteAsync(User user, CancellationToken cancell } } - [ComputeMethod] public virtual async Task TryGetAsync(long userId, CancellationToken cancellationToken = default) { // Debug.WriteLine($"TryGetAsync {userId}"); @@ -99,13 +101,12 @@ public virtual async Task DeleteAsync(User user, CancellationToken cancell return user; } - [ComputeMethod(KeepAliveTime = 5)] public virtual async Task CountAsync(CancellationToken cancellationToken = default) { await Everything().ConfigureAwait(false); await using var dbContext = DbContextPool.Rent(); var count = await dbContext.Users.LongCountAsync(cancellationToken).ConfigureAwait(false); - _log.LogDebug($"Users.Count query: {count}"); + // _log.LogDebug($"Users.Count query: {count}"); return count; } diff --git a/tests/Stl.Fusion.Tests/UserProviderTest.cs b/tests/Stl.Fusion.Tests/UserProviderTest.cs index de5b9febc..c4b883eb2 100644 --- a/tests/Stl.Fusion.Tests/UserProviderTest.cs +++ b/tests/Stl.Fusion.Tests/UserProviderTest.cs @@ -157,8 +157,8 @@ public async Task KeepAliveTimeTest() var cUser0 = await Computed.CaptureAsync(_ => users.TryGetAsync(0)); var cCount = await Computed.CaptureAsync(_ => users.CountAsync()); - cUser0!.Options.KeepAliveTime.Should().Be(Computed.DefaultKeepAliveTime); - cCount!.Options.KeepAliveTime.Should().Be(TimeSpan.FromSeconds(5)); + cUser0!.Options.KeepAliveTime.Should().Be(TimeSpan.FromSeconds(1)); + cCount!.Options.KeepAliveTime.Should().Be(TimeSpan.FromSeconds(1)); } } }