ComposeInvestigator Internals¶
ComposeInvestigator is built by utilizing the Kotlin compiler plugin and the open source of the Compose compiler.
@Composable fun Main(args: Any) {
val count = mutableStateOf(0)
Text(text = "Count: $count")
}
val composeInvestigatorTable: ComposableInvalidationTrackTable = ComposableInvalidationTrackTable()
val composableCallstack: Stack<String> = Stack()
@Composable fun Main(args: Any, $composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup()
if (!$composer.skipping) {
val affectFields = mutableListOf()
val argsValueParam = ValueParameter("args", "kotlin.Any", args.toString(), args.hashCode(), Certain(false))
affectFields.add(argsValueParam)
val invalidationReason = composeInvestigatorTable.computeInvalidationReason("fun-Main(Any,Composer,Int)Unit", affectFields)
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", composableCallstack.toList(), AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
ComposeInvestigatorConfig.invalidationLogger(composableCallstack.toList(), AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
val count = mutableStateOf(0).registerStateObjectTracking(
composer = $composer,
composable = AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column),
composableKeyName = "fun-Main(Any,Composer,Int)Unit",
stateName = "count",
)
try {
composableCallstack.push("my.package.name.Main")
Text("$count")
} finally {
composableCallstack.pop()
}
} else {
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", composableCallstack.toList(), AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Skipped)
ComposeInvestigatorConfig.invalidationLogger(composableCallstack.toList(), AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Skipped)
$composer.skipToGroupEnd()
}
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", composableCallstack.toList(), AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(Invalidate))
ComposeInvestigatorConfig.invalidationLogger(composableCallstack.toList(), AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(Invalidate))
Main(args, $composer, $changed)
}
}
Composable call stacks tracking¶
Composable callstack tracing has been implemented since issue #77 and is still an experimental feature.
The concept is simple: Wrap all calls to composable functions in try-finally
, and push the
parent function name onto the stack before calling the composable. Then pop it from finally
.
val composableCallstack: Stack<String> = Stack()
@Composable fun Main() {
try {
composableCallstack.push("my.package.name.Main")
Call()
} finally {
composableCallstack.pop()
}
}
If you have an idea for a better way to track callstacks, please open an issue.
Recomposition tracking¶
Recomposition tracing involves three different kinds of code generation.
- compute composable argument changes
- detect composable invalidation requests
- detect composable invalidation skips
Composable argument change detection starts by sending all the arguments of the composable to the
ComposableInvalidationTrackTable
. It then calculates which arguments have changed and determines
the reason for the recomposition.
val composeInvestigatorTable: ComposableInvalidationTrackTable = ComposableInvalidationTrackTable()
@Composable fun Main(args: Any) {
val affectFields = mutableListOf()
val argsValueParam = ValueParameter("args", "kotlin.Any", args.toString(), args.hashCode(), Certain(false))
affectFields.add(argsValueParam)
val invalidationReason = composeInvestigatorTable.computeInvalidationReason("fun-Main(Any)Unit", affectFields)
Text(args.toString())
}
If the composable body has been executed, it means that the composable has been recomposed, so we generate recomposition logging and event sending code in the first line of the composable body.
val composeInvestigatorTable: ComposableInvalidationTrackTable = ComposableInvalidationTrackTable()
@Composable fun Main(args: Any) {
val affectFields = mutableListOf()
val argsValueParam = ValueParameter("args", "kotlin.Any", args.toString(), args.hashCode(), Certain(false))
affectFields.add(argsValueParam)
val invalidationReason = composeInvestigatorTable.computeInvalidationReason("fun-Main(Any)Unit", affectFields)
composeInvestigatorTable.callListeners("fun-Main(Any)Unit", callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
ComposeInvestigatorConfig.invalidationLogger(callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
Text(args.toString())
}
It also generates recomposition logging and event sending code in the body of the invalidation request lambda.
val composeInvestigatorTable: ComposableInvalidationTrackTable = ComposableInvalidationTrackTable()
@Composable fun Main(args: Any, $composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup()
val affectFields = mutableListOf()
val argsValueParam = ValueParameter("args", "kotlin.Any", args.toString(), args.hashCode(), Certain(false))
affectFields.add(argsValueParam)
val invalidationReason = composeInvestigatorTable.computeInvalidationReason("fun-Main(Any,Composer,Int)Unit", affectFields)
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
ComposeInvestigatorConfig.invalidationLogger(callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
Text(args.toString())
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(Invalidate))
ComposeInvestigatorConfig.invalidationLogger(callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(Invalidate))
Main(args, $composer, $changed)
}
}
Finally, we also generate recomposition skip logging and event sending code in the body of the code that performs the invalidation skip.
val composeInvestigatorTable: ComposableInvalidationTrackTable = ComposableInvalidationTrackTable()
@Composable fun Main(args: Any, $composer: Composer?, $changed: Int) {
$composer = $composer.startRestartGroup()
if (!$composer.skipping) {
val affectFields = mutableListOf()
val argsValueParam = ValueParameter("args", "kotlin.Any", args.toString(), args.hashCode(), Certain(false))
affectFields.add(argsValueParam)
val invalidationReason = composeInvestigatorTable.computeInvalidationReason("fun-Main(Any,Composer,Int)Unit", affectFields)
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
ComposeInvestigatorConfig.invalidationLogger(callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(invalidationReason))
Text(args.toString())
} else {
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Skipped)
ComposeInvestigatorConfig.invalidationLogger(callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Skipped)
$composer.skipToGroupEnd()
}
$composer.endRestartGroup()?.updateScope { $composer: Composer?, $force: Int ->
composeInvestigatorTable.callListeners("fun-Main(Any,Composer,Int)Unit", callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(Invalidate))
ComposeInvestigatorConfig.invalidationLogger(callstacks, AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column), Processed(Invalidate))
Main(args, $composer, $changed)
}
}
State change tracking¶
All state variables that inherit from State
or Animatable
generate registerStateObjectTracking
code to enable tracking of state changes.
@Composable fun Main($composer: Composer?, $changed: Int) {
val count = mutableStateOf(0).registerStateObjectTracking(
composer = $composer,
composable = AffectedComposable("Main", "my.package.name", "MyFileName.kt", line, column),
composableKeyName = "fun-Main(Composer,Int)Unit",
stateName = "count",
)
}