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:

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>() {
    compilerOptions.freeCompilerArgs.addAll(
        "-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:

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>() {
    compilerOptions.freeCompilerArgs.addAll(
        "-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 (-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.

Here’s 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 (-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 (-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 (-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 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, it’s 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 doesn’t 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 doesn’t mean that it can’t 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.