Lesson 2
Multiplatform behavior has boundaries
Separate common runtime semantics from target-specific synchronization, atomics, thread identity, and frame-clock integration.
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.
| Target | Synchronization / atomic implementation | Thread identity / thread-local evidence | Default frame implementation | Concurrency 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. |
| Android | Kotlin/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. |
| Native | kotlinx.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. |
| JS | Web 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. |
| Wasm | Shares the web synchronization source and non-JVM atomic actual | Shares the web thread actual in the pinned source tree | window.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
| Claim | Evidence | Label |
|---|---|---|
| Common code defines contracts, not platform implementations | Synchronization.kt, MonotonicFrameClock.kt#L20-L61 | Durable |
| JVM, Android, Native, web, and frame actuals differ as shown | Platform links in the matrix | Version-specific |
| Unknown cells must not be generalized | Source-set evidence does not establish the missing guarantee | Durable |
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.