Debugging

Testing Plans

Use Injector#assert 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.ZIO.attempt { ??? })
}
// the effect types are mismatched - `badModule` uses `zio.Task`, but we expect `cats.effect.IO`

Injector[cats.effect.IO]().assert(badModule, Roots.target[A])
// izumi.distage.model.exceptions.PlanVerificationException: Plan verification failed, issues were:
// 
// 
//  - IncompatibleEffectType({type.repl.MdocSession.MdocApp0.B},{type.repl.MdocSession.MdocApp0.B} BindingOrigin((debugging.md:22)) := effect[λ %0 → zio.ZIO[-scala.Any,+java.lang.Throwable,+0]]{effect.{type.repl.MdocSession.MdocApp0.B}/zio.ZIO[-scala.Any,+java.lang.Throwable,+scala.Nothing]},λ %0 → cats.effect.IO[+0],λ %0 → zio.ZIO[-scala.Any,+java.lang.Throwable,+0])
// 
// Visited keys:
// 
// 
//  - {type.repl.MdocSession.MdocApp0.A}
// 
// 	at izumi.distage.planning.solver.PlanVerifier$PlanVerifierResult.throwOnError(PlanVerifier.scala:308)
// 	at izumi.distage.model.Injector.assert(Injector.scala:270)
// 	at izumi.distage.model.Injector.assert$(Injector.scala:264)
// 	at izumi.distage.InjectorDefaultImpl.assert(InjectorDefaultImpl.scala:23)
// 	at repl.MdocSession$MdocApp0$$anonfun$3.apply$mcV$sp(debugging.md:30)
// 	at repl.MdocSession$MdocApp0$$anonfun$3.apply(debugging.md:30)
// 	at repl.MdocSession$MdocApp0$$anonfun$3.apply(debugging.md:30)
def goodModule = new ModuleDef {
  make[A]
  make[B].fromEffect(cats.effect.IO(new B))
}
// the effect types in `goodModule` and here match now

Injector[cats.effect.IO]().assert(goodModule, Roots.target[A])

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!

val plan = Injector().plan(goodModule, Roots.target[A]).getOrThrow()
// plan: izumi.distage.model.plan.Plan = 1: {effect.{type.MdocSession::MdocApp0::B}} BindingOrigin((debugging.md:41)) := value cats.effect.IO$Delay#-96351630
// 2: {type.MdocSession::MdocApp0::B} BindingOrigin((debugging.md:41)) := effect[λ %0 → cats.effect.IO[+0]]{effect.{type.MdocSession::MdocApp0::B}}
// 3: {type.MdocSession::MdocApp0::A} BindingOrigin((debugging.md:40)) :=
// 4:     call(π:Constructor(MdocSession::MdocApp0::B): MdocSession::MdocApp0::A) {
// 5:       arg b: MdocSession::MdocApp0::B <- {type.MdocSession::MdocApp0::B}
// 6:     }

println(plan.render())
// 1: {effect.{type.MdocSession::MdocApp0::B}} BindingOrigin((debugging.md:41)) := value cats.effect.IO$Delay#-96351630
// 2: {type.MdocSession::MdocApp0::B} BindingOrigin((debugging.md:41)) := effect[λ %0 → cats.effect.IO[+0]]{effect.{type.MdocSession::MdocApp0::B}}
// 3: {type.MdocSession::MdocApp0::A} BindingOrigin((debugging.md:40)) :=
// 4:     call(π:Constructor(MdocSession::MdocApp0::B): MdocSession::MdocApp0::A) {
// 5:       arg b: MdocSession::MdocApp0::B <- {type.MdocSession::MdocApp0::B}
// 6:     }

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.renderDeps(DIKey[A]))
// ➤ {type.MdocSession::MdocApp0::A} BindingOrigin((debugging.md:40))
//   ⮑ 1: {type.MdocSession::MdocApp0::B} BindingOrigin((debugging.md:41))
//     ⮑ 2: {effect.{type.MdocSession::MdocApp0::B}} BindingOrigin((debugging.md:41))
// 

// Print reverse dependencies
println(plan.renderDependees(DIKey[B]))
// ➤ {type.MdocSession::MdocApp0::B} BindingOrigin((debugging.md:41))
//   ↖ 1: {type.MdocSession::MdocApp0::A} BindingOrigin((debugging.md:40))
//

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 distage.Plan. 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[[A]izumi.fundamentals.platform.functional.package.Identity[A]] = izumi.distage.InjectorDefaultImpl@6b4186b2

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

Command-line activation

You may activate GraphViz dump for a distage-framework Role-based application by passing a --debug-dump-graph option:

./launcher --debug-dump-graph :myrole

Testkit activation

You may activate GraphViz dump in distage-testkit tests by setting PlanningOptions(addGraphVizDump = true) in config:

import izumi.distage.testkit.scalatest.Spec2
import izumi.distage.testkit.TestConfig
import izumi.distage.framework.config.PlanningOptions

final class MyTest extends Spec2[zio.IO] {
  override def config: TestConfig = super.config.copy(
    planningOptions = PlanningOptions(
      addGraphVizDump = true,
    )
  )
}
Launcher activation

PlanningOptions are also modifiable in distage-framework applications:

import distage.{Module, ModuleDef}
import izumi.distage.framework.config.PlanningOptions
import izumi.distage.roles.RoleAppMain
import zio.IO

abstract class MyRoleLauncher extends RoleAppMain.LauncherBIO[IO] {
  override protected def roleAppBootOverrides(argv: RoleAppMain.ArgV): Module = new ModuleDef {
    make[PlanningOptions].from(PlanningOptions(addGraphVizDump = true))
  }
}