From cdd06ce5eb3d3b91dc20439063b0b95190501474 Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 19 Jun 2024 13:31:36 -0400 Subject: [PATCH 1/4] no concurrent and single cache --- cli/Benchmarks/Benchmarks.csproj | 18 +++ .../DependencyProviderBenchmarks.cs | 64 ++++++++ .../MultiThreadedAccessBenchmarks.cs | 37 +++++ cli/Benchmarks/Program.cs | 12 ++ .../Dependencies/DependencyProvider.cs | 140 +++++++++--------- cli/cli.sln | 6 + cli/tests/DI/MultiThreadedAccessTests.cs | 37 +++++ 7 files changed, 243 insertions(+), 71 deletions(-) create mode 100644 cli/Benchmarks/Benchmarks.csproj create mode 100644 cli/Benchmarks/DependencyProviderBenchmarks.cs create mode 100644 cli/Benchmarks/MultiThreadedAccessBenchmarks.cs create mode 100644 cli/Benchmarks/Program.cs create mode 100644 cli/tests/DI/MultiThreadedAccessTests.cs diff --git a/cli/Benchmarks/Benchmarks.csproj b/cli/Benchmarks/Benchmarks.csproj new file mode 100644 index 0000000000..1838a372fb --- /dev/null +++ b/cli/Benchmarks/Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + Exe + + + + + + + + + + + diff --git a/cli/Benchmarks/DependencyProviderBenchmarks.cs b/cli/Benchmarks/DependencyProviderBenchmarks.cs new file mode 100644 index 0000000000..c37d5ccf24 --- /dev/null +++ b/cli/Benchmarks/DependencyProviderBenchmarks.cs @@ -0,0 +1,64 @@ +using Beamable.Common.Dependencies; +using BenchmarkDotNet.Attributes; +using System.Collections.Concurrent; + +namespace Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob] +public class DependencyProviderBenchmarks +{ + // [Benchmark] + // public void JustAService() + // { + // var x = new ServiceDescriptor(); + // } + // [Benchmark] + // public void Dict_Concurrent() + // { + // var x = new ConcurrentDictionary(); + // } + // [Benchmark] + // public void Dict_Regular() + // { + // var x = new Dictionary(); + // } + // [Benchmark] + public void BaseCase_NoDispose() + { + var builder = new DependencyBuilder(); + // builder.AddSingleton(); + var provider = builder.Build(); + + // var service = provider.GetService(); + } + // + [Benchmark] + public void BaseCase_NoDispose_RegisterAndResolve() + { + var builder = new DependencyBuilder(); + builder.AddSingleton(()=>new TestService()); + var provider = builder.Build(); + var service = provider.GetService(); + // var serv = new TestService(); + } + // + // + // [Benchmark] + // public void BaseCase_Dispose() + // { + // var builder = new DependencyBuilder(); + // // builder.AddSingleton(); + // var provider = builder.Build(); + // + // // var service = provider.GetService(); + // + // provider.Dispose(); + // } + + + public class TestService + { + + } +} diff --git a/cli/Benchmarks/MultiThreadedAccessBenchmarks.cs b/cli/Benchmarks/MultiThreadedAccessBenchmarks.cs new file mode 100644 index 0000000000..700a45a812 --- /dev/null +++ b/cli/Benchmarks/MultiThreadedAccessBenchmarks.cs @@ -0,0 +1,37 @@ +using Beamable.Common.Dependencies; +using BenchmarkDotNet.Attributes; + +namespace Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob] +public class MultiThreadedAccessBenchmarks +{ + [Benchmark] + public async Task SingletonOnlyGetsMadeOnce() + { + var builder = new DependencyBuilder(); + int count = 0; + builder.AddSingleton(_ => + { + count++; + return new A(); + }); + var provider = builder.Build(); + + var tasks = Enumerable.Range(0, 10_000).Select(i => Task.Run(async () => + { + await Task.Delay(1); + provider.GetService(); + })); + + await Task.WhenAll(tasks); + + + } + + public class A + { + // no-op class just for testing + } +} diff --git a/cli/Benchmarks/Program.cs b/cli/Benchmarks/Program.cs new file mode 100644 index 0000000000..077227140d --- /dev/null +++ b/cli/Benchmarks/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Running; + +namespace Benchmarks; + +public class Program +{ + public static void Main(string[] args) + { + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + } +} diff --git a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs index 13a539ede1..e18eb70e18 100644 --- a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs +++ b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs @@ -165,14 +165,29 @@ public interface IDependencyProviderScope : IDependencyProvider void RemoveChild(IDependencyProviderScope child); } + enum DependencyWorkItemType + { + NOOP, + GET_SINGLETON + } + + struct DependencyWorkItem + { + public DependencyWorkItemType type; + public Type typeArg; + } + public class DependencyProvider : IDependencyProviderScope { - private ConcurrentDictionary Transients { get; set; } - private ConcurrentDictionary Scoped { get; set; } - private ConcurrentDictionary Singletons { get; set; } + private Queue _workItems = new Queue(); + + private Dictionary Transients { get; set; } + private Dictionary Scoped { get; set; } + private Dictionary Singletons { get; set; } - private ConcurrentDictionary SingletonCache { get; set; } = new ConcurrentDictionary(); - private ConcurrentDictionary ScopeCache { get; set; } = new ConcurrentDictionary(); + private Dictionary InstanceCache { get; set; } = new Dictionary(); + // private Dictionary SingletonCache { get; set; } = new Dictionary(); + // private Dictionary ScopeCache { get; set; } = new Dictionary(); private bool _destroyed; private bool _isDestroying; @@ -203,31 +218,22 @@ public DependencyProvider(DependencyBuilder builder, BuildOptions options = null } _options = options; - Transients = new ConcurrentDictionary(); + Transients = new Dictionary(); foreach (var desc in builder.TransientServices) { - if (!Transients.TryAdd(desc.Interface, desc)) - { - Debug.LogError($"Failed to add transient interface=[{desc.Interface.Name}] because it already existed."); - } + Transients.Add(desc.Interface, desc); } - Scoped = new ConcurrentDictionary(); + Scoped = new Dictionary(); foreach (var desc in builder.ScopedServices) { - if (!Scoped.TryAdd(desc.Interface, desc)) - { - Debug.LogError($"Failed to add scoped interface=[{desc.Interface.Name}] because it already existed."); - } + Scoped.Add(desc.Interface, desc); } - Singletons = new ConcurrentDictionary(); + Singletons = new Dictionary(); foreach (var desc in builder.SingletonServices) { - if (!Singletons.TryAdd(desc.Interface, desc)) - { - Debug.LogError($"Failed to add singleton interface=[{desc.Interface.Name}] because it already existed."); - } + Singletons.Add(desc.Interface, desc); } } @@ -297,47 +303,52 @@ public bool CanBuildService(Type t) return Transients.ContainsKey(t) || Scoped.ContainsKey(t) || Singletons.ContainsKey(t) || (Parent?.CanBuildService(t) ?? false); } + public object GetService(Type t) { - if (_destroyed) throw new ServiceScopeDisposedException(nameof(GetService), t, this); - - if (t == typeof(IDependencyProvider)) return this; - if (t == typeof(IDependencyProviderScope)) return this; - - if (Transients.TryGetValue(t, out var descriptor)) + lock (this) { - var service = descriptor.Factory(this); - return service; - } + if (_destroyed) throw new ServiceScopeDisposedException(nameof(GetService), t, this); - if (Scoped.TryGetValue(t, out descriptor)) - { - if (ScopeCache.TryGetValue(t, out var instance)) + if (t == typeof(IDependencyProvider)) return this; + if (t == typeof(IDependencyProviderScope)) return this; + + if (Transients.TryGetValue(t, out var descriptor)) { - return instance; + var service = descriptor.Factory(this); + return service; } - return ScopeCache[t] = descriptor.Factory(this); - } + if (Scoped.TryGetValue(t, out descriptor)) + { + if (InstanceCache.TryGetValue(t, out var instance)) + { + return instance; + } + return InstanceCache[t] = descriptor.Factory(this); + } - if (Singletons.TryGetValue(t, out descriptor)) - { - if (SingletonCache.TryGetValue(t, out var instance)) + + if (Singletons.TryGetValue(t, out descriptor)) { - return instance; + if (InstanceCache.TryGetValue(t, out var instance)) + { + return instance; + } + + return InstanceCache[t] = descriptor.Factory(this); } - return SingletonCache[t] = descriptor.Factory(this); - } - if (Parent != null) - { - return Parent.GetService(t); - } + if (Parent != null) + { + return Parent.GetService(t); + } - throw new Exception($"Service not found {t.Name}"); + throw new Exception($"Service not found {t.Name}"); + } } List> childRemovalPromises = new List>(); @@ -401,7 +412,7 @@ async Promise DisposeServices(IEnumerable services) } } - void ClearServices(ConcurrentDictionary descriptors) + void ClearServices(Dictionary descriptors) { foreach (var kvp in descriptors) { @@ -414,11 +425,9 @@ void ClearServices(ConcurrentDictionary descriptors) } - await DisposeServices(SingletonCache.Values.Distinct()); - await DisposeServices(ScopeCache.Values.Distinct()); + await DisposeServices(InstanceCache.Values.Distinct()); - SingletonCache.Clear(); - ScopeCache.Clear(); + InstanceCache.Clear(); if (!_options.allowHydration) { @@ -432,8 +441,7 @@ void ClearServices(ConcurrentDictionary descriptors) Transients = null; Scoped = null; - SingletonCache = null; - ScopeCache = null; + InstanceCache = null; } _destroyed = true; @@ -445,32 +453,22 @@ public void Hydrate(IDependencyProviderScope other) _destroyed = other.IsDisposed; _isDestroying = false; - Transients = new ConcurrentDictionary(); + Transients = new Dictionary(); foreach (var desc in other.TransientServices) { - if (!Transients.TryAdd(desc.Interface, desc)) - { - Debug.LogError($"Failed to hydrate transient interface=[{desc.Interface.Name}] because it already existed"); - } + Transients.Add(desc.Interface, desc); } - Scoped = new ConcurrentDictionary(); + Scoped = new Dictionary(); foreach (var desc in other.ScopedServices) { - if (!Scoped.TryAdd(desc.Interface, desc)) - { - Debug.LogError($"Failed to hydrate scoped interface=[{desc.Interface.Name}] because it already existed"); - } + Scoped.Add(desc.Interface, desc); } - Singletons = new ConcurrentDictionary(); + Singletons = new Dictionary(); foreach (var desc in other.SingletonServices) { - if (!Singletons.TryAdd(desc.Interface, desc)) - { - Debug.LogError($"Failed to hydrate singleton interface=[{desc.Interface.Name}] because it already existed"); - } + Singletons.Add(desc.Interface, desc); } - SingletonCache.Clear(); - ScopeCache.Clear(); + InstanceCache.Clear(); lock (_children) { @@ -502,7 +500,7 @@ public void RemoveChild(IDependencyProviderScope child) } void AddDescriptors(List target, - ConcurrentDictionary source, + Dictionary source, Func factory) { foreach (var kvp in source) diff --git a/cli/cli.sln b/cli/cli.sln index 1bd4245495..b2199da2ea 100644 --- a/cli/cli.sln +++ b/cli/cli.sln @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "beamable.server.common", "b EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unityenginestubs.addressables", "..\microservice\unityEngineStubs.addressables\unityenginestubs.addressables.csproj", "{04402A61-B144-4536-BCA1-E2122B342B04}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{B78AE042-D90B-4E38-AEC6-F0CC61E4DA3C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,5 +50,9 @@ Global {04402A61-B144-4536-BCA1-E2122B342B04}.Debug|Any CPU.Build.0 = Debug|Any CPU {04402A61-B144-4536-BCA1-E2122B342B04}.Release|Any CPU.ActiveCfg = Release|Any CPU {04402A61-B144-4536-BCA1-E2122B342B04}.Release|Any CPU.Build.0 = Release|Any CPU + {B78AE042-D90B-4E38-AEC6-F0CC61E4DA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B78AE042-D90B-4E38-AEC6-F0CC61E4DA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B78AE042-D90B-4E38-AEC6-F0CC61E4DA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B78AE042-D90B-4E38-AEC6-F0CC61E4DA3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/cli/tests/DI/MultiThreadedAccessTests.cs b/cli/tests/DI/MultiThreadedAccessTests.cs new file mode 100644 index 0000000000..73f1a88d7f --- /dev/null +++ b/cli/tests/DI/MultiThreadedAccessTests.cs @@ -0,0 +1,37 @@ +using Beamable.Common.Dependencies; +using NUnit.Framework; +using System.Linq; +using System.Threading.Tasks; + +namespace tests.DI; + +public class MultiThreadedAccessTests +{ + [Test] + public async Task SingletonOnlyGetsMadeOnce() + { + var builder = new DependencyBuilder(); + int count = 0; + builder.AddSingleton(_ => + { + count++; + return new A(); + }); + var provider = builder.Build(); + + var tasks = Enumerable.Range(0, 10_000).Select(i => Task.Run(async () => + { + await Task.Delay(1); + provider.GetService(); + })); + + await Task.WhenAll(tasks); + + Assert.That(count, Is.EqualTo(1), "the factory function should only be invoked once."); + } + + public class A + { + // no-op class just for testing + } +} From f543c0c155bbc19187b66259d775fe0b1ec2fb07 Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 19 Jun 2024 15:28:39 -0400 Subject: [PATCH 2/4] collapse dicts into one --- .../DependencyProviderBenchmarks.cs | 33 +-- .../Runtime/Dependencies/DependencyBuilder.cs | 111 ++++++---- .../Dependencies/DependencyProvider.cs | 194 +++++++++--------- .../Runtime/Dependencies/Utils.cs | 4 +- .../MicroserviceBootstrapper.cs | 5 +- 5 files changed, 186 insertions(+), 161 deletions(-) diff --git a/cli/Benchmarks/DependencyProviderBenchmarks.cs b/cli/Benchmarks/DependencyProviderBenchmarks.cs index c37d5ccf24..486076dd14 100644 --- a/cli/Benchmarks/DependencyProviderBenchmarks.cs +++ b/cli/Benchmarks/DependencyProviderBenchmarks.cs @@ -23,6 +23,13 @@ public class DependencyProviderBenchmarks // { // var x = new Dictionary(); // } + + // [Benchmark] + public void BaseCase_JustProvider() + { + var provider = new DependencyProvider(null, null); + } + // [Benchmark] public void BaseCase_NoDispose() { @@ -37,24 +44,24 @@ public void BaseCase_NoDispose() public void BaseCase_NoDispose_RegisterAndResolve() { var builder = new DependencyBuilder(); - builder.AddSingleton(()=>new TestService()); + // builder.AddSingleton(); var provider = builder.Build(); - var service = provider.GetService(); + // var service = provider.GetService(); // var serv = new TestService(); } // // - // [Benchmark] - // public void BaseCase_Dispose() - // { - // var builder = new DependencyBuilder(); - // // builder.AddSingleton(); - // var provider = builder.Build(); - // - // // var service = provider.GetService(); - // - // provider.Dispose(); - // } + [Benchmark] + public void BaseCase_Dispose() + { + var builder = new DependencyBuilder(); + // builder.AddSingleton(); + var provider = builder.Build(); + + // var service = provider.GetService(); + + provider.Dispose(); + } public class TestService diff --git a/cli/beamable.common/Runtime/Dependencies/DependencyBuilder.cs b/cli/beamable.common/Runtime/Dependencies/DependencyBuilder.cs index 77176ff41c..875bbf2285 100644 --- a/cli/beamable.common/Runtime/Dependencies/DependencyBuilder.cs +++ b/cli/beamable.common/Runtime/Dependencies/DependencyBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; @@ -8,10 +9,10 @@ namespace Beamable.Common.Dependencies public enum DependencyLifetime { - Unknown, - Transient, - Scoped, - Singleton + Unknown = 1, + Transient = 2, + Scoped = 3, + Singleton = 4 } /// @@ -449,18 +450,21 @@ public class BuildOptions /// public class DependencyBuilder : IDependencyBuilder { - public List TransientServices { get; protected set; } = new List(); - public List ScopedServices { get; protected set; } = new List(); - public List SingletonServices { get; protected set; } = new List(); + // public List TransientServices { get; protected set; } = new List(); + // public List ScopedServices { get; protected set; } = new List(); + // public List SingletonServices { get; protected set; } = new List(); - List IDependencyBuilder.GetTransientServices() => TransientServices; - List IDependencyBuilder.GetScopedServices() => ScopedServices; - List IDependencyBuilder.GetSingletonServices() => SingletonServices; + public List Descriptors { get; protected set; } = new List(); + + List IDependencyBuilder.GetTransientServices() => Descriptors.Where(x => x.Lifetime == DependencyLifetime.Transient).ToList(); + List IDependencyBuilder.GetScopedServices() => Descriptors.Where(x => x.Lifetime == DependencyLifetime.Scoped).ToList(); + List IDependencyBuilder.GetSingletonServices() => Descriptors.Where(x => x.Lifetime == DependencyLifetime.Singleton).ToList(); public IDependencyBuilder AddTransient(Func factory) where TImpl : TInterface { - TransientServices.Add(new ServiceDescriptor + Descriptors.Add(new ServiceDescriptor { + Lifetime = DependencyLifetime.Transient, Interface = typeof(TInterface), Implementation = typeof(TImpl), Factory = (provider) => factory(provider) @@ -482,8 +486,9 @@ public IDependencyBuilder AddTransient() where TImpl : TInter public IDependencyBuilder AddScoped(Func factory) where TImpl : TInterface { - ScopedServices.Add(new ServiceDescriptor + Descriptors.Add(new ServiceDescriptor { + Lifetime = DependencyLifetime.Scoped, Interface = typeof(TInterface), Implementation = typeof(TImpl), Factory = (provider) => factory(provider) @@ -511,8 +516,9 @@ public IDependencyBuilder AddScoped() where TImpl : TInterfac public IDependencyBuilder AddSingleton(Func factory) where TImpl : TInterface { - SingletonServices.Add(new ServiceDescriptor + Descriptors.Add(new ServiceDescriptor { + Lifetime = DependencyLifetime.Singleton, Interface = typeof(TInterface), Implementation = typeof(TImpl), Factory = (provider) => factory(provider) @@ -522,8 +528,9 @@ public IDependencyBuilder AddSingleton(Func Instantiate(type, provider) @@ -534,8 +541,9 @@ public IDependencyBuilder AddSingleton(Type type) public IDependencyBuilder AddSingleton(Type registeringType, Func factory) { System.Diagnostics.Debug.Assert(typeof(T).IsAssignableFrom(registeringType), $"RegisteringType [{registeringType.Name}] does not implement/inherit from [{typeof(T).Name}]!"); - SingletonServices.Add(new ServiceDescriptor + Descriptors.Add(new ServiceDescriptor { + Lifetime = DependencyLifetime.Singleton, Interface = registeringType, Implementation = registeringType, Factory = (provider) => factory() @@ -584,8 +592,9 @@ public IDependencyBuilder ReplaceSingleton(Func(); } - SingletonServices.Add(new ServiceDescriptor + Descriptors.Add(new ServiceDescriptor { + Lifetime = DependencyLifetime.Singleton, Interface = typeof(TExisting), Implementation = typeof(TNew), Factory = provider => factory(provider) @@ -677,37 +686,56 @@ public IDependencyProviderScope Build(BuildOptions options = null) public IDependencyBuilder Remove() { - if (TryGetTransient(typeof(T), out var transient)) + // foreach (var service in Descriptors) + for (var i = 0 ; i < Descriptors.Count; i ++) { - TransientServices.Remove(transient); - return this; - } - - if (TryGetScoped(typeof(T), out var scoped)) - { - ScopedServices.Remove(scoped); - return this; - } - - if (TryGetSingleton(typeof(T), out var singleton)) - { - SingletonServices.Remove(singleton); - return this; + if (Descriptors[i].Interface == typeof(T)) + { + Descriptors.RemoveAt(i); + return this; + } } + + // if (TryGetTransient(typeof(T), out var transient)) + // { + // TransientServices.Remove(transient); + // return this; + // } + // + // if (TryGetScoped(typeof(T), out var scoped)) + // { + // ScopedServices.Remove(scoped); + // return this; + // } + // + // if (TryGetSingleton(typeof(T), out var singleton)) + // { + // SingletonServices.Remove(singleton); + // return this; + // } throw new Exception($"Service does not exist, so cannot be removed. type=[{typeof(T)}]"); } public bool Has() { - return TryGetTransient(typeof(T), out _) || TryGetScoped(typeof(T), out _) || TryGetSingleton(typeof(T), out _); + for (var i = 0 ; i < Descriptors.Count; i ++) + { + if (Descriptors[i].Interface == typeof(T)) + { + return true; + } + } + + return false; } + [Obsolete] public bool TryGetTransient(Type type, out ServiceDescriptor descriptor) { - foreach (var serviceDescriptor in TransientServices) + foreach (var serviceDescriptor in Descriptors) { - if (serviceDescriptor.Interface == type) + if (serviceDescriptor.Interface == type && serviceDescriptor.Lifetime == DependencyLifetime.Transient) { descriptor = serviceDescriptor; return true; @@ -717,11 +745,13 @@ public bool TryGetTransient(Type type, out ServiceDescriptor descriptor) descriptor = default(ServiceDescriptor); return false; } + + [Obsolete] public bool TryGetScoped(Type type, out ServiceDescriptor descriptor) { - foreach (var serviceDescriptor in ScopedServices) + foreach (var serviceDescriptor in Descriptors) { - if (serviceDescriptor.Interface == type) + if (serviceDescriptor.Interface == type && serviceDescriptor.Lifetime == DependencyLifetime.Scoped) { descriptor = serviceDescriptor; return true; @@ -732,11 +762,12 @@ public bool TryGetScoped(Type type, out ServiceDescriptor descriptor) return false; } + [Obsolete] public bool TryGetSingleton(Type type, out ServiceDescriptor descriptor) { - foreach (var serviceDescriptor in SingletonServices) + foreach (var serviceDescriptor in Descriptors) { - if (serviceDescriptor.Interface == type) + if (serviceDescriptor.Interface == type && serviceDescriptor.Lifetime == DependencyLifetime.Singleton) { descriptor = serviceDescriptor; return true; @@ -752,9 +783,7 @@ public IDependencyBuilder Clone() { return new DependencyBuilder { - ScopedServices = new List(ScopedServices), - TransientServices = new List(TransientServices), - SingletonServices = new List(SingletonServices) + Descriptors = new List(Descriptors), }; } } diff --git a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs index e18eb70e18..bea67efc8b 100644 --- a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs +++ b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs @@ -164,26 +164,15 @@ public interface IDependencyProviderScope : IDependencyProvider /// void RemoveChild(IDependencyProviderScope child); } - - enum DependencyWorkItemType - { - NOOP, - GET_SINGLETON - } - struct DependencyWorkItem - { - public DependencyWorkItemType type; - public Type typeArg; - } public class DependencyProvider : IDependencyProviderScope { - private Queue _workItems = new Queue(); - private Dictionary Transients { get; set; } - private Dictionary Scoped { get; set; } - private Dictionary Singletons { get; set; } + // private Dictionary Transients { get; set; } + // private Dictionary Scoped { get; set; } + // private Dictionary Singletons { get; set; } + private Dictionary Descriptors { get; set; } private Dictionary InstanceCache { get; set; } = new Dictionary(); // private Dictionary SingletonCache { get; set; } = new Dictionary(); @@ -195,9 +184,9 @@ public class DependencyProvider : IDependencyProviderScope public bool IsDisposed => _destroyed; public bool IsActive => !_isDestroying && !_destroyed; - public IEnumerable TransientServices => Transients.Values; - public IEnumerable ScopedServices => Scoped.Values; - public IEnumerable SingletonServices => Singletons.Values; + public IEnumerable TransientServices => Descriptors.Values.Where(x => x.Lifetime == DependencyLifetime.Transient); + public IEnumerable ScopedServices => Descriptors.Values.Where(x => x.Lifetime == DependencyLifetime.Scoped); + public IEnumerable SingletonServices => Descriptors.Values.Where(x => x.Lifetime == DependencyLifetime.Singleton); public IDependencyProviderScope Parent { get; private set; } private HashSet _children = new HashSet(); @@ -218,37 +207,33 @@ public DependencyProvider(DependencyBuilder builder, BuildOptions options = null } _options = options; - Transients = new Dictionary(); - foreach (var desc in builder.TransientServices) - { - Transients.Add(desc.Interface, desc); - } + Descriptors = new Dictionary(); - Scoped = new Dictionary(); - foreach (var desc in builder.ScopedServices) + if (builder == null) return; + + foreach (var desc in builder.Descriptors) { - Scoped.Add(desc.Interface, desc); - } + if (Descriptors.TryGetValue(desc.Interface, out var existingDesc)) + { + if (existingDesc.Lifetime <= desc.Lifetime) + { + throw new Exception( + $"Cannot add service=[{existingDesc.Interface.Name}] to scope as lifetime=[{desc.Lifetime}], because the service has already been added to the scope as existing-lifetime=[{existingDesc.Lifetime}]. "); + } - Singletons = new Dictionary(); - foreach (var desc in builder.SingletonServices) - { - Singletons.Add(desc.Interface, desc); + Descriptors[desc.Interface] = desc; + } + else + { + Descriptors.Add(desc.Interface, desc); + } } } public bool TryGetServiceDescriptor(out ServiceDescriptor descriptor) { descriptor = null; - if (Transients.TryGetValue(typeof(T), out descriptor)) - { - return true; - } - if (Scoped.TryGetValue(typeof(T), out descriptor)) - { - return true; - } - if (Singletons.TryGetValue(typeof(T), out descriptor)) + if (Descriptors.TryGetValue(typeof(T), out descriptor)) { return true; } @@ -260,19 +245,9 @@ public bool TryGetServiceLifetime(out DependencyLifetime lifetime) { lifetime = DependencyLifetime.Unknown; - if (Transients.TryGetValue(typeof(T), out _)) + if (Descriptors.TryGetValue(typeof(T), out var desc)) { - lifetime = DependencyLifetime.Transient; - return true; - } - if (Scoped.TryGetValue(typeof(T), out _)) - { - lifetime = DependencyLifetime.Scoped; - return true; - } - if (Singletons.TryGetValue(typeof(T), out _)) - { - lifetime = DependencyLifetime.Singleton; + lifetime = desc.Lifetime; return true; } @@ -300,7 +275,7 @@ public bool CanBuildService(Type t) { if (_destroyed) throw new ServiceScopeDisposedException(nameof(CanBuildService), t, this); - return Transients.ContainsKey(t) || Scoped.ContainsKey(t) || Singletons.ContainsKey(t) || (Parent?.CanBuildService(t) ?? false); + return Descriptors.ContainsKey(t) || (Parent?.CanBuildService(t) ?? false); } @@ -313,34 +288,25 @@ public object GetService(Type t) if (t == typeof(IDependencyProvider)) return this; if (t == typeof(IDependencyProviderScope)) return this; - if (Transients.TryGetValue(t, out var descriptor)) + if (Descriptors.TryGetValue(t, out var descriptor)) { - var service = descriptor.Factory(this); - return service; - } - - if (Scoped.TryGetValue(t, out descriptor)) - { - if (InstanceCache.TryGetValue(t, out var instance)) + if (descriptor.Lifetime != DependencyLifetime.Transient) { - return instance; + if (InstanceCache.TryGetValue(t, out var instance)) + { + return instance; + } + else + { + return InstanceCache[t] = descriptor.Factory(this); + } } - - return InstanceCache[t] = descriptor.Factory(this); - } - - - if (Singletons.TryGetValue(t, out descriptor)) - { - if (InstanceCache.TryGetValue(t, out var instance)) + else { - return instance; + return descriptor.Factory(this); } - - return InstanceCache[t] = descriptor.Factory(this); } - if (Parent != null) { return Parent.GetService(t); @@ -434,12 +400,8 @@ void ClearServices(Dictionary descriptors) // remove from parent. Parent?.RemoveChild(this); - ClearServices(Singletons); - ClearServices(Transients); - ClearServices(Scoped); - Singletons = null; - Transients = null; - Scoped = null; + ClearServices(Descriptors); + Descriptors = null; InstanceCache = null; } @@ -453,21 +415,23 @@ public void Hydrate(IDependencyProviderScope other) _destroyed = other.IsDisposed; _isDestroying = false; - Transients = new Dictionary(); - foreach (var desc in other.TransientServices) - { - Transients.Add(desc.Interface, desc); - } - Scoped = new Dictionary(); - foreach (var desc in other.ScopedServices) - { - Scoped.Add(desc.Interface, desc); - } - Singletons = new Dictionary(); + Descriptors = new Dictionary(); foreach (var desc in other.SingletonServices) - { - Singletons.Add(desc.Interface, desc); - } + // Transients = new Dictionary(); + // foreach (var desc in other.TransientServices) + // { + // Transients.Add(desc.Interface, desc); + // } + // Scoped = new Dictionary(); + // foreach (var desc in other.ScopedServices) + // { + // Scoped.Add(desc.Interface, desc); + // } + // Singletons = new Dictionary(); + // foreach (var desc in other.SingletonServices) + // { + // Singletons.Add(desc.Interface, desc); + // } InstanceCache.Clear(); lock (_children) @@ -529,15 +493,41 @@ public IDependencyProviderScope Fork(Action configure = null var builder = new DependencyBuilder(); // populate all of the existing services we have in this scope. + foreach (var source in Descriptors) + { + switch (source.Value.Lifetime) + { + case DependencyLifetime.Transient: + case DependencyLifetime.Scoped: + builder.Descriptors.Add(new ServiceDescriptor + { + Interface = source.Value.Interface, + Implementation = source.Value.Implementation, + Lifetime = source.Value.Lifetime, + Factory = (nextProvider) => source.Value.Factory(nextProvider) + }); + break; + case DependencyLifetime.Singleton: + builder.Descriptors.Add(new ServiceDescriptor + { + Interface = source.Value.Interface, + Implementation = source.Value.Implementation, + Lifetime = source.Value.Lifetime, + Factory = _ => GetService(source.Value.Interface) + }); + break; + + } + } // transients are stupid, and I should probably delete them. - AddDescriptors(builder.TransientServices, Transients, (nextProvider, desc) => desc.Factory(nextProvider)); - - // all scoped descriptors - AddDescriptors(builder.ScopedServices, Scoped, (nextProvider, desc) => desc.Factory(nextProvider)); - // scopes services build brand new instances per provider - - // singletons use their parent singleton cache. - AddDescriptors(builder.SingletonServices, Scoped, (_, desc) => GetService(desc.Interface)); + //AddDescriptors(builder.TransientServices, Transients, (nextProvider, desc) => desc.Factory(nextProvider)); + // + // // all scoped descriptors + // AddDescriptors(builder.ScopedServices, Scoped, (nextProvider, desc) => desc.Factory(nextProvider)); + // // scopes services build brand new instances per provider + // + // // singletons use their parent singleton cache. + // AddDescriptors(builder.SingletonServices, Scoped, (_, desc) => GetService(desc.Interface)); configure?.Invoke(builder); diff --git a/cli/beamable.common/Runtime/Dependencies/Utils.cs b/cli/beamable.common/Runtime/Dependencies/Utils.cs index 0638be8e3a..dcbe3d1a0a 100644 --- a/cli/beamable.common/Runtime/Dependencies/Utils.cs +++ b/cli/beamable.common/Runtime/Dependencies/Utils.cs @@ -38,10 +38,12 @@ public class ServiceDescriptor { public Type Interface, Implementation; public Func Factory; + public DependencyLifetime Lifetime; + public ServiceDescriptor Clone() { - return new ServiceDescriptor { Interface = Interface, Implementation = Implementation, Factory = Factory }; + return new ServiceDescriptor { Interface = Interface, Implementation = Implementation, Factory = Factory, Lifetime = Lifetime }; } } } diff --git a/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs b/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs index 7521705797..95165562e1 100644 --- a/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs +++ b/microservice/microservice/dbmicroservice/MicroserviceBootstrapper.cs @@ -236,10 +236,7 @@ public static IDependencyBuilder ConfigureServices(IMicroserviceArgs envArgs) return new MicroserviceHttpRequester(envArgs, new HttpClient(handler)); }) .AddSingleton(envArgs) - .AddSingleton(_ => - { - return Instances[0].SocketContext; - }) + .AddSingleton(_ => Instances[0].SocketContext) .AddScoped(provider => new MicroserviceRequester(provider.GetService(), provider.GetService(), provider.GetService(), true)) .AddScoped(provider => provider.GetService()) From 96d0ec16be954f574ee12f410e146596dbdba1b9 Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Wed, 19 Jun 2024 21:11:23 -0400 Subject: [PATCH 3/4] promises mid work, struct and dispose function --- .../DependencyProviderBenchmarks.cs | 8 +- cli/Benchmarks/Program.cs | 4 +- cli/Benchmarks/PromiseBenchmarks.cs | 78 +++++++++++++++++++ .../Dependencies/DependencyProvider.cs | 62 ++++++++------- cli/beamable.common/Runtime/Promise.cs | 70 +++++++++++++---- .../PromiseTests/AsyncTests.cs | 20 +++++ .../PromiseTests/WhenAllTests.cs | 38 +++++++++ 7 files changed, 227 insertions(+), 53 deletions(-) create mode 100644 cli/Benchmarks/PromiseBenchmarks.cs create mode 100644 microservice/microserviceTests/PromiseTests/AsyncTests.cs create mode 100644 microservice/microserviceTests/PromiseTests/WhenAllTests.cs diff --git a/cli/Benchmarks/DependencyProviderBenchmarks.cs b/cli/Benchmarks/DependencyProviderBenchmarks.cs index 486076dd14..aa00f03ef9 100644 --- a/cli/Benchmarks/DependencyProviderBenchmarks.cs +++ b/cli/Benchmarks/DependencyProviderBenchmarks.cs @@ -30,7 +30,7 @@ public void BaseCase_JustProvider() var provider = new DependencyProvider(null, null); } - // [Benchmark] + [Benchmark] public void BaseCase_NoDispose() { var builder = new DependencyBuilder(); @@ -40,7 +40,7 @@ public void BaseCase_NoDispose() // var service = provider.GetService(); } // - [Benchmark] + // [Benchmark] public void BaseCase_NoDispose_RegisterAndResolve() { var builder = new DependencyBuilder(); @@ -55,11 +55,7 @@ public void BaseCase_NoDispose_RegisterAndResolve() public void BaseCase_Dispose() { var builder = new DependencyBuilder(); - // builder.AddSingleton(); var provider = builder.Build(); - - // var service = provider.GetService(); - provider.Dispose(); } diff --git a/cli/Benchmarks/Program.cs b/cli/Benchmarks/Program.cs index 077227140d..7bf8029e32 100644 --- a/cli/Benchmarks/Program.cs +++ b/cli/Benchmarks/Program.cs @@ -7,6 +7,8 @@ public class Program public static void Main(string[] args) { // BenchmarkRunner.Run(); - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + } } diff --git a/cli/Benchmarks/PromiseBenchmarks.cs b/cli/Benchmarks/PromiseBenchmarks.cs new file mode 100644 index 0000000000..a3849cf371 --- /dev/null +++ b/cli/Benchmarks/PromiseBenchmarks.cs @@ -0,0 +1,78 @@ +using Beamable.Common; +using BenchmarkDotNet.Attributes; +using System.Runtime.InteropServices.JavaScript; + +namespace Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob] +public class PromiseBenchmarks +{ + + // [Benchmark] + public void PromiseAllocation_Many() + { + for (var i = 0; i < 10_000; i++) + { + var p = new Promise(); + } + } + + // [Benchmark] + public void PromiseAllocation() + { + var p = new Promise(); + } + + + [Benchmark] + public Promise ReturnPromise() + { + var p = new Promise(); + return null; + } + + [Benchmark] + public async Promise ReturnAsyncPromise() + { + var p = new Promise(); + } + + + // [Benchmark] + public async Promise AsyncAwait2() + { + var p = new Promise(); + p.CompleteSuccess(); + } + + // [Benchmark] + public async Task Sequence() + { + var pList = Enumerable.Range(0, 10).Select(_ => new Promise()).ToList(); + var final = Promise.Sequence(pList); + + var _ = pList.Select(p => Task.Run(async () => + { + await Task.Delay(1); + p.CompleteSuccess(1); + })).ToList(); + + await final; + } + + // [Benchmark] + public async Task WhenAll() + { + var pList = Enumerable.Range(0, 10).Select(_ => new Promise()).ToList(); + var final = Promise.WhenAll(pList); + + var _ = pList.Select(p => Task.Run(async () => + { + await Task.Delay(1); + p.CompleteSuccess(1); + })).ToList(); + + await final; + } +} diff --git a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs index bea67efc8b..fdd563ab77 100644 --- a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs +++ b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs @@ -344,38 +344,42 @@ public async Promise Dispose() } } - await Promise.Sequence(childRemovalPromises); + + await Promise.WhenAll(childRemovalPromises); async Promise DisposeServices(IEnumerable services) { - var clonedList = new List(services); - var groups = clonedList.GroupBy(x => - { - if (x is IBeamableDisposableOrder disposableOrder) - return disposableOrder.DisposeOrder; - return 0; - }); - groups = groups.OrderBy(x => x.Key); - foreach (var group in groups) - { - var promises = new List>(); - - foreach (var service in group) - { - if (service == null) continue; - if (service is IBeamableDisposable disposable) - { - var promise = disposable.OnDispose(); - if (promise != null) - { - promises.Add(promise); - } - } - } - - var final = Promise.Sequence(promises); - await final; - } + var p = new Promise(); + p.CompleteSuccess(); + await p; + // var clonedList = new List(services); + // var groups = clonedList.GroupBy(x => + // { + // if (x is IBeamableDisposableOrder disposableOrder) + // return disposableOrder.DisposeOrder; + // return 0; + // }); + // groups = groups.OrderBy(x => x.Key); + // foreach (var group in groups) + // { + // var promises = new List>(); + // + // foreach (var service in group) + // { + // if (service == null) continue; + // if (service is IBeamableDisposable disposable) + // { + // var promise = disposable.OnDispose(); + // if (promise != null) + // { + // promises.Add(promise); + // } + // } + // } + // + // var final = Promise.Sequence(promises); + // await final; + // } } void ClearServices(Dictionary descriptors) diff --git a/cli/beamable.common/Runtime/Promise.cs b/cli/beamable.common/Runtime/Promise.cs index 16f56ee43d..603d2405ee 100644 --- a/cli/beamable.common/Runtime/Promise.cs +++ b/cli/beamable.common/Runtime/Promise.cs @@ -48,8 +48,6 @@ public abstract class PromiseBase protected Exception err; protected ExceptionDispatchInfo errInfo; - protected StackTrace _errStackTrace; - protected object _lock = new object(); internal bool RaiseInnerException { get; set; } #if DISABLE_THREADING @@ -81,6 +79,7 @@ protected bool done /// public bool IsFailed => done && err != null; + private static event PromiseEvent OnPotentialUncaughtError; public static bool HasUncaughtErrorHandler => OnPotentialUncaughtError != null; @@ -161,7 +160,7 @@ public class Promise : PromiseBase, ICriticalNotifyCompletion /// public void CompleteSuccess(T val) { - lock (_lock) + lock (this) { if (done) { @@ -190,7 +189,7 @@ public void CompleteSuccess(T val) /// public void CompleteError(Exception ex) { - lock (_lock) + lock (this) { if (done) { @@ -232,7 +231,7 @@ public void CompleteError(Exception ex) /// public Promise Then(Action callback) { - lock (_lock) + lock (this) { if (done) { @@ -292,7 +291,7 @@ public Promise Merge(Promise other) /// public Promise Error(Action errback) { - lock (_lock) + lock (this) { HadAnyErrbacks = true; if (done) @@ -740,6 +739,29 @@ public static SequencePromise ObservableSequence(IList> promise return result; } + public static Promise WhenAll(List> promises) + { + if (promises.Count == 0) + return Success; + + var result = new Promise(); + Check(); + return result; + void Check() + { + for (var i = 0; i < promises.Count; i++) + { + if (promises[i].IsCompleted) + continue; + + promises[i].Error(ex => result.CompleteError(ex)); + promises[i].Then(_ => Check()); + return; + } + result.CompleteSuccess(); + } + } + /// /// Create a of List from a List of s. /// @@ -1250,25 +1272,38 @@ public void Start(ref TStateMachine stateMachine) public Promise Task => _promise; } - public sealed class PromiseAsyncMethodBuilder + public struct PromiseAsyncMethodBuilder { private IAsyncStateMachine _stateMachine; - private Promise _promise = new Promise(); // TODO: allocation. + private Promise _promise;// = new Promise(); // TODO: allocation. + private Promise Promise + { + get + { + if (_promise == null) + { + _promise = new Promise(); // TODO: allocation? + } + + return _promise; + } + } + public static PromiseAsyncMethodBuilder Create() { - return new PromiseAsyncMethodBuilder(); + return default; } public void SetResult() { - _promise.CompleteSuccess(PromiseBase.Unit); + Promise.CompleteSuccess(PromiseBase.Unit); } public void SetException(Exception ex) { - _promise.RaiseInnerException = true; - _promise.CompleteError(ex); + Promise.RaiseInnerException = true; + Promise.CompleteError(ex); } public void SetStateMachine(IAsyncStateMachine machine) @@ -1287,10 +1322,11 @@ public void AwaitOnCompleted( _stateMachine.SetStateMachine(stateMachine); } - awaiter.OnCompleted(() => - { - _stateMachine.MoveNext(); - }); + awaiter.OnCompleted(_stateMachine.MoveNext); + // awaiter.OnCompleted(() => + // { + // // _stateMachine.MoveNext(); + // }); } public void AwaitUnsafeOnCompleted( @@ -1307,6 +1343,6 @@ public void Start(ref TStateMachine stateMachine) stateMachine.MoveNext(); } - public Promise Task => _promise; + public Promise Task => Promise; } } diff --git a/microservice/microserviceTests/PromiseTests/AsyncTests.cs b/microservice/microserviceTests/PromiseTests/AsyncTests.cs new file mode 100644 index 0000000000..cc12cc7a28 --- /dev/null +++ b/microservice/microserviceTests/PromiseTests/AsyncTests.cs @@ -0,0 +1,20 @@ +using Beamable.Common; +using Beamable.Common.Api; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace microserviceTests.PromiseTests; + +public class AsyncTests +{ + [Test] + public async Task Test() + { + async Promise Method() + { + await Task.Delay(50); + } + await Method(); + + } +} diff --git a/microservice/microserviceTests/PromiseTests/WhenAllTests.cs b/microservice/microserviceTests/PromiseTests/WhenAllTests.cs new file mode 100644 index 0000000000..a921875d9c --- /dev/null +++ b/microservice/microserviceTests/PromiseTests/WhenAllTests.cs @@ -0,0 +1,38 @@ +using Beamable.Common; +using NUnit.Framework; +using System.Collections.Generic; + +namespace microserviceTests.PromiseTests; + +public class WhenAllTests +{ + [Test] + public void Simple() + { + var pList = new List> + { + new Promise(), + new Promise(), + new Promise(), + new Promise(), + }; + + var whenAll = Promise.WhenAll(pList); + var ran = false; + whenAll.Then(_ => + { + for (var i = 0; i < pList.Count; i++) + { + Assert.That(pList[i].IsCompleted, $"promise index=[{i}] must be completed."); + } + + ran = true; + }); + + foreach (var p in pList) + { + p.CompleteSuccess(1); + } + Assert.That(ran, Is.True); + } +} From 09ecf89f7d294aca67063548af611a803ce300cb Mon Sep 17 00:00:00 2001 From: Chris Hanna Date: Thu, 20 Jun 2024 08:43:56 -0400 Subject: [PATCH 4/4] change dispose to use presorted datastructure --- .gitignore | 3 + cli/Benchmarks/Program.cs | 4 +- cli/Benchmarks/PromiseBenchmarks.cs | 36 ++++-- .../Dependencies/DependencyProvider.cs | 113 +++++++++++------- cli/beamable.common/Runtime/Promise.cs | 35 +++++- cli/tests/DI/MultiThreadedAccessTests.cs | 7 ++ .../PromiseTests/AsyncTests.cs | 8 ++ 7 files changed, 148 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 74a2f8d0c3..b6ea476bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,9 @@ user-token.json /cli/tests/bin/ /cli/tests/obj/ /cli/tests/.idea/ +/cli/benchmarks/bin/ +/cli/benchmarks/obj/ +/cli/benchmarks/.idea/ /cli/*.sln.DotSettings.* cli/cli/beamoLocalRuntime.json diff --git a/cli/Benchmarks/Program.cs b/cli/Benchmarks/Program.cs index 7bf8029e32..6e4feb1faa 100644 --- a/cli/Benchmarks/Program.cs +++ b/cli/Benchmarks/Program.cs @@ -7,8 +7,8 @@ public class Program public static void Main(string[] args) { // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); } } diff --git a/cli/Benchmarks/PromiseBenchmarks.cs b/cli/Benchmarks/PromiseBenchmarks.cs index a3849cf371..3cf584fbca 100644 --- a/cli/Benchmarks/PromiseBenchmarks.cs +++ b/cli/Benchmarks/PromiseBenchmarks.cs @@ -9,6 +9,18 @@ namespace Benchmarks; public class PromiseBenchmarks { + // [GlobalSetup] + public void Setup() + { + var x = PromiseBase.Unit; + } + // [Benchmark] + public Promise PromiseComplete() + { + var p = new Promise(); + //p.CompleteSuccess(); + return p; + } // [Benchmark] public void PromiseAllocation_Many() { @@ -16,26 +28,30 @@ public void PromiseAllocation_Many() { var p = new Promise(); } + } + + [Benchmark] + public async Task Await() + { + var p = new Promise(); + p.CompleteSuccess(3); + + return await p; } // [Benchmark] - public void PromiseAllocation() + public Promise PromiseAllocation() { var p = new Promise(); + return p; } - [Benchmark] - public Promise ReturnPromise() - { - var p = new Promise(); - return null; - } - - [Benchmark] + // [Benchmark] public async Promise ReturnAsyncPromise() { - var p = new Promise(); + // var p = new Promise(); + } diff --git a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs index fdd563ab77..0f783803b4 100644 --- a/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs +++ b/cli/beamable.common/Runtime/Dependencies/DependencyProvider.cs @@ -164,9 +164,12 @@ public interface IDependencyProviderScope : IDependencyProvider /// void RemoveChild(IDependencyProviderScope child); } - - - public class DependencyProvider : IDependencyProviderScope + + public interface IDependencyOrderComparer : IComparer + { + + } + public class DependencyProvider : IDependencyProviderScope, IDependencyOrderComparer { // private Dictionary Transients { get; set; } @@ -175,6 +178,8 @@ public class DependencyProvider : IDependencyProviderScope private Dictionary Descriptors { get; set; } private Dictionary InstanceCache { get; set; } = new Dictionary(); + + private SortedDictionary> SortedInstanceValues = new SortedDictionary>(); // private Dictionary SingletonCache { get; set; } = new Dictionary(); // private Dictionary ScopeCache { get; set; } = new Dictionary(); @@ -208,7 +213,6 @@ public DependencyProvider(DependencyBuilder builder, BuildOptions options = null _options = options; Descriptors = new Dictionary(); - if (builder == null) return; foreach (var desc in builder.Descriptors) @@ -298,7 +302,20 @@ public object GetService(Type t) } else { - return InstanceCache[t] = descriptor.Factory(this); + var service = InstanceCache[t] = descriptor.Factory(this); + + var sortOrder = 0; + if (service is IBeamableDisposableOrder orderable) + { + sortOrder = orderable.DisposeOrder; + } + if (!SortedInstanceValues.TryGetValue(sortOrder, out var services)) + { + services = new List(); + SortedInstanceValues.Add(sortOrder, services); + } + services.Add(service); + return service; } } else @@ -330,8 +347,7 @@ public async Promise Dispose() lock (_children) { - var childrenClone = new List(_children); - foreach (var child in childrenClone) + foreach (var child in _children) { if (child != null) { @@ -347,39 +363,32 @@ public async Promise Dispose() await Promise.WhenAll(childRemovalPromises); - async Promise DisposeServices(IEnumerable services) + async Promise DisposeServices(SortedDictionary> groups) { - var p = new Promise(); - p.CompleteSuccess(); - await p; - // var clonedList = new List(services); - // var groups = clonedList.GroupBy(x => - // { - // if (x is IBeamableDisposableOrder disposableOrder) - // return disposableOrder.DisposeOrder; - // return 0; - // }); - // groups = groups.OrderBy(x => x.Key); - // foreach (var group in groups) - // { - // var promises = new List>(); - // - // foreach (var service in group) - // { - // if (service == null) continue; - // if (service is IBeamableDisposable disposable) - // { - // var promise = disposable.OnDispose(); - // if (promise != null) - // { - // promises.Add(promise); - // } - // } - // } - // - // var final = Promise.Sequence(promises); - // await final; - // } + if (groups.Count == 0) return; + + foreach (var kvp in groups) + { + var services = kvp.Value; + + var promises = new List>(); + + foreach (var service in services) + { + if (service == null) continue; + if (service is IBeamableDisposable disposable) + { + var promise = disposable.OnDispose(); + if (promise != null) + { + promises.Add(promise); + } + } + } + + var final = Promise.Sequence(promises); + await final; + } } void ClearServices(Dictionary descriptors) @@ -395,9 +404,10 @@ void ClearServices(Dictionary descriptors) } - await DisposeServices(InstanceCache.Values.Distinct()); + await DisposeServices(SortedInstanceValues); InstanceCache.Clear(); + SortedInstanceValues.Clear(); if (!_options.allowHydration) { @@ -408,6 +418,7 @@ void ClearServices(Dictionary descriptors) Descriptors = null; InstanceCache = null; + SortedInstanceValues = null; } _destroyed = true; @@ -437,6 +448,7 @@ public void Hydrate(IDependencyProviderScope other) // Singletons.Add(desc.Interface, desc); // } InstanceCache.Clear(); + SortedInstanceValues.Clear(); lock (_children) { @@ -544,6 +556,27 @@ public IDependencyProviderScope Fork(Action configure = null return provider; } + + + public int Compare(object x, object y) + { + if (x == null || y == null) return 0; + + var xOrder = 0; + var yOrder = 0; + + if (x is IBeamableDisposableOrder xDisposable) + { + xOrder = xDisposable.DisposeOrder; + } + + if (y is IBeamableDisposableOrder yDisposable) + { + yOrder = yDisposable.DisposeOrder; + } + + return xOrder.CompareTo(yOrder); + } } } diff --git a/cli/beamable.common/Runtime/Promise.cs b/cli/beamable.common/Runtime/Promise.cs index 603d2405ee..16beff0eb2 100644 --- a/cli/beamable.common/Runtime/Promise.cs +++ b/cli/beamable.common/Runtime/Promise.cs @@ -65,7 +65,7 @@ protected bool done } #endif - public static readonly Unit Unit = new Unit(); + public static Unit Unit = new Unit(); public static Promise SuccessfulUnit => Promise.Successful(Unit); @@ -151,8 +151,8 @@ public static Promise ToPromise(this ITaskLike))] public class Promise : PromiseBase, ICriticalNotifyCompletion { - private Action _callbacks; - private T _val; + protected Action _callbacks; + protected T _val; /// /// Call to set the value and resolve the %Promise @@ -656,9 +656,32 @@ public class Promise : Promise { public static Promise Success { get; } = new Promise { done = true }; - public void CompleteSuccess() => CompleteSuccess(PromiseBase.Unit); + public void CompleteSuccess() + { + // CompleteSuccessRef(); + lock (this) + { + if (done) + { + return; + } + + _val = Unit; + done = true; + try + { + _callbacks?.Invoke(Unit); + } + catch (Exception e) + { + BeamableLogger.LogException(e); + } + + _callbacks = null; + errbacks = null; + } + } - /// /// This function accepts a generator that will produce a promise, and then /// that promise will be executed. @@ -1297,7 +1320,7 @@ public static PromiseAsyncMethodBuilder Create() public void SetResult() { - Promise.CompleteSuccess(PromiseBase.Unit); + Promise.CompleteSuccess(); } public void SetException(Exception ex) diff --git a/cli/tests/DI/MultiThreadedAccessTests.cs b/cli/tests/DI/MultiThreadedAccessTests.cs index 73f1a88d7f..ebdbd68ded 100644 --- a/cli/tests/DI/MultiThreadedAccessTests.cs +++ b/cli/tests/DI/MultiThreadedAccessTests.cs @@ -1,5 +1,6 @@ using Beamable.Common.Dependencies; using NUnit.Framework; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -7,6 +8,12 @@ namespace tests.DI; public class MultiThreadedAccessTests { + + [Test] + public void SetTest() + { + } + [Test] public async Task SingletonOnlyGetsMadeOnce() { diff --git a/microservice/microserviceTests/PromiseTests/AsyncTests.cs b/microservice/microserviceTests/PromiseTests/AsyncTests.cs index cc12cc7a28..10b5d2cd27 100644 --- a/microservice/microserviceTests/PromiseTests/AsyncTests.cs +++ b/microservice/microserviceTests/PromiseTests/AsyncTests.cs @@ -17,4 +17,12 @@ async Promise Method() await Method(); } + + [Test] + public async Task SimpleAwait() + { + var p = new Promise(); + p.CompleteSuccess(); + await p; + } }