Skip to content

Commit

Permalink
Merge pull request #6 from Ceilidh-Team/parallel
Browse files Browse the repository at this point in the history
Parallel
  • Loading branch information
OrionNebula authored Sep 8, 2018
2 parents 7bc3cea + 50ec963 commit 266df53
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 65 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ language: csharp
solution: ceilidh-core.sln
mono: none
dotnet: 2.1.302
os:
- linux
- osx
install:
- dotnet restore
script:
Expand Down
3 changes: 2 additions & 1 deletion GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

## Advanced Usage

* To improve application startup time, use `CobbleContext.ExecuteAsync` instead of `CobbleContext.Execute`. This will cause initialization steps to be performed in parallel where possible, saving a lot of time if any of your constructors are long-running.
* If any class you register with the system implements `ILateInject<>` at least once, the constructed instance will be notified whenever any new classes are registered with the owner `CobbleContext` after `CobbleContext.Execute` is called.
* This allows for, in certain conditions, plugins to be loaded without requiring an application restart.
* `CobbleContext.AddManaged` can accept any implementation of `IInstanceGenerator`, not just existing types. This can allow for object construction that does not use a type constructor.
* See the [reference implementations](ProjectCeilidh.Cobble/Generator) of `IInstanceGenerator` for examples on how to implement this interface.
* Creating a `CobbleContext` automatically registers itself with the dependency injection system, allowing you to depend on it and access it later on.
* The `CobbleContext` you create automatically registers itself with the dependency injection system, allowing you to depend on it and access it later on.
18 changes: 8 additions & 10 deletions ProjectCeilidh.Cobble.Tests/DirectedGraphTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using ProjectCeilidh.Cobble.Data;

Expand All @@ -25,22 +26,19 @@ void InitialInspector(int value)
}

[Fact]
public void ParallelTopologicalSort()
public async Task ParallelTopologicalSort()
{
var graph = new DirectedGraph<int>(Enumerable.Range(0, 5));
graph.Link(0, 1);
graph.Link(2, 1);
graph.Link(1, 3);
graph.Link(4, 3);

Assert.Collection(graph.ParallelTopologicalSort(), InitialInspector, x => Assert.Equal(new []{ 1 }, x), x => Assert.Equal(new []{ 3 }, x));
var queue = new ConcurrentQueue<int>();

void InitialInspector(int[] value)
{
Array.Sort(value);
await graph.ParallelTopologicalSort(x => queue.Enqueue(x));

Assert.Equal(new []{ 0, 2, 4 }, value);
}
Assert.Collection(queue, value => Assert.True(value == 0 || value == 2 || value == 4), value => Assert.True(value == 0 || value == 2 || value == 4), value => Assert.True(value == 0 || value == 2 || value == 4 || value == 1), value => Assert.True(value == 4 || value == 1), x => Assert.Equal(3, x));
}

[Fact]
Expand All @@ -57,7 +55,7 @@ public void CircularDependency()
}

[Fact]
public void ParallelCircularDependency()
public async Task ParallelCircularDependency()
{
var graph = new DirectedGraph<int>(Enumerable.Range(0, 5));
graph.Link(0, 1);
Expand All @@ -66,7 +64,7 @@ public void ParallelCircularDependency()
graph.Link(4, 3);
graph.Link(3, 0);

Assert.Throws<DirectedGraph<int>.CyclicGraphException>(() => graph.ParallelTopologicalSort().ToList());
await Assert.ThrowsAsync<DirectedGraph<int>.CyclicGraphException>(async () => await graph.ParallelTopologicalSort(_ => { }));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
<TargetFramework>netcoreapp2.1</TargetFramework>

<IsPackable>false</IsPackable>

<StartupObject></StartupObject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
49 changes: 23 additions & 26 deletions ProjectCeilidh.Cobble/CobbleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ public sealed class CobbleContext
private bool _firstStage;

private readonly List<IInstanceGenerator> _instanceGenerators;
private readonly ConcurrentDictionary<Type, HashSet<object>> _lateInjectInstances;
private readonly ConcurrentDictionary<Type, HashSet<object>> _implementations;
private readonly ConcurrentDictionary<Type, ConcurrentBag<object>> _lateInjectInstances;
private readonly ConcurrentDictionary<Type, ConcurrentBag<object>> _implementations;

/// <summary>
/// Construct a new CobbleContext.
/// </summary>
public CobbleContext()
{
_instanceGenerators = new List<IInstanceGenerator>();
_lateInjectInstances = new ConcurrentDictionary<Type, HashSet<object>>();
_implementations = new ConcurrentDictionary<Type, HashSet<object>>();
_lateInjectInstances = new ConcurrentDictionary<Type, ConcurrentBag<object>>();
_implementations = new ConcurrentDictionary<Type, ConcurrentBag<object>>();

AddUnmanaged(this);
}
Expand Down Expand Up @@ -165,7 +165,7 @@ public void Execute()

// If the generator supports late injection, we need to add it to our list
foreach (var lateDep in late.LateDependencies)
_lateInjectInstances.AddOrUpdate(lateDep, x => new HashSet<object>(new[] {obj}),
_lateInjectInstances.AddOrUpdate(lateDep, x => new ConcurrentBag<object>(new[] {obj}),
(a, b) =>
{
b.Add(a);
Expand Down Expand Up @@ -208,25 +208,22 @@ public async Task ExecuteAsync()

try
{
foreach (var level in graph.ParallelTopologicalSort()) // Sort the dependency graph topologically - all dependencies should be satisfied by the time we get to each unit
await graph.ParallelTopologicalSort(gen =>
{
await Task.WhenAll(level.Select(gen => Task.Run(() =>
{
var obj = CreateInstance(gen, _implementations);
PushInstanceProvides(gen, obj, _implementations);
if (!(gen is ILateInstanceGenerator late)) return;
// If the generator supports late injection, we need to add it to our list
foreach (var lateDep in late.LateDependencies)
_lateInjectInstances.AddOrUpdate(lateDep, x => new HashSet<object>(new[] { obj }),
(a, b) =>
{
b.Add(obj);
return b;
});
})));
}
var obj = CreateInstance(gen, _implementations);
PushInstanceProvides(gen, obj, _implementations);
if (!(gen is ILateInstanceGenerator late)) return;
// If the generator supports late injection, we need to add it to our list
foreach (var lateDep in late.LateDependencies)
_lateInjectInstances.AddOrUpdate(lateDep, x => new ConcurrentBag<object>(new[] { obj }),
(a, b) =>
{
b.Add(obj);
return b;
});
});
}
catch (DirectedGraph<IInstanceGenerator>.CyclicGraphException) {
throw new CircularDependencyException();
Expand All @@ -239,10 +236,10 @@ await Task.WhenAll(level.Select(gen => Task.Run(() =>
/// <param name="gen">The instance generator that produced the instance.</param>
/// <param name="instance">The instance that was produced.</param>
/// <param name="instances">A dictionary mapping provided types to a set of instances.</param>
private static void PushInstanceProvides(IInstanceGenerator gen, object instance, ConcurrentDictionary<Type, HashSet<object>> instances)
private static void PushInstanceProvides(IInstanceGenerator gen, object instance, ConcurrentDictionary<Type, ConcurrentBag<object>> instances)
{
foreach (var prov in gen.Provides)
instances.AddOrUpdate(prov, x => new HashSet<object>(new[] {instance}), (a, b) =>
instances.AddOrUpdate(prov, x => new ConcurrentBag<object>(new[] {instance}), (a, b) =>
{
b.Add(instance);
return b;
Expand All @@ -255,7 +252,7 @@ private static void PushInstanceProvides(IInstanceGenerator gen, object instance
/// <returns>The created object.</returns>
/// <param name="gen">The generator instance.</param>
/// <param name="instances">A dictionary mapping provided types to a set of instances.</param>
private object CreateInstance(IInstanceGenerator gen, IDictionary<Type, HashSet<object>> instances)
private object CreateInstance(IInstanceGenerator gen, IDictionary<Type, ConcurrentBag<object>> instances)
{
var args = new object[gen.Dependencies.Count()];
var i = 0;
Expand Down
56 changes: 33 additions & 23 deletions ProjectCeilidh.Cobble/Data/DirectedGraph.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ProjectCeilidh.Cobble.Data
{
Expand All @@ -10,6 +12,7 @@ namespace ProjectCeilidh.Cobble.Data
/// <typeparam name="TNode"></typeparam>
internal class DirectedGraph<TNode>
{
private readonly HashSet<TNode> _initialNodes;
private readonly Dictionary<TNode, int> _nodes;
private readonly Dictionary<TNode, HashSet<TNode>> _outgoingEdges;

Expand All @@ -18,7 +21,6 @@ internal class DirectedGraph<TNode>
/// </summary>
public DirectedGraph() : this(Enumerable.Empty<TNode>())
{

}

/// <summary>
Expand All @@ -27,7 +29,8 @@ public DirectedGraph() : this(Enumerable.Empty<TNode>())
/// <param name="initialNodes">Initial nodes.</param>
public DirectedGraph(IEnumerable<TNode> initialNodes)
{
_nodes = initialNodes.ToDictionary(x => x, x => 0);
_initialNodes = new HashSet<TNode>(initialNodes);
_nodes = _initialNodes.ToDictionary(x => x, x => 0);
_outgoingEdges = new Dictionary<TNode, HashSet<TNode>>();
}

Expand All @@ -36,7 +39,11 @@ public DirectedGraph(IEnumerable<TNode> initialNodes)
/// </summary>
/// <returns>True if the node was added, false otherwise.</returns>
/// <param name="node">The node to add.</param>
public void Add(TNode node) => _nodes[node] = 0;
public void Add(TNode node)
{
_nodes[node] = 0;
_initialNodes.Add(node);
}

/// <summary>
/// Add a link from <paramref name="src"/> to <paramref name="dst"/>.
Expand All @@ -50,6 +57,7 @@ public void Link(TNode src, TNode dst)

outSet.Add(dst);
_nodes[dst]++;
_initialNodes.Remove(dst);
}

/// <summary>
Expand All @@ -64,7 +72,7 @@ public IEnumerable<TNode> TopologicalSort()
{
var refDict = new Dictionary<TNode, int>(_nodes);

var list = new LinkedList<TNode>(refDict.Where(x => x.Value == 0).Select(x => x.Key));
var list = new LinkedList<TNode>(_initialNodes);

while (list.Count > 0)
{
Expand All @@ -86,32 +94,34 @@ public IEnumerable<TNode> TopologicalSort()
throw new CyclicGraphException(remaining.Select(x => x.Key));
}

public IEnumerable<TNode[]> ParallelTopologicalSort()
/// <summary>
/// Topologically sort the graph, invoking the callback in parallel where possible
/// </summary>
/// <param name="callback">The callback to invoke</param>
/// <returns>A task following the parallel sort</returns>
public async Task ParallelTopologicalSort(Action<TNode> callback)
{
var refDict = new Dictionary<TNode, int>(_nodes);
var refDict = new ConcurrentDictionary<TNode, int>(_nodes);

var list = new LinkedList<TNode>(refDict.Where(x => x.Value == 0).Select(x => x.Key));
await Task.WhenAll(_initialNodes.Select(HandleItem));

while (list.Count > 0)
{
var arr = new TNode[list.Count];
list.CopyTo(arr, 0);
list.Clear();
var remaining = refDict.Where(x => x.Value != 0).ToList();

yield return arr;
if (remaining.Count > 0)
throw new CyclicGraphException(remaining.Select(x => x.Key));

foreach (var node in arr)
foreach (var target in _outgoingEdges.TryGetValue(node, out var set) ? set : Enumerable.Empty<TNode>())
{
var con = --refDict[target];
if (con == 0) list.AddLast(target);
}
}
async Task HandleItem(TNode node)
{
await Task.Run(() => callback(node));

var remaining = refDict.Where(x => x.Value > 0).ToList();
if (_outgoingEdges.TryGetValue(node, out var set))
await Task.WhenAll(set.Select(async target =>
{
var con = refDict.AddOrUpdate(target, 0, (a, b) => b - 1);
if (remaining.Count > 0)
throw new CyclicGraphException(remaining.Select(x => x.Key));
if (con == 0) await HandleItem(target);
}));
}
}

public class CyclicGraphException : Exception
Expand Down
1 change: 0 additions & 1 deletion ProjectCeilidh.Cobble/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace ProjectCeilidh.Cobble
{
Expand Down
2 changes: 2 additions & 0 deletions ProjectCeilidh.Cobble/Generator/BareLateInstanceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ public BareLateInstanceGenerator(object instance)
}

public object GenerateInstance(object[] args) => _instance;

public override string ToString() => $"BareLateInstanceGenerator({_instance.GetType().FullName})";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public object GenerateInstance(object[] args)
return proxy;
}

public override string ToString() => $"DictionaryInstanceGenerator({_contractType.FullName})";

/// <summary>
/// Provides a way to implement the specified contract at runtime.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions ProjectCeilidh.Cobble/Generator/TypeLateInstanceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ public object GenerateInstance(object[] args)
var ctor = _target.GetConstructors().Single();
return ctor.Invoke(args);
}

public override string ToString() => $"TypeLateInstanceGenerator({_target.FullName})";
}
}
2 changes: 1 addition & 1 deletion ProjectCeilidh.Cobble/ProjectCeilidh.Cobble.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<RepositoryUrl>https://github.com/Ceilidh-Team/Cobble.git</RepositoryUrl>
<Copyright>2018 Olivia Trewin</Copyright>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>1.1.0</Version>
<Version>1.1.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Reflection.DispatchProxy" Version="4.5.1" />
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Cobble is a [.NET Standard](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) library for building extensible applications through bespoke dependency injection.

* Cross-platform: Because it uses .NET Standard, Cobble will work with any modern implementation of .NET.
* Lightweight: Cobble uses high-performance algorithms to execute quickly, and any overhead is only incurred one time.
* Lightweight: Cobble uses high-performance parallel algorithms to execute quickly, and any overhead is only incurred one time.
* Learn Once, Write Anywhere: Cobble makes no assumptions about the function of your program or the technology you use, making it easy to use for any task.

[Learn how to get started with Cobble](GettingStarted.md)

0 comments on commit 266df53

Please sign in to comment.