Lesson 1

Source information becomes tooling data

Map compiler-emitted source strings and markers to composition groups, diagnostic traces, tracer events, and scope observers.

4 min readUpdated Jul 12, 2026

Evidence legend: Durable marks a stable mental model; Version-specific marks behavior tied to the pins above; Experimental marks a tooling-only, opt-in, or changing surface.

A crash in measure does not carry the original Kotlin call stack through every composition operation. Instead, Compose can build a second, diagnostic path: the compiler leaves source metadata around groups, the runtime stores it, and tooling interprets it later.

Compiler markers are breadcrumbs, not a source map

Version-specific The pinned compiler lowering accepts collectSourceInformation and traceMarkersEnabled switches. With source collection enabled, it emits sourceInformation immediately after a group starts and emits sourceInformationMarkerStart/End around calls whose group was elided—ReadOnlyComposable is the example in the lowering comments (ComposableFunctionBodyTransformer.kt#L373-L386, #L815-L850). The string is an internal encoding of function/call information, file, and offsets; it is not the original stack frame.

The runtime’s CompositionData exposes a tree of CompositionGroups. A group has a compiler key, optional sourceInfo, an optional emitted node, and slot data; its source-info format is explicitly internal and meant to be translated by tools (CompositionData.kt#L30-L93). This is the mapping:

Diagram
flowchart LR
    A[Compiler source markers] --> B[Composer groups]
    B --> C[CompositionData]
    C --> D[Tooling or diagnostics]
    B --> E[Compose stack frames]

A useful boundary follows: tooling can inspect a call/group tree and node association, but CompositionData is not a supported slot-table API. Its own KDoc tells tools to use a higher-level interpretation. Treat data, keys, and offsets as evidence at this revision, not application contracts.

Traces and errors use different channels

Version-specific CompositionTracer is a global internal hook. The compiler can guard traceEventStart(key, dirty1, dirty2, info) and traceEventEnd() behind isTraceInProgress; the current signature carries a generated group key, dirty metadata, and display text (Composer.kt#L1087-L1148). The runtime documentation deliberately permits occasional information loss and says callers should not synchronize it. A trace is therefore a low-friction instrumentation stream, not a complete event log.

Diagnostic stack traces are another path. ComposeStackTraceMode.SourceInformation records source information and appends a suppressed exception when a crash occurs; GroupKeys is less precise but works with minified builds and adds no runtime overhead until a crash (ComposeStackTrace.kt#L38-L94). LocalCompositionErrorContext lets custom node code attach a diagnostic trace to failures in measure, layout, or draw. It returns false when the node or source information cannot be found, including a minified build without source information (CompositionErrorContext.kt#L24-L65).

Observers report scope edges, not “all recomposition”

Experimental CompositionObserver reports composition begin/end, scope enter/exit, reads, invalidation, and disposal. Its KDoc warns that a state invalidation is not necessarily composed: a branch may disappear, or movable content may move elsewhere. It also says state-change invalidations are normally reported before recomposition, not synchronously at the write (CompositionObserver.kt#L85-L156). Use it to explain observed scope edges, not to infer a universal scheduler or a rendered-frame count.

Reproduce the mapping

At the pinned AndroidX and Kotlin checkouts:

# AndroidX runtime tooling and observer behavior
./gradlew :compose:runtime:runtime:desktopTest \
  --tests 'androidx.compose.runtime.tooling.CompositionDataTests' \
  --tests 'androidx.compose.runtime.CompositionObserverTests'

# Kotlin compiler emission and source/no-source variants
./gradlew :plugins:compose-compiler-plugin:compiler-hosted:integration-tests:test \
  --tests 'androidx.compose.compiler.plugins.kotlin.TraceInformationTest' \
  --tests 'androidx.compose.compiler.plugins.kotlin.RuntimeTestsK2'

Controls: compare CompositionDataTests.canFindSourceInfo with a no-source compiler variant; compare observer read/invalidation callbacks with a branch that disappears. Expected: groups can expose source info when collection is enabled, compiler goldens show paired marker calls, and observer events distinguish reads from invalidations/disposal. Limits: these tests do not guarantee an IDE’s presentation, a custom node’s error path, or behavior after an optimizer changes the generated code. This procedure was not executed in this content edit.

Build caveats and check

  • “A Kotlin stack trace is preserved.” The runtime reconstructs Compose frames from group keys, source strings, and offsets.
  • “Source information is free.” The pinned diagnostic docs call out runtime overhead and advise against enabling source-information traces in unoptimized release builds.
  • “R8 only removes useful diagnostics.” Minification can remove source information, but group-key mode is designed to remain usable with mapping files.

Before enabling a diagnostic mode, ask: is this a trace stream, a scope-observation stream, or a crash-time reconstructed path? If the build can remove source markers, keep the fallback and the limitation visible.

Evidence ledger

ClaimEvidenceLabel
Compiler emission is optional and markers can stand in for elided groupsComposableFunctionBodyTransformer.kt#L373-L386Version-specific
Groups expose internal source info, nodes, and slot data to toolingCompositionData.kt#L30-L93Version-specific
Source and group-key stack traces have different cost/precision trade-offsComposeStackTrace.kt#L38-L94Version-specific

Freshness

Refresh when compiler source-info flags, marker pairing, CompositionData parsing, diagnostic stack-trace modes, tracer delivery, or observer callback timing changes. Re-check optimizer rules before making a release-build recommendation.

Finished this lesson?

Your progress stays only in this browser.