Skip to content

Commit 7b99a72

Browse files
author
Joshua Rogers
committed
When resolving an object from TinyIoC, before resolution, it first needs
to verify that it's dependencies can be resolved. The way it handles this is to reflect a list of all constructors from the dependency and to go through them checking to see if all parameters for the constructor can themselves be constructed. This continues recursively, until a constructor that can be satisfied can be found for each dependency and the original object. After finding that an object can be resolved, TinyIoC then begins construction, a process that requires performing the previous calculation again to determine the correct constructor, as no data is cached. This alone causes an slight performance degredation, but the problem only appears when multiplied by the prescence of large dependency trees, multiple resolution, or the presence of constructors that cannot be successfully resolved. In all three cases, the performance problem itself is a result of repeated recalculation of constructability for the same types. **Large Dependency Tree** Consider the below as an example of a small "large dependency tree". ``` class Foo { public Foo(Bar bar, Baz baz, Qux qux) { } } class Bar { public Bar(Baz baz, Qux qux) { } } class Baz { public Baz(Qux qux) { } } class Qux { } ``` Due to the lack of caching, a request to resolve Foo leads to the following: - Check if Foo can be resolved - Check if Bar can be resolved - Check if Baz can be resolved - Check if Qux can be resolved - Check if Qux can be resolved - Check if Baz can be resolved - Check if Quz can be resolved - Check if Qux can be resolved This totals 1 check for Bar, 2 for Baz, and 3 for Qux. For a total of 6 tests for eligability. As the same logic must be performed again during the resolution stage, after we are certain that a constructor chain exists, the effective number of tests is doubled, coming in at 12. **Multiple Resolutions** Another possibility is that we have a number of event handlers all of which need to be resolved and which share dependencies, such as one might expect of a database or logger. ``` public class FooEventHandler1 : IFooEventHandler { public FooEventHandler1(Foo foo) { } } public class FooEventHandler2 : IFooEventHandler { public FooEventHandler2(Foo foo) { } } public class FooEventHandler3 : IFooEventHandler { public FooEventHandler3(Foo foo) { } } public class FooEventHandler4 : IFooEventHandler { public FooEventHandler4(Foo foo) { } } ``` Assuming the above classes are all registered with TinyIoC as IFooEventHandler, then a call to `ResolveAll<IFooEventHandler>()` would check that the common dependency Foo could be resolved on each handler resolution, for a total of four checks of the same type. If, continuing with the class definitions from the previous example, we remember that each check of Foo causes 6 tests to occur, followed by another 6 during the actual construction of the object, then we could expect this call to ResolveAll to cause 6 * (4 + 1) test for a total of 30 tests. **Unusable Constructors** There's still one more case that leads to test-multiplication: if the most preferable constructor (the one requiring the most arguments) has a dependency that cannot be resolved via IoC, such as for the case where it is generated by a factory or a deserializer. ``` class Foo { public Foo(Bar bar, Baz baz, Qux qux, Bang bang) { } public Foo(Bar bar, Baz baz, Qux qux) { } } ``` If we were to update our original Foo class to now have a constructor that requires an unresolvable type "Bang", the effect is that we manage to double the test performed: TinyIoC will test Bar, Baz, and Qux successfully before finding that Bang cannot be constructed. With the knowledge that this constructor cannot be resolved, TinyIoC will then try the next constructor in line, attempting a second time to resolve Bar, Baz, and Qux again, not remembering that we've already found them to be resolvable. The effect here is (4 + 1) * (6 + 6 + 1): Each of the 4 IFooEventHandlers requiring the chain be tested, followed by one final test in during construction in order to find the appropriate constructor. The result, given these simplistic classes and their hierarchy, is 65 tests even though there are only 10 constructors defined between the 9 classes. **Summary** Given these three very real and very common patterns, the effective number of checks can be in the millions in a larger codebase, taking 20-30 seconds per request, depending on the number of handlers needing construction. **Resolution** The resolution for this issue is rather straightforward: after testing a Type or constructor, the result should be stored so that it does not have to be recalculated. My first approach was to store the value within TinyIoC so that separate calls to Resolve or ResolveAll would be able to use a common cache. This turned out to be non-practical though, since calls to these methods can specify extra parameters that affect resolution, such as named resolution. Using a global cache would require that either named resolutions were not cached or that logic was complicated to cache them while preventing a memory leak. Given that limitation, I took the approach of creating a cache that gets passed around internally over the lifetime of the Resolve call and gets populated as tests occur. This means that repeated calls to Resolve will perform the same checks, but that each check will only be performed a single time within a Resolve, changing what could be quadradic growth of time into linear time.
1 parent 49889cc commit 7b99a72

File tree

1 file changed

+108
-50
lines changed

1 file changed

+108
-50
lines changed

0 commit comments

Comments
 (0)