Debugging

Testing Plans

Use OrderedPlan#assertImportsResolvedOrThrow method to test whether all dependencies in a given plan are present and the plan will execute correctly when passed to Injector#produce.

import distage.{DIKey, GCMode, ModuleDef, Injector}

class A(b: B)
class B

val badModule = new ModuleDef {
  make[A]
}
// badModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.A}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.B): repl.Session::repl.Session.App0::repl.Session.App0.A)) ((debugging.md:21)))

val badPlan = Injector().plan(badModule, GCMode.NoGC)
// badPlan: izumi.distage.model.plan.OrderedPlan = {type.Session::App0::B} (debugging.md:21) := import {type.repl.Session::repl.Session.App0::repl.Session.App0.B} // required for {type.Session::App0::A}
// {type.Session::App0::A} (debugging.md:21) := call(<function1>(Session::App0::B): Session::App0::A) {
//   arg b: Session::App0::B = lookup({type.Session::App0::B})
// }
badPlan.assertImportsResolvedOrThrow
// izumi.distage.model.exceptions.InvalidPlanException: 
// - Instance is not available in the object graph: {type.repl.Session::repl.Session.App0::repl.Session.App0.B}.
// Required by refs:
//  - {type.repl.Session::repl.Session.App0::repl.Session.App0.A}
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.$anonfun$assertImportsResolved$1(OrderedPlanOps.scala:46)
// 	at scala.util.Either$LeftProjection.map(Either.scala:573)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.assertImportsResolved(OrderedPlanOps.scala:45)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.assertImportsResolved$(OrderedPlanOps.scala:42)
// 	at izumi.distage.model.plan.OrderedPlan.assertImportsResolved(AbstractPlan.scala:36)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.assertImportsResolvedOrThrow(OrderedPlanOps.scala:56)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.assertImportsResolvedOrThrow$(OrderedPlanOps.scala:55)
// 	at izumi.distage.model.plan.OrderedPlan.assertImportsResolvedOrThrow(AbstractPlan.scala:36)
// 	at repl.Session$App0$$anonfun$5.apply$mcV$sp(debugging.md:32)
// 	at repl.Session$App0$$anonfun$5.apply(debugging.md:32)
// 	at repl.Session$App0$$anonfun$5.apply(debugging.md:32)
val goodModule = new ModuleDef {
  make[A]
  make[B]
}
// goodModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.A}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.B): repl.Session::repl.Session.App0::repl.Session.App0.A)) ((debugging.md:40)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.B}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.B)) ((debugging.md:41)))

val plan = Injector().plan(goodModule, GCMode.NoGC)
// plan: izumi.distage.model.plan.OrderedPlan = {type.Session::App0::B} (debugging.md:41) := call(<function1>(): Session::App0::B) {}
// {type.Session::App0::A} (debugging.md:40) := call(<function1>(Session::App0::B): Session::App0::A) {
//   arg b: Session::App0::B = lookup({type.Session::App0::B})
// }

plan.assertImportsResolvedOrThrow

Pretty-printing plans

You can print the output of plan.render() to get detailed info on what will happen during instantiation. The printout includes source and line numbers so your IDE can show you where the binding was defined!

println(plan.render())
// {type.Session::App0::B} (debugging.md:41) := call(<function1>(): Session::App0::B) {}
// {type.Session::App0::A} (debugging.md:40) := call(<function1>(Session::App0::B): Session::App0::A) {
//   arg b: Session::App0::B = lookup({type.Session::App0::B})
// }

print-test-plan

You can also query a plan to see the dependencies and reverse dependencies of a specific class and their order of instantiation:

// Print dependencies
println(plan.topology.dependencies.tree(DIKey.get[A]))
// DepNode({type.repl.Session::repl.Session.App0::repl.Session.App0.A},DependencyGraph(Map({type.repl.Session::repl.Session.App0::repl.Session.App0.B} -> Set(), {type.repl.Session::repl.Session.App0::repl.Session.App0.A} -> Set({type.repl.Session::repl.Session.App0::repl.Session.App0.B})),Depends),0,None,Set())

// Print reverse dependencies
println(plan.topology.dependees.tree(DIKey.get[B]))
// DepNode({type.repl.Session::repl.Session.App0::repl.Session.App0.B},DependencyGraph(Map({type.repl.Session::repl.Session.App0::repl.Session.App0.B} -> Set({type.repl.Session::repl.Session.App0::repl.Session.App0.A}), {type.repl.Session::repl.Session.App0::repl.Session.App0.A} -> Set()),Required),0,None,Set())

The printer highlights circular dependencies:

print-dependencies

To debug macros used by distage you may use the following Java Properties:

sbt -Dizumi.debug.macro.rtti=true compile # fundamentals-reflection & LightTypeTag macros
sbt -Dizumi.debug.macro.distage.constructors=true compile # izumi.distage.constructors.* macros
sbt -Dizumi.debug.macro.distage.providermagnet=true compile # ProviderMagnet macro

Graphviz rendering

Add GraphDumpBootstrapModule to your Injector’s configuration to enable dumping of graphviz files with a graphical representation of the Plan.

import distage.GraphDumpBootstrapModule

val injector = Injector(GraphDumpBootstrapModule())
// injector: Injector = izumi.distage.InjectorDefaultImpl@179ab107

Data will be saved dumped to ./target/plan-last-full.gv and ./target/plan-last-nogc.gv in current working directory.

You’ll need a GraphViz installation to render these files into a viewable PNG images:

dot -Tpng target/plan-last-nogc.gv -o out.png

plan-graph