Skip to content
This repository was archived by the owner on Nov 2, 2018. It is now read-only.

[Design]: Changes to improve performance #291

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Diagnostics;

namespace System.Collections.Concurrent
{
internal static class ConcurrentDictionaryExtensions
{
// From https://github.com/dotnet/corefx/issues/394#issuecomment-69494764
// This lets us pass a state parameter allocation free GetOrAdd
internal static TValue GetOrAdd<TKey, TValue, TArg>(this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TArg, TValue> valueFactory, TArg arg)
{
Debug.Assert(dictionary != null);
Debug.Assert(key != null);
Debug.Assert(valueFactory != null);

while (true)
{
TValue value;
if (dictionary.TryGetValue(key, out value))
{
return value;
}

value = valueFactory(key, arg);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this code a little clearer by getting rid of the while and changing the TryAdd to GetOrAdd(key, value). If your add fails, it's because someone was contending with you to also add a value and so you'll get the value they added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this is easier to read.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow your ❤️ - but this gives me the impression that it will run an arbitrary number of times, but it will only ever run 1.5 times at most.

if (dictionary.TryAdd(key, value))
{
return value;
}
}
}

}
}
79 changes: 49 additions & 30 deletions src/Microsoft.Framework.DependencyInjection/ServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ namespace Microsoft.Framework.DependencyInjection
/// </summary>
internal class ServiceProvider : IServiceProvider, IDisposable
{
private readonly object _sync = new object();

private readonly Func<Type, Func<ServiceProvider, object>> _createServiceAccessor;
private readonly ServiceProvider _root;
private readonly ServiceTable _table;

private readonly Dictionary<IService, object> _resolvedServices = new Dictionary<IService, object>();
private ConcurrentBag<IDisposable> _disposables = new ConcurrentBag<IDisposable>();
private List<IDisposable> _transientDisposables;

private static readonly Func<Type, ServiceProvider, Func<ServiceProvider, object>> _createServiceAccessor = CreateServiceAccessor;

public ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors)
{
Expand All @@ -34,38 +33,35 @@ public ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors)
_table.Add(typeof(IServiceProvider), new ServiceProviderService());
_table.Add(typeof(IServiceScopeFactory), new ServiceScopeService());
_table.Add(typeof(IEnumerable<>), new OpenIEnumerableService(_table));

// Caching to avoid allocating a Func<,> object per call to GetService
_createServiceAccessor = CreateServiceAccessor;
}

// This constructor is called exclusively to create a child scope from the parent
internal ServiceProvider(ServiceProvider parent)
{
_root = parent._root;
_table = parent._table;

// Caching to avoid allocating a Func<,> object per call to GetService
_createServiceAccessor = CreateServiceAccessor;
}

// Reusing _resolvedServices as an implementation detail of the lock
private object SyncObject => _resolvedServices;

/// <summary>
/// Gets the service object of the specified type.
/// </summary>
/// <param name="serviceType"></param>
/// <returns></returns>
public object GetService(Type serviceType)
{
var realizedService = _table.RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
var realizedService = _table.RealizedServices.GetOrAdd(serviceType, _createServiceAccessor, this);
return realizedService.Invoke(this);
}

private Func<ServiceProvider, object> CreateServiceAccessor(Type serviceType)
private static Func<ServiceProvider, object> CreateServiceAccessor(Type serviceType, ServiceProvider serviceProvider)
{
var callSite = GetServiceCallSite(serviceType, new HashSet<Type>());
var callSite = serviceProvider.GetServiceCallSite(serviceType, new HashSet<Type>());
if (callSite != null)
{
return RealizeService(_table, serviceType, callSite);
return RealizeService(serviceProvider._table, serviceType, callSite);
}

return _ => null;
Expand Down Expand Up @@ -145,25 +141,52 @@ internal IServiceCallSite GetResolveCallSite(IService service, ISet<Type> callSi

public void Dispose()
{
var disposables = Interlocked.Exchange(ref _disposables, null);

if (disposables != null)
lock (SyncObject)
{
foreach (var disposable in disposables)
if (_transientDisposables != null)
{
disposable.Dispose();
foreach (var disposable in _transientDisposables)
{
disposable.Dispose();
}

_transientDisposables.Clear();
}

// PERF: We've enumerating the dictionary so that we don't allocate to enumerate.
// .Values allocates a KeyCollection on the heap, enumerating the dictionary allocates
// a struct enumerator
foreach (var entry in _resolvedServices)
{
(entry.Value as IDisposable)?.Dispose();
}

_resolvedServices.Clear();
}
}

private object CaptureDisposable(object service)
{
// Skip capturing disposables for the root
if (_root == this)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nobody disposes the root scope, an argument can be made that it is possible but the interface for the top level (IServiceProvider) requires a downcast to IDisposable to do so and that's pretty rare unless you're scoped. We can let the GC handle those objects.

{
return service;
}

if (!object.ReferenceEquals(this, service))
{
var disposable = service as IDisposable;
if (disposable != null)
{
_disposables.Add(disposable);
lock (SyncObject)
{
if (_transientDisposables == null)
{
_transientDisposables = new List<IDisposable>();
}

_transientDisposables.Add(disposable);
}
}
}
return service;
Expand Down Expand Up @@ -255,11 +278,11 @@ public ScopedCallSite(IService key, IServiceCallSite serviceCallSite)
public virtual object Invoke(ServiceProvider provider)
{
object resolved;
lock (provider._sync)
lock (provider._resolvedServices)
{
if (!provider._resolvedServices.TryGetValue(_key, out resolved))
{
resolved = provider.CaptureDisposable(_serviceCallSite.Invoke(provider));
resolved = _serviceCallSite.Invoke(provider);
provider._resolvedServices.Add(_key, resolved);
}
}
Expand All @@ -284,12 +307,8 @@ public virtual Expression Build(Expression providerExpression)
keyExpression,
resolvedExpression);

var captureDisposableExpression = Expression.Assign(
resolvedExpression,
Expression.Call(
providerExpression,
CaptureDisposableMethodInfo,
_serviceCallSite.Build(providerExpression)));
var assignExpression = Expression.Assign(
resolvedExpression, _serviceCallSite.Build(providerExpression));

var addValueExpression = Expression.Call(
resolvedServicesExpression,
Expand All @@ -302,7 +321,7 @@ public virtual Expression Build(Expression providerExpression)
new[] { resolvedExpression },
Expression.IfThen(
Expression.Not(tryGetValueExpression),
Expression.Block(captureDisposableExpression, addValueExpression)),
Expression.Block(assignExpression, addValueExpression)),
resolvedExpression);

return Lock(providerExpression, blockExpression);
Expand All @@ -312,7 +331,7 @@ private static Expression Lock(Expression providerExpression, Expression body)
{
// The C# compiler would copy the lock object to guard against mutation.
// We don't, since we know the lock object is readonly.
var syncField = Expression.Field(providerExpression, "_sync");
var syncField = Expression.Field(providerExpression, "_resolvedServices");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does nameof(provider_resolvedServices) work here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that would be better

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have access to it.

var lockWasTaken = Expression.Variable(typeof(bool), "lockWasTaken");

var monitorEnter = Expression.Call(MonitorEnterMethodInfo, syncField, lockWasTaken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,35 @@ public void DisposingScopeDisposesService()
{
var container = CreateContainer();
FakeService disposableService;
FakeService transient1;
FakeService transient2;
FakeService singleton;

var scopeFactory = container.GetService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
disposableService = (FakeService)scope.ServiceProvider.GetService<IFakeScopedService>();
transient1 = (FakeService)scope.ServiceProvider.GetService<IFakeService>();
transient2 = (FakeService)scope.ServiceProvider.GetService<IFakeService>();
singleton = (FakeService)scope.ServiceProvider.GetService<IFakeSingletonService>();

Assert.False(disposableService.Disposed);
Assert.False(transient1.Disposed);
Assert.False(transient2.Disposed);
Assert.False(singleton.Disposed);
}

Assert.True(disposableService.Disposed);
Assert.True(transient1.Disposed);
Assert.True(transient2.Disposed);
Assert.False(singleton.Disposed);

var disposableContainer = container as IDisposable;
if (disposableContainer != null)
{
disposableContainer.Dispose();
Assert.True(singleton.Disposed);
}
}

[Fact]
Expand Down