Debugging

Testing Plans

Use OrderedPlan#assertValid method to test whether the plan will execute correctly when passed to Injector#produce.

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

class A(b: B)
class B

def badModule = new ModuleDef {
  make[A]
  make[B].fromEffect(zio.Task { ??? })
}

val badPlan = Injector[cats.effect.IO]().plan(badModule, Roots.target[A])
// badPlan: izumi.distage.model.plan.OrderedPlan = 
// {effect.{type.MdocSession::App0::B}} (debugging.md:22) := value zio.ZIO$EffectPartial#399976329
// {type.MdocSession::App0::B} (debugging.md:22) := effect[λ %0 → zio.ZIO[-Any,+Throwable,+0]]{effect.{type.MdocSession::App0::B}}
// {type.MdocSession::App0::A} (debugging.md:21) := call(π:Class(MdocSession::App0::B): MdocSession::App0::A) {
//   arg b: MdocSession::App0::B = lookup({type.MdocSession::App0::B})
// }
// the effect types are mismatched - `badModule` uses `zio.Task`, but we expect `cats.effect.IO`

badPlan.assertValid[cats.effect.IO]().unsafeRunSync()
// izumi.distage.model.exceptions.InvalidPlanException: 
// - Incompatible effect types when trying to execute operation:
// 
//   - {type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B} (debugging.md:22) := effect[λ %0 → zio.ZIO[-Any,+Throwable,+0]]{effect.{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B}/zio.ZIO[-Any,+Throwable,+Nothing]}
// 
// Can't execute an effect in `λ %0 → zio.ZIO[-Any,+Throwable,+0]` which is neither equivalent to `izumi.fundamentals.platform.Identity`, nor a subtype of the Injector's effect type: `λ %0 → cats.effect.IO[+0]`
// 
//   - To execute `make[_].fromEffect` and `make[_].fromResource` bindings for effects other than `Identity`, you must parameterize the `Injector` with the corresponding effect type when creating it, as in `Injector[F]()`.
//   - Subtype type constructors are allowed. e.g. when using ZIO you can execute effects in `IO[Nothing, ?]` when using an `Injector[IO[Throwable, ?]]()`.
//        
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.$anonfun$isValid$3(OrderedPlanOps.scala:58)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.$anonfun$isValid$3$adapted(OrderedPlanOps.scala:57)
// 	at scala.Option.map(Option.scala:242)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.isValid(OrderedPlanOps.scala:57)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.isValid$(OrderedPlanOps.scala:52)
// 	at izumi.distage.model.plan.OrderedPlan.isValid(AbstractPlan.scala:36)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.assertValid(OrderedPlanOps.scala:38)
// 	at izumi.distage.model.plan.impl.OrderedPlanOps.assertValid$(OrderedPlanOps.scala:37)
// 	at izumi.distage.model.plan.OrderedPlan.assertValid(AbstractPlan.scala:36)
// 	at repl.MdocSession$App0$$anonfun$4.apply$mcV$sp(debugging.md:33)
// 	at repl.MdocSession$App0$$anonfun$4.apply(debugging.md:33)
// 	at repl.MdocSession$App0$$anonfun$4.apply(debugging.md:33)
def goodModule = new ModuleDef {
  make[A]
  make[B].fromEffect(cats.effect.IO(new B))
}

val plan = Injector[cats.effect.IO]().plan(goodModule, Roots.target[A])
// plan: izumi.distage.model.plan.OrderedPlan = 
// {effect.{type.MdocSession::App0::B}} (debugging.md:42) := value cats.effect.IO$Delay#-1482545196
// {type.MdocSession::App0::B} (debugging.md:42) := effect[λ %0 → cats.effect.IO[+0]]{effect.{type.MdocSession::App0::B}}
// {type.MdocSession::App0::A} (debugging.md:41) := call(π:Class(MdocSession::App0::B): MdocSession::App0::A) {
//   arg b: MdocSession::App0::B = lookup({type.MdocSession::App0::B})
// }
// the effect types in `goodModule` and here match now

plan.assertValid[cats.effect.IO]().unsafeRunSync()

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())
// {effect.{type.MdocSession::App0::B}} (debugging.md:42) := value cats.effect.IO$Delay#-1482545196
// {type.MdocSession::App0::B} (debugging.md:42) := effect[λ %0 → cats.effect.IO[+0]]{effect.{type.MdocSession::App0::B}}
// {type.MdocSession::App0::A} (debugging.md:41) := call(π:Class(MdocSession::App0::B): MdocSession::App0::A) {
//   arg b: MdocSession::App0::B = lookup({type.MdocSession::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[A]))
// DepNode({type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.A},DependencyGraph(Map({effect.{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B}/cats.effect.IO[+MdocSession::App0::B]} -> Set(), {type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B} -> Set({effect.{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B}/cats.effect.IO[+MdocSession::App0::B]}), {type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.A} -> Set({type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B})),Depends),0,None,Set())

// Print reverse dependencies
println(plan.topology.dependees.tree(DIKey[B]))
// DepNode({type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B},DependencyGraph(Map({effect.{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B}/cats.effect.IO[+MdocSession::App0::B]} -> Set({type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B}), {type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.B} -> Set({type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.A}), {type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.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:

# izumi-reflect macros
-Dizumi.debug.macro.rtti=true

# izumi.distage.constructors.* macros
-Dizumi.debug.macro.distage.constructors=true

# Functoid macro
-Dizumi.debug.macro.distage.functoid=true

Graphviz rendering

Add GraphDumpBootstrapModule to your Injector’s configuration to enable writing GraphViz files with a graphical representation of the OrderedPlan. Data will be saved to ./target/plan-last-full.gv and ./target/plan-last-nogc.gv in the current working directory.

import distage.{GraphDumpBootstrapModule, Injector}

Injector(GraphDumpBootstrapModule)
// res6: Injector[izumi.fundamentals.platform.functional.package.Identity[A]] = izumi.distage.InjectorDefaultImpl@73ae7fc7

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