mcpField
D
diagnosing-compose-stability

diagnosing-compose-stability

github.com/skydoves开发520141 次安装19 周前更新
代码仓库

关于 diagnosing-compose-stability

面向 Claude 的技能,用于诊断 Jetpack Compose 可组合项为何无法跳过。它引导开发者检查 release 构建的编译器报告,定位不稳定参数并确定问题优先级,再交由独立技能修复。

主要功能

  1. 01为 release 构建启用 Compose Compiler 报告
  2. 02检查可组合项的可跳过状态与参数稳定性
  3. 03从类报告追溯不稳定类型的原因
  4. 04结合热路径与运行时测量排列问题优先级
  5. 05整理问题清单并交由修复技能处理

适用场景

  1. 01排查过度重组或丢帧
  2. 02查明可组合项无法跳过的原因
  3. 03审查性能敏感 UI 的稳定性依据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skydoves/compose-performance-skills --skill diagnosing-compose-stability

安装后在 Claude Code 中输入/diagnosing-compose-stability确认已加载。

附带文件references/reading-classes-txt.mdreferences/reading-composables-txt.md

详细介绍

诊断 Compose 稳定性——先查看编译器报告

Compose 会通过比较参数来跳过重组。参数不稳定时,跳过功能会被禁用——本技能会告诉 Claude 如何找出哪些参数不稳定,以及原因。输出是一份按优先级排列的不稳定类型和不可跳过的可组合项列表;修复方法见 ../stabilizing-compose-types/SKILL.md。

何时使用此技能

  • 开发者询问“为什么会发生重组?”,报告卡顿、丢帧或滚动不流畅。
  • @TraceRecomposition 日志显示的重组次数超过了有意义的状态变化次数。
  • 开发者提到 Compose Compiler Reports、classes.txt、composables.txt、composables.csv、module.json 或“不可跳过”。
  • 开发者询问如何查找不稳定参数,或 List<Foo>、LocalDateTime 或某种领域类型是否稳定。
  • 代码审查者要求提供证据,证明对性能敏感的可组合项可以跳过。

何时不使用此技能

  • 已知不稳定类型——直接转到 ../stabilizing-compose-types/SKILL.md。
  • 问题是读取了错误阶段的状态(Modifier.alpha(state.value));请使用 ../../recomposition/deferring-state-reads/SKILL.md。
  • 问题是误用了 derivedStateOf;请使用 ../../recomposition/choosing-derivedstateof/SKILL.md。
  • 开发者希望通过 CI 实施门禁,而不是进行一次性诊断;请使用 ../enforcing-stability-in-ci/SKILL.md。

前置条件

  • 使用 Kotlin 2.0.0+,并应用 Compose Compiler Gradle 插件:id("org.jetbrains.kotlin.plugin.compose")。2.0 之前的 kotlinCompilerExtensionVersion 流程已废弃。
  • 目标模块必须有可构建的 release 变体。报告必须在 release 模式下生成;debug 会添加 Live Literals,使常量看起来像动态值,并扭曲所有报告。
  • 应用 org.jetbrains.kotlin.plugin.compose Gradle 插件(Kotlin 2.0+)。composeCompiler { … } 扩展由该插件提供;AGP 版本无关紧要。
  • 可选但**推荐:**开发者的目标应是建立稳定性基线,而不是追求 100% 可跳过(skydoves 热门观点 #1——可跳过性是诊断指标,不是 KPI)。

工作流

  • 1. 在模块的 build.gradle.kts 中启用报告。 仅限 release 构建——debug 构建会产生误导性数据。
// app/build.gradle.kts (or any compose module)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
}

composeCompiler {
    // Only emit reports for release to avoid Live Literals noise.
    val isReleaseBuild = providers.gradleProperty("composeCompilerReports").orNull == "true"
    if (isReleaseBuild) {
        reportsDestination = layout.buildDirectory.dir("compose_compiler")
        metricsDestination = layout.buildDirectory.dir("compose_compiler")
    }
}

或使用始终启用的形式:

composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_compiler")
    metricsDestination = layout.buildDirectory.dir("compose_compiler")
    // Optional — opt mutable third-party types into stability:
    stabilityConfigurationFiles.add(
        rootProject.layout.projectDirectory.file("stability_config.conf")
    )
}
  • 2. 构建 release 变体,以排除 Live Literals 和解释模式产生的噪声:
./gradlew :app:assembleRelease -PcomposeCompilerReports=true

对于库模块:./gradlew :feature-feed:assembleRelease。必须使用 release flavor——见下文的 WRONG/RIGHT。

  • 3. 找到四个输出文件。 它们写入 <module>/build/compose_compiler/:
app/build/compose_compiler/
├── app_release-classes.txt        # per-class stability
├── app_release-composables.txt    # per-composable signatures
├── app_release-composables.csv    # CSV mirror of the above (CI-friendly)
└── app_release-module.json        # aggregate counts

如果缺少任何文件,说明插件未针对该变体运行——请重新检查 composeCompiler { reportsDestination = ... } 是否配置在正确的模块中,以及构建是否为 release。

  • 4. 首先打开 composables.txt。 这是信号最强的文件。搜索未跟在 skippable 后面的 restartable 行。每一行都表示一个无法跳过的重组入口点。在每个代码块中,查看各参数的前缀:stable、unstable、@static、@dynamic。任何 unstable 参数都会阻止跳过。完整语法见 references/reading-composables-txt.md。

  • 5. 打开 classes.txt,了解类为何不稳定。 对于 composables.txt 中标记为 unstable 的每种类型,在 classes.txt 中找到其声明。该行会说明原因是 var 字段、泛型参数还是不稳定的嵌套类型。runtime stable class Box { stable val value: T } 这种形式表示“仅当替换后的 T 的运行时 $stable: Int 字段表明其稳定时,此类才稳定”——见 references/reading-classes-txt.md。

  • 6. 打开 module.json 查看排查数据。 其中包含可跳过的可组合项、可重启的可组合项、稳定类等的数量。可用于比较修复前后的情况,或决定优先处理哪个模块。切勿将这些数量视为目标——它们用于发现回归,而不是追求 100%。

  • 7. 按热点路径确定优先级。 启动时只运行一次的不可跳过可组合项无关紧要;位于 LazyColumn 项内的则至关重要。修复前,请结合测量数据进行交叉验证(使用 skydoves/compose-stability-analyzer 的 @TraceRecomposition,或 Macrobenchmark 的 FrameTimingMetric)——见 ../../measurement/tracing-recompositions-at-runtime/SKILL.md。

  • 8. 移交给修复技能。 列出不稳定类型和有问题的可组合项,并应用 ../stabilizing-compose-types/SKILL.md。

模式

模式:HighlightedSnacks — 解读不可跳过的可组合项

编译器报告会直接在函数签名中呈现原因。通过这个真实示例向开发者说明。

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int,
  unstable snacks: List<Snack>,                  // <-- blocks skipping
  stable onSnackClick: Function1<Long, Unit>,
)

诊断步骤:

  1. 函数是 restartable,但没有 skippable 前缀。因此,每当其父项重组时,它都会重组。
  2. 原因是 unstable snacks: List<Snack> 参数。kotlin.collections.List 是接口;编译器无法证明其实现不可变。
  3. 打开 classes.txt 并查找 Snack。如果 Snack 本身是 unstable,先修复数据类;如果它是 stable,那么问题仅在 List 包装类型。
  4. 移交给修复技能:将 List<Snack> 替换为 kotlinx.collections.immutable.ImmutableList<Snack>,或在开发者认可这一约定的前提下,将 kotlin.collections.* 添加到 stability_config.conf。
模式:“stable but runtime”——runtime 的含义
runtime stable class Box {
  stable val value: T
}

这并不表示不稳定。编译器会在运行时生成合成的 $stable: Int 字段,并在组合过程中查询它。仅当替换后的 T 报告为稳定时,此类才稳定。切勿使用 @Stable 注解 runtime 类来“提升”其稳定性——运行时检查才是正确机制。

模式:错误与正确——运行构建
# WRONG
./gradlew :app:assembleDebug
# WRONG because: debug enables Live Literals; constant 0 dp becomes a getter, every literal looks dynamic, and counts in module.json drift versus what ships to users.
# RIGHT
./gradlew :app:assembleRelease -PcomposeCompilerReports=true
模式:错误与正确——读取报告文件
// WRONG — reading composables.txt without checking the leading flags
fun MyScreen(...)
// WRONG because: skipping the `restartable`/`skippable` prefix discards the only data point that determines whether unstable params actually cost anything.
// RIGHT — every diagnosis quotes the full prefix and per-param annotations
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MyScreen(
  stable user: User,
  stable onClick: Function0<Unit>,
)
模式:输出目录为空

如果发布构建后 build/compose_compiler/ 缺失或为空:

  1. 确认此模块已应用 org.jetbrains.kotlin.plugin.compose 插件——该扩展是按模块配置的。
  2. 确认构建确实编译了 Kotlin 源码(而不是因任务已是最新而未执行)。修改一个文件,或运行 ./gradlew :app:clean :app:assembleRelease。
  3. 确认 reportsDestination 设置在 composeCompiler { } 块内,而不是通过旧版 kotlinOptions 的 freeCompilerArgs 流程设置。

强制规则

  • MUST 构建 release 变体。Debug 构建会生成 Live Literals,让常量看起来像动态值,并扩大报告中“unstable”的范围。
  • MUST 阅读 composables.txt 中按参数列出的注解(stable/unstable/@static/@dynamic),不能只看函数名。标志前缀就是诊断依据。
  • MUST 将不稳定参数交叉对照 classes.txt,以确定根本原因(var、泛型、不稳定的字段类型或接口)。
  • MUST NOT 追求 100 percent 的可跳过性。Skydoves 的犀利观点 #1:可跳过性是诊断指标,不是 KPI。启动时只运行一次的可组合函数不需要具备可跳过性。
  • MUST NOT 仅凭报告就将类标注为 @Stable——该决定应由修复技能做出,并由其评估契约。
  • MUST NOT 只读取 composables.csv 的行数就认为工作已完成。这些计数是回归哨兵;逐行注解才是真正的诊断依据。
  • PREFERRED: 通过 ../enforcing-stability-in-ci/SKILL.md,在 CI 中接入相同的标志(skydoves compose-stability-analyzer 插件或社区的 ComposeGuard 插件),以便在 PR 审查中发现回归。
  • PREFERRED: 为每次发布记录一个基线 module.json,以便通过差异发现未来的回归。

验证

  • 应用 org.jetbrains.kotlin.plugin.compose 插件后,./gradlew :app:assembleRelease(或针对特定模块的构建)成功完成。
  • 以下四个文件均存在:<module>_release-classes.txt、<module>_release-composables.txt、<module>_release-composables.csv、<module>_release-module.json。
  • 至少具备以下之一:从 composables.txt 复制的不稳定参数位置列表,或来自 module.json、确认计数为零并证明无需修复的结果。
  • 已将每个不稳定参数追溯到 classes.txt 中的一个类,以便修复技能获得具体的根本原因(var、List、LocalDateTime 等)。
  • 开发者理解,runtime stable class … 不是问题——这是编译器正确发出的惰性稳定性信息。

参考资料

评论

支持 Markdown