Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dd-kcp-repro

Minimal reproduction: Datadog Android Gradle plugin's Jetpack Compose auto-instrumentation (composeInstrumentation = InstrumentationMode.AUTO) silently defeats Compose recomposition skipping for any composable that receives a forwarded Modifier.

Reproduces on plugin 1.21.0 and 1.28.0 (latest at time of writing). Kotlin 2.2.21, AGP 8.7.3, Compose BOM 2025.04.01. The Datadog SDK is never initialized — the compile-time Modifier rewrite alone causes the defect.

The app

A LazyColumn of 100 rows fed from a StateFlow. Every 500 ms exactly one row's value changes. Rows receive their Modifier through a small chain of plain skippable composables (ScreenRowItemRowInner) — idiomatic modifier forwarding.

Each row detects wasted executions: its body re-ran while already composed, with data equal to what it last rendered — exactly the work Compose skipping exists to eliminate. Initial composition, scroll-into-view, and genuine data changes are all excluded (LazyColumn disposes off-screen rows, resetting the row's remember), so the metric is scroll-proof. The header shows a live verdict:

  • skipping works → wasted per change = 0 (green)
  • skipping defeated → wasted per change ≈ visible-row count (red)

Run it

# Variant A — instrumentation AUTO (the default here), applicationId com.example.ddkcprepro.auto
./gradlew :app:installDebug

# Variant B — instrumentation DISABLE, applicationId com.example.ddkcprepro.off
./gradlew :app:installDebug -PddOff=true

Both install side by side. Open each and read the header after ~30 seconds.

Measured (Pixel 8a, Android 15, ~21 rows visible)

Build wasted row executions per change
AUTO (plugin 1.21.0) ~17–21 (= visible rows)
AUTO (plugin 1.28.0) ~17–21 (= visible rows)
DISABLE 0.0

Every data change re-executes every visible row's body under AUTO; zero wasted executions with instrumentation disabled. The waste scales with visible-row count × update rate.

Mechanism

In AUTO mode the plugin's Kotlin compiler plugin rewrites every Modifier argument at every composable call site:

// source:
RowInner(data = data, modifier = modifier)
// compiled:
RowInner(data = data, modifier = modifier.instrumentedDatadog("RowItem", false))

instrumentedDatadog ends in:

private fun Modifier.datadogSemantics(name: String, isImage: Boolean): Modifier {
    return this.semantics {          // <- Function1 capturing (name, isImage)
        this.datadog = name
        if (isImage) { this[SemanticsProperties.Role] = Role.Image }
    }
}

Modifier.semantics {} produces an AppendedSemanticsElement whose equality is the equality of its properties lambda. The lambda captures (name, isImage), so a new instance is allocated on every execution of the call site, and lambda classes have no equals(). Result: every time a parent recomposes, the child's modifier argument is a fresh, never-equal value. Compose's skipping compares arguments by equality — one never-equal argument means the child can never skip, regardless of how stable everything else is. The defeat cascades down the entire forwarded-modifier chain.

This is invisible to normal tooling: compiler stability reports are written before IR rewriting, the source contains nothing to find, and there is no log or warning. Apps just do (visible-rows × update-rate) more composition work.

Verify the injection (optional)

# AUTO build: > 0 injected call sites; DISABLE build: 0
dexdump -d app/build/outputs/apk/debug/app-debug.apk 2>/dev/null | grep -c "instrumentedDatadog"

Layout Inspector's recomposition/skip columns show the same story: with AUTO, unchanged rows' recomposition counts climb in lockstep and skips stay ~0; with DISABLE, siblings pin at ~0 recompositions with climbing skips.

Suggested fix

Implement the injected tag as an equality-stable ModifierNodeElement keyed on (name, isImage) (data-class equality) instead of semantics {} around a fresh capturing lambda. Two elements with equal (name, isImage) would then compare equal and skipping would survive instrumentation.

About

Minimal repro: Datadog Android SDK Compose auto-instrumentation (composeInstrumentation = AUTO) defeats Jetpack Compose recomposition skipping

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages