-
Notifications
You must be signed in to change notification settings - Fork 233
Commit 7b99a72
Joshua Rogers
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 7b99a72Copy full SHA for 7b99a72
File tree
Expand file treeCollapse file tree
1 file changed
+108
-50
lines changedOpen diff view settings
Filter options
- src/TinyIoC
Expand file treeCollapse file tree
1 file changed
+108
-50
lines changedOpen diff view settings
0 commit comments