Performance
Sting is primarily used in java web applications compiled to javascript using either GWT or the more modern J2CL and was developed with specific performance goals in mind.
- Fast incremental build times.
- Small code size.
- Fast initialization time.
Fast incremental build times make developers happy and increase productivity. Fast refresh times are essential for modern web development. Small code size and fast initialization times make the end users of our websites happy.
The performance is measured relative to Dagger as most of the initial Sting applications were migrated from Dagger. Dagger is a much more mature, established and reasonably optimized library and thus is a great library to compare against.
A procedurally generated "sample application" was developed to be representative of real world application usage of dependency injectors. This sample application helped keep the focus on Stings performance goals.
Build Time
Sting optimizes incremental injector rebuilds over rebuilding an entire application. When Sting processes
a type annotated with either the @Injectable
annotation or the
@Fragment
annotation, the Sting annotation processor will generate a small, binary
descriptor describing the type. The descriptor includes all the information required by Sting to bind the
type to an injector. When the annotation processor attempts to process a type annotated by the
@Injector
annotation, the annotation processor will load the binary descriptors rather
than attempting to load and analyze the type.
This results in a small increase in time compiling types annotated with the @Injectable
annotation or the @Fragment
annotation as the annotation processor needs to write
the binary descriptor file. As of Java 8, writing a non-java source file from an annotation processor is
relatively slow as it forces a synchronous write to the filesystem from within the compiler. However, reading
the binary descriptor rather than the java type when processing types annotated with the
@Injector
annotation is significantly faster.
The table below compares the ratio of the speed of dagger in various scenarios with the speed of Sting. A value
of 1
indicates that they are exactly the same speed while a value of 0.5
would indicate Sting takes twice as
long as Dagger and a value of 2.0
indicates Sting takes half as long as dagger.
Variant | Component Count | Full Compile | Incremental Compile |
---|---|---|---|
Tiny | 10 | 1.048 | 1.273 |
Small | 50 | 0.760 | 1.773 |
Medium | 250 | 0.637 | 1.968 |
Large | 500 | 0.742 | 2.408 |
Huge | 1000 | 0.732 | 2.696 |
Stings architecture gives a nice little performance boost for incremental rebuilds in most circumstances with a slight performance penalty for the initial compile or full rebuilds. As Sting matures, it is expected that the performance penalty for full rebuilds will decrease slightly but will always exist. There are many further optimizations possible in incremental recompiles that will likely lead to even faster recompiles in the future.
Code Size
Sting prioritizes smaller code size. The builtin support for @Eager
components
and the ease of optimizing when eager components are present is a significant contributor to Stings
relatively good performance in this area.
Most web applications contain "eager" components that are expected to be instantiated on application startup to perform actions like subscribing to an event broker, adding listeners for browser events, initializing graphics contexts etc. Dagger-based applications often implement eager components by adding an accessor for the eager component onto the dagger injector and the invoking that accessor early in the application lifecycle.
Variant | Component Count | Eager % | Sting Size | Dagger Size | Size Delta |
---|---|---|---|---|---|
Eager Tiny | 10 | 100% | 14901 | 18935 | +4034 |
Tiny | 10 | 50% | 15005 | 18895 | +3890 |
Lazy Tiny | 10 | 0% | 15203 | 18855 | +3652 |
Eager Small | 50 | 100% | 22187 | 33630 | +11443 |
Small | 50 | 50% | 22739 | 33398 | +10659 |
Lazy Small | 50 | 0% | 23476 | 33146 | +9670 |
Eager Medium | 250 | 100% | 57594 | 105513 | +47919 |
Medium | 250 | 50% | 60287 | 104271 | +43984 |
Lazy Medium | 250 | 0% | 64126 | 103050 | +38924 |
Eager Large | 500 | 100% | 101828 | 195545 | +93717 |
Large | 500 | 50% | 107308 | 193045 | +85737 |
Lazy Large | 500 | 0% | 114914 | 190582 | +75668 |
Eager Huge | 1000 | 100% | 202066 | 404356 | +202290 |
Huge | 1000 | 50% | 218720 | 399357 | +180637 |
Lazy Huge | 1000 | 0% | 232827 | 394393 | +161566 |
As expected, Sting has much smaller code sizes than Dagger when high proportions of the components are "eager". Surprisingly, Sting also has smaller code sizes when all the components are lazy.
Sample Application Description
The sample application used during performance testing is procedurally generated from a number of input
parameters. The generator first generates an directed graph in memory. (Dagger and Sting only support directed
graphs with circular dependencies replaced with either javax.inject.Provider
style dependencies in Dagger
or java.util.function.Supplier
style dependencies in Sting.) The graph consists of a number of layers of nodes
where nodes in one layer can only depend upon nodes in the previous layer.
Several different configurations or "variants" were used during performance evaluation. It was found that the most important aspect was how big the component graph was and what proportion of the components were eagerly created. So we developed "tiny", "small", "medium", "large" and "huge" variants that had 50% of the components as eager. We also developed variants where 0% or 100% of the components are eager and these variants had names prefixed with "lazy_" and "eager_" respectively.
It is expected that most applications that use Sting are in the "medium" or "eager_medium" category. Although it should be noted that when the Sting authors were migrating applications to Sting, all but one of the applications where closer to the "large" category.
The number of inputs into a component seemed to have no significant performance impact in either application but the length of dependency chain does have an impact.
Variant | Component Count | Eager % | Layer Count |
---|---|---|---|
Eager Tiny | 10 | 100% | 2 |
Tiny | 10 | 50% | 2 |
Lazy Tiny | 10 | 0% | 2 |
Eager Small | 50 | 100% | 5 |
Small | 50 | 50% | 5 |
Lazy Small | 50 | 0% | 5 |
Eager Medium | 250 | 100% | 5 |
Medium | 250 | 50% | 5 |
Lazy Medium | 250 | 0% | 5 |
Eager Large | 500 | 100% | 5 |
Large | 500 | 50% | 5 |
Lazy Large | 500 | 0% | 5 |
Eager Huge | 1000 | 100% | 10 |
Huge | 1000 | 50% | 10 |
Lazy Huge | 1000 | 0% | 10 |
The object graph in the sample application is intended to be reasonably representative of the type of object graphs that appear in real-life applications without being unduly biased towards Sting or Dagger. However, the tests do focus on incremental build time, initialization time and code-size which are Sting strengths. This is primarily because this is what Sting is focused upon and intends to improve upon in the future.
It should be noted that the example application only includes @javax.inject.Inject
annotated types for Dagger
and @Injectable
annotated types for Sting and not components provided by @dagger.Module
annotated types for Dagger or @Fragment
annotated types for Sting. This is primarily because
it was too easy to introduce bias against Dagger in this scenario.