Lesson 2

Multiplatform behavior has boundaries

Separate common runtime semantics from target-specific synchronization, atomics, thread identity, and frame-clock integration.

4 min readUpdated Jul 12, 2026

Evidence legend: Durable marks common semantics; Version-specific marks a pinned source-set implementation; Experimental marks a changing or host-dependent path.

“Common code” tells you which algorithm is shared. It does not tell you which lock, atomic primitive, thread identity, or frame host surrounds that algorithm. The safest multiplatform explanation has two layers: first the common contract, then each target’s actual implementation.

What is common

Durable The common runtime defines the snapshot/composition machinery, an expect-based SnapshotThreadLocal, a synchronization abstraction, and the MonotonicFrameClock interface. The frame contract says a clock suspends until a frame, invokes the callback in the dispatch context, and supplies a strictly increasing time base; the host chooses how (MonotonicFrameClock.kt#L20-L61). That is the semantic boundary. A caller still has to provide an appropriate clock in its coroutine context.

The common SnapshotThreadLocal uses a main-thread fast path and a keyed map for other thread IDs, with writes protected by the common synchronization abstraction (SnapshotThreadLocal.kt#L20-L66). The map algorithm is shared; what counts as a thread and what makes a write safe are not.

Verified source matrix

The cells below report only what the pinned source sets show. Unknown means this research did not establish a stronger claim.

TargetSynchronization / atomic implementationThread identity / thread-local evidenceDefault frame implementationConcurrency or host guarantee
JVM (desktop)Synchronization.desktop.kt delegates to kotlin.synchronized; JVM/Android atomics use java.util.concurrent.atomic.AtomicReference/AtomicInteger (desktop sync, JVM atomics)Thread.currentThread().id; non-Android main-thread ID is -1 (thread actual, non-Android ID)Desktop actual delays at 60 fps and uses System.nanoTime() (ActualDesktop.desktop.kt)Unknown: this does not prove an application scheduler or UI-frame policy.
AndroidKotlin/JVM synchronization and Java atomics; Android actual uses Any locks and kotlin.synchronized (Android sync)Main ID comes from Looper.getMainLooper().thread.id; host-stub fallback is -1 (Android thread actual)Real device uses Choreographer; host-side stubs use a delayed fallback (Android frame actual)Verified only for this clock implementation: host/device branches differ; app scheduling beyond the clock is Unknown.
Nativekotlinx.atomicfu atomics and kotlinx.atomicfu.locks.synchronized (Native sync, non-JVM atomics)@ThreadLocal counter supplies a per-thread ID (Native thread actual)yield() then a monotonic TimeSource reading (Native frame actual)Unknown: the source proves primitives and clock behavior, not a universal Native threading model for an app.
JSWeb source aliases synchronization to Any and makes the synchronized block a direct call; non-JVM atomics are implemented through atomicfu (web sync, non-JVM atomics)currentThreadId() is 0 and the name is main (web thread actual)window.requestAnimationFrame (JS frame actual)Unknown: no parallel-worker or snapshot-conflict guarantee is established here.
WasmShares the web synchronization source and non-JVM atomic actualShares the web thread actual in the pinned source treewindow.requestAnimationFrame (Wasm frame actual)Unknown: no broader Wasm host/thread guarantee is established here.

Two details prevent common overreach. First, “atomic” names an implementation primitive, not a claim that all composition operations may run concurrently. Second, the deprecated DefaultMonotonicFrameClock is no longer used by Compose runtime according to each actual’s KDoc; a real host may install a local clock instead.

Reproduce source-set boundaries

From the pinned AndroidX checkout:

git grep -n "actual .*SynchronizedObject\|actual .*Atomic\|actual .*currentThreadId\|actual val DefaultMonotonicFrameClock" -- compose/runtime/runtime/src
git grep -n "class SnapshotThreadLocal\|interface MonotonicFrameClock" -- compose/runtime/runtime/src/commonMain
./gradlew :compose:runtime:runtime:desktopTest \
  --tests 'androidx.compose.runtime.MonotonicFrameClockTest'

Controls: read common declarations beside every actual; compare the Android real-device and host-stub branches; do not substitute a JS result for Wasm. Expected: the five source families above appear, with explicit target implementations and the frame-host differences shown in the matrix. Limits: a source scan and JVM test do not validate every target binary, browser host, worker configuration, or platform UI toolkit. This procedure was not executed in this content edit.

Misconceptions and check

  • “Common source means identical locks.” The common abstraction has JVM, Native, and web actuals with different behavior.
  • “JS’s thread ID of zero proves all JS concurrency semantics.” It proves this source’s identity function only; worker and host behavior remain Unknown.
  • “A frame clock is Android’s frame scheduler.” The interface is common; Android’s pinned actual uses Choreographer, while other targets use different clocks.

When reviewing a new target, fill a matrix row from actual files and target tests. Leave the cell blank/Unknown until a source or test earns the stronger statement.

Evidence ledger

ClaimEvidenceLabel
Common code defines contracts, not platform implementationsSynchronization.kt, MonotonicFrameClock.kt#L20-L61Durable
JVM, Android, Native, web, and frame actuals differ as shownPlatform links in the matrixVersion-specific
Unknown cells must not be generalizedSource-set evidence does not establish the missing guaranteeDurable

Freshness

Refresh when source-set names, expect/actual primitives, Native memory/concurrency rules, web host support, or frame-clock deprecations change. Re-run target tests instead of inferring behavior from a common file.

Finished this lesson?

Your progress stays only in this browser.