From 6395be01b948be04278047b3bfecae8bf86f892d Mon Sep 17 00:00:00 2001 From: Olivia Trewin Date: Sat, 8 Sep 2018 13:47:59 -0500 Subject: [PATCH 1/2] Added improved parallelization Changed the signature of DirectedGraph.ParallelTopologicalSort ExecuteAsync can now resolve partial subtrees in parallel, improving the perfomance dramatically when there are long-running initialization sequences in a far-off corner of the dependency graph --- GettingStarted.md | 3 +- .../DirectedGraphTests.cs | 18 +++--- .../ProjectCeilidh.Cobble.Tests.csproj | 9 ++- ProjectCeilidh.Cobble/CobbleContext.cs | 49 ++++++++-------- ProjectCeilidh.Cobble/Data/DirectedGraph.cs | 56 +++++++++++-------- ProjectCeilidh.Cobble/Extensions.cs | 1 - .../Generator/BareLateInstanceGenerator.cs | 2 + .../Generator/DictionaryInstanceGenerator.cs | 2 + .../Generator/TypeLateInstanceGenerator.cs | 2 + .../ProjectCeilidh.Cobble.csproj | 2 +- README.md | 2 +- 11 files changed, 81 insertions(+), 65 deletions(-) diff --git a/GettingStarted.md b/GettingStarted.md index f980cda..da13252 100644 --- a/GettingStarted.md +++ b/GettingStarted.md @@ -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. diff --git a/ProjectCeilidh.Cobble.Tests/DirectedGraphTests.cs b/ProjectCeilidh.Cobble.Tests/DirectedGraphTests.cs index afaa275..538b600 100644 --- a/ProjectCeilidh.Cobble.Tests/DirectedGraphTests.cs +++ b/ProjectCeilidh.Cobble.Tests/DirectedGraphTests.cs @@ -1,5 +1,6 @@ -using System; +using System.Collections.Concurrent; using System.Linq; +using System.Threading.Tasks; using Xunit; using ProjectCeilidh.Cobble.Data; @@ -25,7 +26,7 @@ void InitialInspector(int value) } [Fact] - public void ParallelTopologicalSort() + public async Task ParallelTopologicalSort() { var graph = new DirectedGraph(Enumerable.Range(0, 5)); graph.Link(0, 1); @@ -33,14 +34,11 @@ public void ParallelTopologicalSort() 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(); - 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] @@ -57,7 +55,7 @@ public void CircularDependency() } [Fact] - public void ParallelCircularDependency() + public async Task ParallelCircularDependency() { var graph = new DirectedGraph(Enumerable.Range(0, 5)); graph.Link(0, 1); @@ -66,7 +64,7 @@ public void ParallelCircularDependency() graph.Link(4, 3); graph.Link(3, 0); - Assert.Throws.CyclicGraphException>(() => graph.ParallelTopologicalSort().ToList()); + await Assert.ThrowsAsync.CyclicGraphException>(async () => await graph.ParallelTopologicalSort(_ => { })); } } } diff --git a/ProjectCeilidh.Cobble.Tests/ProjectCeilidh.Cobble.Tests.csproj b/ProjectCeilidh.Cobble.Tests/ProjectCeilidh.Cobble.Tests.csproj index da16b05..b2f2c9d 100644 --- a/ProjectCeilidh.Cobble.Tests/ProjectCeilidh.Cobble.Tests.csproj +++ b/ProjectCeilidh.Cobble.Tests/ProjectCeilidh.Cobble.Tests.csproj @@ -4,12 +4,17 @@ netcoreapp2.1 false + + - - + + + all + runtime; build; native; contentfiles; analyzers + diff --git a/ProjectCeilidh.Cobble/CobbleContext.cs b/ProjectCeilidh.Cobble/CobbleContext.cs index 9214ba7..4b30079 100644 --- a/ProjectCeilidh.Cobble/CobbleContext.cs +++ b/ProjectCeilidh.Cobble/CobbleContext.cs @@ -20,8 +20,8 @@ public sealed class CobbleContext private bool _firstStage; private readonly List _instanceGenerators; - private readonly ConcurrentDictionary> _lateInjectInstances; - private readonly ConcurrentDictionary> _implementations; + private readonly ConcurrentDictionary> _lateInjectInstances; + private readonly ConcurrentDictionary> _implementations; /// /// Construct a new CobbleContext. @@ -29,8 +29,8 @@ public sealed class CobbleContext public CobbleContext() { _instanceGenerators = new List(); - _lateInjectInstances = new ConcurrentDictionary>(); - _implementations = new ConcurrentDictionary>(); + _lateInjectInstances = new ConcurrentDictionary>(); + _implementations = new ConcurrentDictionary>(); AddUnmanaged(this); } @@ -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(new[] {obj}), + _lateInjectInstances.AddOrUpdate(lateDep, x => new ConcurrentBag(new[] {obj}), (a, b) => { b.Add(a); @@ -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(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(new[] { obj }), + (a, b) => + { + b.Add(obj); + return b; + }); + }); } catch (DirectedGraph.CyclicGraphException) { throw new CircularDependencyException(); @@ -239,10 +236,10 @@ await Task.WhenAll(level.Select(gen => Task.Run(() => /// The instance generator that produced the instance. /// The instance that was produced. /// A dictionary mapping provided types to a set of instances. - private static void PushInstanceProvides(IInstanceGenerator gen, object instance, ConcurrentDictionary> instances) + private static void PushInstanceProvides(IInstanceGenerator gen, object instance, ConcurrentDictionary> instances) { foreach (var prov in gen.Provides) - instances.AddOrUpdate(prov, x => new HashSet(new[] {instance}), (a, b) => + instances.AddOrUpdate(prov, x => new ConcurrentBag(new[] {instance}), (a, b) => { b.Add(instance); return b; @@ -255,7 +252,7 @@ private static void PushInstanceProvides(IInstanceGenerator gen, object instance /// The created object. /// The generator instance. /// A dictionary mapping provided types to a set of instances. - private object CreateInstance(IInstanceGenerator gen, IDictionary> instances) + private object CreateInstance(IInstanceGenerator gen, IDictionary> instances) { var args = new object[gen.Dependencies.Count()]; var i = 0; diff --git a/ProjectCeilidh.Cobble/Data/DirectedGraph.cs b/ProjectCeilidh.Cobble/Data/DirectedGraph.cs index 796d3eb..02bb248 100644 --- a/ProjectCeilidh.Cobble/Data/DirectedGraph.cs +++ b/ProjectCeilidh.Cobble/Data/DirectedGraph.cs @@ -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 { @@ -10,6 +12,7 @@ namespace ProjectCeilidh.Cobble.Data /// internal class DirectedGraph { + private readonly HashSet _initialNodes; private readonly Dictionary _nodes; private readonly Dictionary> _outgoingEdges; @@ -18,7 +21,6 @@ internal class DirectedGraph /// public DirectedGraph() : this(Enumerable.Empty()) { - } /// @@ -27,7 +29,8 @@ public DirectedGraph() : this(Enumerable.Empty()) /// Initial nodes. public DirectedGraph(IEnumerable initialNodes) { - _nodes = initialNodes.ToDictionary(x => x, x => 0); + _initialNodes = new HashSet(initialNodes); + _nodes = _initialNodes.ToDictionary(x => x, x => 0); _outgoingEdges = new Dictionary>(); } @@ -36,7 +39,11 @@ public DirectedGraph(IEnumerable initialNodes) /// /// True if the node was added, false otherwise. /// The node to add. - public void Add(TNode node) => _nodes[node] = 0; + public void Add(TNode node) + { + _nodes[node] = 0; + _initialNodes.Add(node); + } /// /// Add a link from to . @@ -50,6 +57,7 @@ public void Link(TNode src, TNode dst) outSet.Add(dst); _nodes[dst]++; + _initialNodes.Remove(dst); } /// @@ -64,7 +72,7 @@ public IEnumerable TopologicalSort() { var refDict = new Dictionary(_nodes); - var list = new LinkedList(refDict.Where(x => x.Value == 0).Select(x => x.Key)); + var list = new LinkedList(_initialNodes); while (list.Count > 0) { @@ -86,32 +94,34 @@ public IEnumerable TopologicalSort() throw new CyclicGraphException(remaining.Select(x => x.Key)); } - public IEnumerable ParallelTopologicalSort() + /// + /// Topologically sort the graph, invoking the callback in parallel where possible + /// + /// The callback to invoke + /// A task following the parallel sort + public async Task ParallelTopologicalSort(Action callback) { - var refDict = new Dictionary(_nodes); + var refDict = new ConcurrentDictionary(_nodes); - var list = new LinkedList(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()) - { - 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 diff --git a/ProjectCeilidh.Cobble/Extensions.cs b/ProjectCeilidh.Cobble/Extensions.cs index d78b243..11c6188 100644 --- a/ProjectCeilidh.Cobble/Extensions.cs +++ b/ProjectCeilidh.Cobble/Extensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace ProjectCeilidh.Cobble { diff --git a/ProjectCeilidh.Cobble/Generator/BareLateInstanceGenerator.cs b/ProjectCeilidh.Cobble/Generator/BareLateInstanceGenerator.cs index a09a587..e5b3c4f 100644 --- a/ProjectCeilidh.Cobble/Generator/BareLateInstanceGenerator.cs +++ b/ProjectCeilidh.Cobble/Generator/BareLateInstanceGenerator.cs @@ -26,5 +26,7 @@ public BareLateInstanceGenerator(object instance) } public object GenerateInstance(object[] args) => _instance; + + public override string ToString() => $"BareLateInstanceGenerator({_instance.GetType().FullName})"; } } diff --git a/ProjectCeilidh.Cobble/Generator/DictionaryInstanceGenerator.cs b/ProjectCeilidh.Cobble/Generator/DictionaryInstanceGenerator.cs index 7fa98a6..55156a0 100644 --- a/ProjectCeilidh.Cobble/Generator/DictionaryInstanceGenerator.cs +++ b/ProjectCeilidh.Cobble/Generator/DictionaryInstanceGenerator.cs @@ -43,6 +43,8 @@ public object GenerateInstance(object[] args) return proxy; } + public override string ToString() => $"DictionaryInstanceGenerator({_contractType.FullName})"; + /// /// Provides a way to implement the specified contract at runtime. /// diff --git a/ProjectCeilidh.Cobble/Generator/TypeLateInstanceGenerator.cs b/ProjectCeilidh.Cobble/Generator/TypeLateInstanceGenerator.cs index cf5217b..2b9a530 100644 --- a/ProjectCeilidh.Cobble/Generator/TypeLateInstanceGenerator.cs +++ b/ProjectCeilidh.Cobble/Generator/TypeLateInstanceGenerator.cs @@ -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})"; } } diff --git a/ProjectCeilidh.Cobble/ProjectCeilidh.Cobble.csproj b/ProjectCeilidh.Cobble/ProjectCeilidh.Cobble.csproj index 3e42f28..9332426 100644 --- a/ProjectCeilidh.Cobble/ProjectCeilidh.Cobble.csproj +++ b/ProjectCeilidh.Cobble/ProjectCeilidh.Cobble.csproj @@ -12,7 +12,7 @@ https://github.com/Ceilidh-Team/Cobble.git 2018 Olivia Trewin true - 1.1.0 + 1.1.1 diff --git a/README.md b/README.md index 2bbd1df..b71b309 100644 --- a/README.md +++ b/README.md @@ -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) From 69cefcafcedf113118d0709ce463d50f854748cd Mon Sep 17 00:00:00 2001 From: Olivia Trewin Date: Sat, 8 Sep 2018 13:50:39 -0500 Subject: [PATCH 2/2] OSX unit tests --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index de5a92e..93a68c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: csharp solution: ceilidh-core.sln mono: none dotnet: 2.1.302 +os: + - linux + - osx install: - dotnet restore script: