blob: 01cb9997f06cfdb2e323cb63a4da071957ed522d [file] [log] [blame] [view]
# Interpreting Compose Compiler Metrics
## Context
The Compose Compiler plugin can generate reports / metrics around certain compose-specific concepts
that can be useful to understand what is happening with some of your compose code at a fine-grained
level.
## Enabling Metrics
### AndroidX repository
In the AndroidX repository `./gradlew` accepts options to enable reports.
To enable compiler metrics for a build target include `-Pandroidx.enableComposeCompilerMetrics=true`
prior to the build target such as:
```
.gradlew -Pandroidx.enableComposeCompilerMetrics=true :compose:runtime:runtime:compileKotlin
```
To enable compiler reports for a build target include `-Pandroidx.enableComposeCompilerReports=true`
prior to the build target such as:
```
.gradlew -Pandroidx.enableComposeCompilerReports=true :compose:runtime:runtime:compileKotlin
```
### Other Gradle projects
To enable metrics for a gradle module, include:
```
compileKotlin {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=<directory>"
)
}
```
where `<directory>` is replaced with the location you wish the report written.
To enabled reports for a gradle module, include:
```
compileKotlin {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=<directory>"
)
}
```
where `<directory>` is replaced with the location you wish the report written.
## Reports Breakdown
### Top Level Metrics (<your-module>-module.json)
This report shows some high level metrics that are compose specific. This is mostly to create
numeric data points which can be tracked over time. The ratios between some of these numbers can be
interesting: for instance the number of certainArguments versus totalArguments can give you a
percentage of arguments to composable calls that have metadata propagated.
Heres an example of the report for the foundation module:
```
{
"skippableComposables": 53,
"restartableComposables": 60,
"readonlyComposables": 1,
"totalComposables": 100,
"restartGroups": 60,
"totalGroups": 139,
"staticArguments": 25,
"certainArguments": 138,
"knownStableArguments": 377,
"knownUnstableArguments": 25,
"unknownStableArguments": 24,
"totalArguments": 426,
"markedStableClasses": 8,
"inferredStableClasses": 28,
"inferredUnstableClasses": 0,
"inferredUncertainClasses": 0,
"effectivelyStableClasses": 36,
"totalClasses": 36,
"memoizedLambdas": 40,
"singletonLambdas": 6,
"singletonComposableLambdas": 4,
"composableLambdas": 49,
"totalLambdas": 81
}
```
### Composable Signatures (<your module>-composables.txt)
This report is intended to be consumed by humans, and is printed in pseudo-kotlin style function
signatures. This report shows every composable function in the module, and breaks down each
parameter and information about each. This report indicates if the overall composable is
restartable, skippable, or readonly. Each parameter is marked to be either stable or unstable. And
each default parameter expression is marked as either static or dynamic.
```
restartable fun Image(
unstable bitmap: ImageBitmap
stable contentDescription: String?
stable modifier: Modifier? = @static Companion
stable alignment: Alignment? = @dynamic Companion.Center
stable contentScale: ContentScale? = @dynamic Companion.Fit
stable alpha: Float = @static DefaultAlpha
stable colorFilter: ColorFilter? = @static null
)
```
### Composables Table (<your module>-composables.csv)
This report is a CSV and is intended to be easily thrown into a spreadsheet and digested that way.
This holds high level metrics specific to every composable function.
### Classes (<your module>-classes.txt)
This report is also meant to be consumed by a human. It is written in pseudo-kotlin style class
signatures. This file is primarily meant for you to understand how the stable inferencing algorithm
interpreted a given class. Each class is indicated at the top level as being either stable,
unstable, or runtime. Runtime means that stability depends on other dependencies which will be
resolved at runtime (a type parameter or a type in an external module). Stability is determined by
the fields on the class, so each field is displayed as part of the class, and each field is marked
as either stable, unstable, or runtime stable as well. The <runtime stability> line at the bottom
indicates the expression that is used to resolve this stability at runtime.
```
stable class CornerBasedShape {
stable val topStart: CornerSize
stable val topEnd: CornerSize
stable val bottomEnd: CornerSize
stable val bottomStart: CornerSize
<runtime stability> = Stable
}
```
## Things To Look Out For
### Functions that are `restartable` but not `skippable`
In a composables.txt file, you may see some composable functions which are marked as restartable
but not marked as skippable. These two concepts are closely related, but distinct.
Skippability means that when called during recomposition, compose is able to skip the function if
all of the parameters are equal. Skippability is often very important for public APIs, and can have
a big performance impact if the chances of a composable getting called with the same inputs is
high. The typical reason for a function to not be skippable is when one or more of its parameters
types are not considered `Stable`.
Restartability means that this function serves as a scope where recomposition can start. Any
function that is skippable must be restartable for correctness, but functions can be restartable
and not skippable. Though restartability is needed for correctness when a function is skippable, it
can sometimes be beneficial even if the function is not skippable. If the function reads a `State`
value during its execution that is very likely to change (for instance, an animated value), then
restartability is very important, as if it is not restartable, then compose will use an ancestor
scope to initiate the recomposition when that state value changes.
If you see a function that is restartable but not skippable, its not always a bad sign, but it
sometimes is an opportunity to do one of two things:
1. Make the function skippable by ensuring all of its parameters are stable
2. Make the function not restartable by marking it as a `@NonRestartableComposable`
It is a good idea to do (1) if the function is a highly used public API, and if you think the
parameters not being stable is an oversight.
It is a good idea to do (2) if the composable function is unlikely to ever be the root of a
recomposition. In other words, if the composable function doesnt directly read any state variables,
it is unlikely that this restart scope is ever being used. This can be very difficult for the
compiler to determine though, so the restart scope is generated anyway unless you specify otherwise
directly with a `@NonRestartableComposable` annotation.
### Default parameter expressions that are `@dynamic`
Composable functions make heavy use of parameters default expressions. This is an important tool
that allows a composable to have an API that is both configurable and easy to use. Default
expressions are capable of executing any code that can be executed in the body of the function, so
for composable functions, that includes making calls to other composables. It also means that a
default expression can read a state variable and automatically cause the composable to get
subscribed to the state.
In order for default parameters to have all of this power, the compose compiler often has to
generate a fair amount of code around default expressions to make sure they behave in a predictable
and correct manner. This is only really necessary for composable calls, and for state reads, but it
is really difficult for the compiler to guarantee that an expression will not perform a state read,
since state reads can happen almost anywhere.
In the `composables.txt` file, you will see all default parameter value expressions prefixed with
either `@static` or `@dynamic`. You may find that the compiler is treating it as `@dynamic` even
though it seems like it should be `@static`. If this is the case, you should strive to make this
expression `@static` by making it an expression that compose can infer as such. You can do this by
marking the value as `@Stable`.
Default expressions should be `@static` in every case except for the following two cases:
1. You are explicitly reading an observable dynamic variable. Composition Locals and state
variables are an important example of this. In these cases, you need to rely on the fact that
the default expression will be re-executed when the value changes.
2. You are explicitly calling a composable function, such as `remember`. The most common use case
for this is state hoisting.
### Classes that are unstable
In the `classes.txt` file, you may see classes that are unstable. Not all classes need to be stable,
but a class being stable unlocks a lot of flexibility for the compose compiler to make
optimizations when a stable type is being used in places, which is why it is such an important
concept for compose.
The compose compiler will infer whether or not a given class is stable or not at compile time, and
its algorithm for doing that is not all that complicated. The compiler will look at all of the
fields on the class, and it cannot be inferred as stable if any one of the fields is:
1. The field is mutable (it is associated with a `var` property)
2. The field has a non-stable type
If any one of the fields on the class meets this criteria, it will be inferred as `unstable`, but
that doesnt mean that it cant be marked as stable. Sometimes mutable fields are used in ways that
are still safe in the context of Stability guarantees. For instance, a very common case is to use a
field to cache the result of some calculation. If the caching is only done for performance
reasons, and the public API of that class makes it impossible to know whether the value is cached
or not, then the class could still be marked stable.
In order to find the field(s) which are causing a class to be unstable, you simply need to look for
any field in the class that is a `var` or an `unstable val`.