This project provides a set of tools to visualize the execution of Kotlin suspend functions in real-time. It consists of a Kotlin compiler plugin, a coroutine integration library, and a Compose Multiplatform GUI.
compiler-plugin: An IR-based Kotlin compiler plugin that instruments suspend functions.stack-tracking-core-api: Public-facing APIs and annotations (e.g.,@NonTracked) used by the plugin and integration.gradle-plugin: Integrates the compiler plugin into Gradle builds.tracked-call-tree-as-flow: Logic to capture instrumented calls and expose them as aFlowof events.call-tree-visualizer-gui: A Compose Multiplatform application for real-time visualization.examples: Example code (e.g.,highlyBranchingCalls) that uses the visualization.
-
Instrumentation (Compiler Plugin):
- The
CallStackTrackingTransformervisits everysuspendfunction. - If the function is not
@NonTracked, notinline, and has a body, it wraps the entire body in a call tocom.woutwerkman.calltreevisualizer.stackTracked. stackTrackedtakes the function's Fully Qualified Name (FQN) and the original body as a lambda.
- The
-
Tracking Context (
stack-tracking-core-api):stackTrackedlooks up aStackTrackingContextin the currentcoroutineContext.- It delegates the tracking to
StackTrackingContext.track(functionFqn, body).
-
Event Emission (
tracked-call-tree-as-flow):trackingCallStacksprovides aStackTrackingContextimplementation.- When
trackis called:- A new
CallTreeNodeis created. - A
CallStackPushTypeevent is sent through achannelFlow. - The
childlambda (original function body) is executed within a newCoroutineContextcontaining thechildNode. - Upon completion, a
CallStackPopTypeevent is sent. - If an exception occurs, a
CallStackThrowTypeorCallStackCancelledevent is sent.
- A new
-
Visualization (GUI):
CallTreeUIcollects events from theFlowproduced bytrackingCallStacks.- It maintains a
CallTreedata structure (usingkotlinx.collections.immutable). - The UI renders the tree, showing active calls, completed calls, and exceptions/cancellations.
This is the heart of the instrumentation. It uses Kotlin IR to rewrite functions. It specifically handles:
- Avoiding
inlinefunctions (as they don't have a stable call site in the same sense). - Replacing
IrReturntargets to point to the new lambda instead of the original function. - Injecting the function FQN as a constant string.
Manages the coroutine state and event generation.
- Uses
AtomicIntfor unique node IDs. - Handles cancellation specifically by checking
Job.isActivewhen aCancellationExceptionis caught. - Uses
NonCancellablecontext to ensure "Pop" or "Error" events are sent even if the flow scope is cancelled.
Contains interesting test cases:
highlyBranchingCalls: Useskotlinhax.shadowroutines(a fork ofkotlinx.coroutines) to demonstrate complex branching and yielding.measureLinearly: Demonstrates unstructured concurrency and deep call stacks.
- Shadowroutines: The project depends on
kotlinhax.shadowroutines, which must be available inmavenLocal(). - Compiler Plugin Testing: The
compiler-pluginmodule hastest-fixturesandtestDatafor JVM box tests and diagnostic tests. - Gradle Plugin: To use the visualizer in a project, apply the
com.woutwerkman.calltreevisualizerplugin.
- The project relies on
kotlin-compilerinternal APIs for the IR transformation. - The UI uses
androidx.composeandorg.jetbrains.composecomponents. - The
shadowroutinesfork is likely used to provide deeper integration or "shadowing" of standard coroutine behaviors for better visualization (e.g., customyield,launch,asyncthat interact with the tracker).