How Tracy Transforms Code
This page explains how Tracy's compiler plugin transforms your annotated functions at compile time. Understanding this transformation helps you debug issues and understand what happens "under the hood."
The Transformation at a Glance
When you annotate a function with @Trace, Tracy's compiler plugin wraps your function body with tracing logic.
Before Compilation
@Trace(name = "GreetUser")
fun greetUser(name: String): String {
println("Hello, $name!")
return "Greeting sent to $name"
}
After Compilation
fun greetUser(name: String): String {
return withTrace(
functionRef = ::greetUser,
args = arrayOf(name),
annotation = Trace(name = "GreetUser"),
body = {
println("Hello, $name!")
"Greeting sent to $name"
}
)
}
The withTrace function handles:
- Creating an OpenTelemetry span
- Recording function inputs and outputs
- Measuring execution time
- Propagating trace context
- Handling exceptions
The TracyGeneratorExtension
Tracy's transformation logic lives in TracyGeneratorExtension, which implements Kotlin's IrGenerationExtension interface.
The Transformation Process
For each annotated function, Tracy:
-
Finds the annotation: Checks if the function (or any overridden function) has
@Trace -
Creates a function reference: Builds an IR reference to the original function for metadata extraction
-
Captures arguments: Creates an array of all function parameters
-
Wraps the body in a lambda: Moves the original function body into a lambda expression
-
Generates the wrapper call: Replaces the function body with a call to
withTraceorwithTraceSuspended
Suspend Function Support
Tracy handles suspend functions differently from regular functions. Regular functions are wrapped with withTrace, while suspend functions use withTraceSuspended.
The suspend variant ensures proper coroutine context propagation and allows the traced function to suspend without blocking.
Annotation Propagation
One of Tracy's powerful features is automatic annotation propagation through inheritance hierarchies.
How It Works
When checking for @Trace, Tracy doesn't just look at the current function — it traverses all overridden functions in the hierarchy:
interface Service {
@Trace
fun process(data: String): Result
}
class ServiceImpl : Service {
// No annotation here, but still traced!
override fun process(data: String): Result {
return Result.success(data)
}
}
The ServiceImpl.process() function will be traced because it overrides an annotated function in the Service interface.
Implementation Detail
Tracy uses allOverridden(true) to traverse the entire override chain and find the first @Trace annotation:
private fun IrSimpleFunction.findOverriddenAnnotationWithPropagation(): IrConstructorCall? =
this.allOverridden(true).firstNotNullOfOrNull {
it.annotations.findAnnotation(traceAnnotationFqName)
}
This means you can:
- Annotate an interface method once
- Have all implementations automatically traced
- Override the annotation in specific implementations if needed
What Gets Captured
The transformed function captures several pieces of information:
Function Reference
A reference to the original function (::greetUser) is passed to the tracing infrastructure. This allows Tracy to extract:
- Function name
- Parameter names and types
- Return type
- Declaring class (if applicable)
Arguments Array
All function parameters are captured in an array:
Annotation Instance
The actual @Trace annotation is passed to the wrapper, including:
name: Custom span name (overrides the default method name)metadataCustomizer: ASpanMetadataCustomizerreference for custom serialization. Must be a Kotlinobject.
At runtime, the metadataCustomizer controls how span names are resolved, and how inputs/outputs are serialized into span attributes.
Multiplatform Considerations
Tracy's compiler plugin supports Kotlin Multiplatform by:
-
Finding the correct symbol: The plugin looks for the non-
expectdeclaration ofwithTrace: -
Platform-agnostic IR: All transformations happen at the IR level, which is platform-independent
-
Version-specific builds: Tracy provides compiler plugin builds for each Kotlin version to ensure compatibility