> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modaal.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Kotlin/Native GC and Swift ARC in One iPhone App

> How a Duet iPhone app runs its Kotlin core: the Kotlin/Native collector beside Swift's ARC, SKIE's generated Swift, who frees what, and how to check a build.

The iPhone app of a Duet project on the Kotlin Multiplatform flavor runs two memory managers in one process. The Kotlin core runs under the Kotlin/Native garbage collector, which is linked into the app executable. The Swift code that calls the core runs under Swift's automatic reference counting (ARC). This page shows what the app links, what the collector costs on the main thread, who frees each object that crosses between the two, and the rules a Duet shell follows as a result.

Every figure on this page comes from a build you can reproduce: **Foyer**, the app the [Duet tutorials](/tutorials/duet) build, at `tutorial9-complete`, and **Memory Lane**, a production app on the [App Store](https://apps.apple.com/us/app/memory-lane-private-journal/id6760589051) built on the same flavor.

*Measured October 1, 2026, with Kotlin 2.4.10, SKIE 0.10.14 and Xcode 26.6 on an Apple M4 Pro.*

## What does the iPhone app link?

The iPhone app links one static XCFramework, `FoyerKit.xcframework` in Foyer. Its iPhone slice, `ios-arm64/FoyerKit.framework/FoyerKit`, is an `ar` archive with two object files:

* **`FoyerKit.framework.o`**, written by the Kotlin/Native compiler: the Kotlin core compiled to arm64 machine code, an Objective-C class for every exported Kotlin class, and the Kotlin/Native runtime (the memory allocator, the garbage collector and the safepoint machinery).
* **`FoyerKit.o`**, written by Swift's compiler from source that SKIE generated: the Swift API your app calls. It contains no Kotlin code; it calls the Objective-C classes the first object defines.

A static framework is copied into the app executable at link time, so the collector runs inside the app's own process, on threads the runtime starts.

<Frame caption="The three layers of a Release iPhone build of Foyer. The app's Swift code and SKIE's Swift are managed by ARC; the Kotlin objects are managed by the Kotlin/Native collector.">
  <img className="block dark:hidden" src="https://mintcdn.com/modaal/ffMHllAekVB_tuiL/images/kotlin-native-link-graph-light.svg?fit=max&auto=format&n=ffMHllAekVB_tuiL&q=85&s=2ec132ba4170d4feb1718e037205ce09" alt="Block diagram of the Foyer app executable: the app's Swift code calls SKIE's generated Swift in FoyerKit.o, a 1.5 MB object with 5,009 Swift symbols and no Kotlin functions, which calls 229 Objective-C classes defined in FoyerKit.framework.o, a 16.2 MB object holding the compiled core (7,361 Kotlin functions, 559 Objective-C classes) and the runtime (allocator, concurrent mark and sweep collector, safepoints)" width="920" height="380" data-path="images/kotlin-native-link-graph-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/modaal/ffMHllAekVB_tuiL/images/kotlin-native-link-graph-dark.svg?fit=max&auto=format&n=ffMHllAekVB_tuiL&q=85&s=d2053c3a40fe67a2fb77ae854f383000" alt="Block diagram of the Foyer app executable: the app's Swift code calls SKIE's generated Swift in FoyerKit.o, a 1.5 MB object with 5,009 Swift symbols and no Kotlin functions, which calls 229 Objective-C classes defined in FoyerKit.framework.o, a 16.2 MB object holding the compiled core (7,361 Kotlin functions, 559 Objective-C classes) and the runtime (allocator, concurrent mark and sweep collector, safepoints)" width="920" height="380" data-path="images/kotlin-native-link-graph-dark.svg" />
</Frame>

The figures for both apps, from the Release `iosArm64` framework and the Release app executable:

| | Foyer | Memory Lane |
| - | - | - |
| Kotlin/Native object | 16.2 MB | 24.0 MB |
| Kotlin functions defined in it (`kfun:` symbols) | 7,361 | 11,963 |
| Collector, memory-manager and allocator symbols (`kotlin::gc::`, `kotlin::mm::`, `kotlin::alloc::`) | 54, 58, 96 | 54, 58, 96 |
| Concurrent-mark symbols (`gc::mark::ConcurrentMark`) | 16 | 16 |
| Objective-C classes defined in it | 559 | 852 |
| SKIE's Swift object | 1.5 MB | 2.6 MB |
| Swift symbols defined in it | 5,009 | 7,041 |
| Kotlin functions defined in it | 0 | 0 |
| Kotlin-defined Objective-C classes it references | 229 | 561 |
| Release app executable | 6.2 MB | 31.3 MB |

The object files are larger than the executable because they carry symbol tables and code the app never calls; the Release link drops both. Both executables contain the runtime's message `GC stop the world: concurrent mark took too long`, which is one way to confirm the collector shipped.

## How does Kotlin/Native manage memory?

Kotlin objects live in one heap that every thread shares. The [Kotlin/Native memory manager](https://kotlinlang.org/docs/native-memory-manager.html) reclaims them with a tracing collector:

* The collector is **concurrent mark and sweep**, with no generations. It runs on its own thread and starts a collection on memory-pressure heuristics or on a timer.
* **Marking runs concurrently with the app's threads.** Each collection stops the app's Kotlin-running threads twice, briefly; the runtime logs each stop as a "mutators pause".
* When marking takes too long, the runtime finishes it with the threads stopped and logs `GC stop the world: concurrent mark took too long`. When the heap grows past its target while a collection is running, the threads running Kotlin code wait until that collection finishes.

ARC frees a Swift object when its last strong reference goes away, at that moment and on that thread; the collector does not trace Swift objects.

### What the collector costs on the main thread

The runtime prints one block of lines per collection when the framework is built with its log option:

```kotlin src-kmp/apple-umbrella/build.gradle.kts theme={null}
target.binaries.framework {
  // One log block per collection on stderr; measurement builds only.
  freeCompilerArgs += "-Xruntime-logs=gc=info"
}
```

Each collection reports its two pauses as `Mutators pause time #1` and `#2`, measured from the moment the collector asks the threads to stop until they resume, plus the objects it marked and the heap before and after. The two pause times added together are how long one collection holds the main thread. `Time to pause` is the part of a pause spent waiting for every thread to reach a stopping point.

**Foyer, a scripted 100-second walk** on the iPhone 17 Pro simulator (iOS 26.5): guest sign-in, 55 taps on the buttons on screen, 14 deep links and swipes. Foyer allocates little, so its 10 collections came from the timer, one every 10.0 s, with about 970 live objects. The pauses of a collection added up to a median of 49 µs and at most 241 µs.

**A SwiftUI list scrolling at 60 frames per second on an iPhone 15 Pro (iOS 26.6.1)**, while the main thread creates Kotlin objects from Swift, far more than Foyer does, to show where the collector starts to cost frames. Each row is three 30-second runs of a Release build, timed by the app's own display link:

| Allocation on the main thread | Collections | Pauses per collection | Dropped frames |
| - | - | - | - |
| 300,000 Kotlin objects per second | 568, one every 0.16 s | median 78 µs; 99% under 3.1 ms; longest 13.7 ms | 1 of 5,399 (a 33 ms frame) |
| 1,200,000 Kotlin objects per second | 2,295, one every 0.04 s | median 19 µs; 99% under 0.07 ms; longest 7.7 ms | 1 of 5,400 (a 25 ms frame, outside any collection) |
| 300,000 Kotlin objects per second, collector disabled (`kotlin.native.binary.gc=noop`) | 0 | — | 0 of 5,400 |
| The same scroll with no Kotlin objects | — | — | 1 of 5,400 (a 25 ms frame) |

No pause lasted a full frame (16.7 ms), and the Kotlin runs dropped as many frames as the scroll with no Kotlin objects did. In the longest pause, 10.3 of the 13.7 ms was the wait for a thread to reach it. The collector-disabled build did not finish a run at 1,200,000 objects per second: it held 2.37 GiB when iOS logged a low-memory event, and it exited before its 30 seconds ended.

**Recording with Instruments changes the result.** With the Animation Hitches template recording, the same 300,000-objects-per-second runs dropped 15 frames in 5,301, and the longest frame lasted 869 ms; the runs with no Kotlin objects and with the collector disabled dropped none by the app's own timing. During the 869 ms frame the main thread was inside `+[KotlinBase allocWithZone:]`, in the allocator's page lookup, where it yielded the CPU 43,147 times, and the collector thread was blocked; the collection's two logged pauses added up to 40 µs. When a hitch trace shows a long frame inside a Kotlin allocation, run the same scenario without the recording and time the frames in the app before attributing the frame to the collector.

To see collections in Instruments next to your own intervals, also set `kotlin.native.binary.enableSafepointSignposts=true` in `gradle.properties`.

## What does SKIE add?

The Kotlin/Native compiler exports the core through an Objective-C header. [SKIE](https://skie.touchlab.co/) adds Swift source on top of that header, and Swift's compiler builds it into the framework's second object. SKIE generates wrappers only: the Kotlin runs as Kotlin/Native compiled it, and SKIE's Swift calls the Objective-C classes. Foyer's Release build generates 127 Swift files, under `src-kmp/apple-umbrella/build/skie/binaries/releaseFramework/RELEASE/iosArm64/swift/generated/`.

Its `StateFlow` wrapper shows the pattern. The Swift class holds the Kotlin flow and reads through to it:

```swift Skie/Skie.SkieSwiftStateFlow.swift (generated by SKIE 0.10.14) theme={null}
public final class SkieSwiftStateFlow<T> : FoyerKit.SkieSwiftFlowProtocol,
        Swift._ObjectiveCBridgeable {

    @_spi(SKIE)
    public let delegate: FoyerKit.Kotlinx_coroutines_coreStateFlow

    public var value: T {
        delegate.value as! T
    }
    // …bridging and makeAsyncIterator()
}
```

Holding a `SkieSwiftStateFlow` therefore holds the Kotlin `StateFlow`. Iterating it with `for await` creates a `SkieSwiftFlowIterator`, which starts a Kotlin collection and cancels it in its `deinit`.

What crosses as what:

| Kotlin declaration | What Swift sees |
| - | - |
| A sealed interface or class | An Objective-C protocol or class for each type in the hierarchy, plus `onEnum(of:)`, which returns a Swift enum to `switch` over exhaustively |
| `StateFlow<T>` returned by a top-level function | `SkieSwiftStateFlow<T>`: `.value`, and an `AsyncSequence` of `T` |
| A `StateFlow` declared on an exported interface | The raw `Kotlinx_coroutines_coreStateFlow`, with no element type ([Tutorial 4](/tutorials/duet-04-workers#add-the-two-streams-to-the-ports) re-exposes such streams as top-level functions) |
| A `suspend` function | An `async` Swift function |
| A Kotlin class | An Objective-C class with the module's prefix, such as `FoyerKitSplashState`, named `SplashState` in Swift |

## What happens when Swift holds a Kotlin object?

Swift holds a Kotlin object through its Objective-C class, so ARC counts the references. The Kotlin runtime treats every object with a nonzero count as a root: the collector does not free it while Swift holds it.

After the last Swift reference is released, the object is freed **on a later collection**, not at the release. The Kotlin page on [ARC integration](https://kotlinlang.org/docs/native-arc-integration.html#object-reclamation) states it: "An object is reclaimed only during the garbage collection." Nothing in the Kotlin object runs when Swift lets go of it.

<Frame caption="The objects behind one Foyer screen. ARC frees the Swift objects at their last release; the collector frees the Kotlin objects. The arrows that cross the boundary are the ones whose timing changes.">
  <img className="block dark:hidden" src="https://mintcdn.com/modaal/ffMHllAekVB_tuiL/images/kotlin-native-two-heaps-light.svg?fit=max&auto=format&n=ffMHllAekVB_tuiL&q=85&s=9d8464a279e27e73635373445a7ca30d" alt="Two panels. Swift objects: SplashViewShell holds BridgedStore; LiveSplashEnvironment holds the onDelegate closure, which points back to SplashViewShell through a weak capture. Kotlin objects: the Kotlin Store holds its StateFlow. BridgedStore holds the Kotlin Store through an ARC reference that is a GC root. The Kotlin Store retains LiveSplashEnvironment through objc_retain; the finalizer releases it on the main thread." width="920" height="470" data-path="images/kotlin-native-two-heaps-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/modaal/ffMHllAekVB_tuiL/images/kotlin-native-two-heaps-dark.svg?fit=max&auto=format&n=ffMHllAekVB_tuiL&q=85&s=019a2c33925ef4f82c2e196c74f626dd" alt="Two panels. Swift objects: SplashViewShell holds BridgedStore; LiveSplashEnvironment holds the onDelegate closure, which points back to SplashViewShell through a weak capture. Kotlin objects: the Kotlin Store holds its StateFlow. BridgedStore holds the Kotlin Store through an ARC reference that is a GC root. The Kotlin Store retains LiveSplashEnvironment through objc_retain; the finalizer releases it on the main thread." width="920" height="470" data-path="images/kotlin-native-two-heaps-dark.svg" />
</Frame>

Measured with Foyer's splash store in a test program on macOS (the harness in [the cycle section](#why-does-a-cycle-across-the-boundary-leak-and-how-do-you-avoid-it)): after the last Swift reference to the store was released, the store was freed 0.30 s later while the program allocated Kotlin objects, and 10.08 s later while it allocated nothing, when the collector's timer fired.

## What happens when Kotlin holds a Swift object?

A Duet feature hands Swift objects to its Kotlin store: the live environment (`LiveSplashEnvironment` in Foyer), each port implemented in Swift, and the closures they carry. The Kotlin object that holds one keeps it alive with an Objective-C retain.

The Swift object is released after the collector frees its Kotlin holder. The runtime's finalizer does the release, and the Kotlin page on [deinitializers](https://kotlinlang.org/docs/native-arc-integration.html#deinitializers) states where: on the main thread for objects passed to Kotlin on the main thread, and on a collector thread otherwise. The binary option `kotlin.native.binary.objcDisposeOnMain=false` moves every release to the collector thread. Because a Swift `deinit` may call back into Kotlin, it runs after the collection ends. A chain in which Kotlin and Swift objects alternate is therefore freed over several collections: the Kotlin page's example, Kotlin → Swift → Kotlin → Swift, takes two.

In the same test program, the environment's `deinit` ran on the main thread 0.30 s after its store's last Swift release while the program allocated, and 10.08 s after it while idle. Code in the `deinit` of a Swift object you hand to Kotlin runs that long after the screen that created the object closed.

## Why does a cycle across the boundary leak, and how do you avoid it?

ARC frees a Swift cycle only when you break it with a `weak` or `unowned` reference. The collector frees a cycle of Kotlin objects on its own. A cycle that contains objects of both kinds is never freed: ARC does not see the Kotlin references, and the collector treats the Swift-held Kotlin object as a root. The Kotlin page on [retain cycles](https://kotlinlang.org/docs/native-arc-integration.html#retain-cycles): "if at least one Objective-C object is present, the retain cycle of a whole graph of objects cannot be reclaimed".

The cycle a Duet shell can create: an object owns a Kotlin store, the store holds its environment, and the environment's delegate closure captures the owner strongly.

```swift theme={null}
final class Owner {
  var store: AnyObject?
  func handle(_ event: SplashDelegateEvent) {}
}

let owner = Owner()
// Leaks: the closure holds `owner`, which holds the store, which holds the environment and its closure.
let environment = LiveSplashEnvironment { event in owner.handle(event) }
owner.store = makeSplashStore(environment: environment, scope: mainImmediateStoreScope())
```

The fix is a weak capture in every closure that crosses into Kotlin:

```swift theme={null}
let environment = LiveSplashEnvironment { [weak owner] event in owner?.handle(event) }
```

The test program builds both owners, releases them, and allocates 4 million Kotlin objects:

| Owner | Owner `deinit` | Environment `deinit` |
| - | - | - |
| Weak capture | at the release | 0.30 s later, on the main thread |
| Strong capture | never | never |

**What the memory graph shows.** `leaks`, the command-line reader for the memory graphs Xcode exports, reports `0 leaks for 0 total leaked bytes` for the strong-capture owner, and lists the owner and its environment as live objects. The scanner cannot see the cycle: it scans the Kotlin heap as a root, so every Swift object the cycle holds is reachable from it. Find the cycle from the object that should be gone instead:

1. Close the screen, wait 10 seconds for a collection, and export the memory graph (**File ▸ Export Memory Graph** in Xcode's memory graph debugger).
2. Find an instance of the owner type that should have been freed.
3. Trace what holds it: `leaks --traceTree=<address> <file>.memgraph`. A path that runs through `Region Memory Tag 246` goes through the Kotlin heap. 246 is the tag the Kotlin/Native allocator gives its memory unless the `mmapTag` binary option changes it.

Duet's emitted code captures weakly in the closures it hands to Kotlin. `BridgedStore`'s collector task is `Task { [weak self] in … }`, and a parent shell routes a child's delegate events with `mounter.mountWelcome { [weak self] event in self?.store.send(…) }`. Keep both forms when you edit them.

## Why does a Duet shell stop its store explicitly?

Releasing a Kotlin store does not stop it. A store whose effects are still running stays reachable from them: a pending timer, for example, is held by the dispatcher that will resume it. The collector does not free such a store, and its effects keep running. A Duet shell therefore stops the Kotlin store as a step of its own. Foyer's splash builder gives the bridge a teardown closure:

```swift src-ios/Libraries/FoyerKit/Sources/SplashShell/SplashBuilder.swift theme={null}
let scope = mainImmediateStoreScope()
let store = makeSplashStore(
  environment: LiveSplashEnvironment(onDelegate: onDelegate),
  scope: scope)
let bridged = SplashKitStore(
  state: splashStateFlow(store: store),
  send: { store.send(action: $0) },
  teardown: {
    store.teardown()
    cancelStoreScope(scope: scope)
  })
```

`BridgedStore.cancel()` cancels the Swift collector task and calls that closure. The shell adopts the bridged store into its `StoreHost`, which calls `cancel()` when the shell deactivates, in reverse order of adoption. [Tutorial 2](/tutorials/duet-02-two-apps#write-the-swift-shell) writes this shell.

## Why does `BridgedStore` re-read the state after `send`?

A Duet shell must observe a reduce before `send` returns: its view state and the child mounts follow from the new state in the same call. The Kotlin store reduces on the calling thread, because its scope runs on `Dispatchers.Main.immediate`. The bridged `StateFlow` delivers to Swift through `for await`, and every delivery crosses a continuation hop, so it arrives in a later main-actor turn. `BridgedStore.send` therefore forwards the action and then reads `stateFlow.value` in the same call. On each delivery its collector task re-reads `value` and discards the delivered element, because the bridged sequence buffers and can deliver a state older than the one `send` already mirrored. [Tutorial 2](/tutorials/duet-02-two-apps#write-the-consumer-package-and-the-store-mirror) writes the mirror.

## What happens to a Kotlin exception?

A Kotlin exception that reaches Swift through a function without `@Throws` terminates the process. With `@Throws(SomeException::class)` on the Kotlin function, Swift sees a `throws` function and the exception arrives as a Swift `Error`. Foyer's boundary object annotates every exported function that can throw; [Tutorial 2](/tutorials/duet-02-two-apps#export-the-replay-boundary) shows it. The Kotlin page on [Swift/Objective-C interop](https://kotlinlang.org/docs/native-objc-interop.html#errors-and-exceptions) has the full rules.

## How is Android different?

On Android the same Kotlin modules compile to JVM bytecode and run on ART, under ART's garbage collector. The Compose shell, the store and the workers are objects in one heap, and the Android app calls the store's `send` directly, with no generated layer and no retain/release at a boundary. The rules on this page apply to the iPhone app only. [Inside a Duet Android app](/articles/duet-android-app-anatomy) walks that tree.

## How do you check your own build?

Run these against the Release framework of your app's iPhone slice. The paths are Foyer's; replace `FoyerKit` with your project's framework name.

```sh theme={null}
cd src-kmp/apple-umbrella/build/bin/iosArm64/releaseFramework
mkdir -p /tmp/kit && cp FoyerKit.framework/FoyerKit /tmp/kit/ && cd /tmp/kit
ar -t FoyerKit                       # the two objects
ar -x FoyerKit
nm -m FoyerKit.framework.o | grep -v undefined | grep -c 'kfun:'                 # Kotlin functions
nm FoyerKit.framework.o | c++filt | grep -c 'gc::mark::ConcurrentMark'           # the collector
nm -m FoyerKit.framework.o | grep -v undefined | grep -c '_OBJC_CLASS_\$_'       # Objective-C classes
nm -m FoyerKit.o | grep -v undefined | grep -c ' _\$s'                           # SKIE's Swift symbols
nm -m FoyerKit.o | grep -v undefined | grep -c 'kfun:'                           # 0: no Kotlin in SKIE's object
```

Foyer prints:

```text theme={null}
__.SYMDEF SORTED
FoyerKit.framework.o
FoyerKit.o
7361
16
559
5009
0
```

A Debug framework lists more objects: one per generated Swift file and one per cached Kotlin library. Use the Release framework for these counts.

To check for the collector in the app you ship, search the executable inside the archive:

```sh theme={null}
strings -a "Foyer.xcarchive/Products/Applications/Foyer.app/Foyer" | grep 'concurrent mark took too long'
```

## Common questions

<AccordionGroup>
  <Accordion title="Does the collector pause the main thread?" icon="pause">
    Yes, twice per collection, and briefly in the measurements on this page. On an iPhone 15 Pro scrolling a list while the main thread created 300,000 Kotlin objects per second, the pauses of a collection added up to a median of 78 µs and at most 13.7 ms, and 1 frame in 5,399 was dropped, as many as in the same scroll with no Kotlin objects. In a scripted Foyer walk on the simulator the longest was 241 µs. [What the collector costs on the main thread](#what-the-collector-costs-on-the-main-thread) has the method, the flag to measure your own app, and how recording with Instruments changed the result.
  </Accordion>

  <Accordion title="Can a SwiftUI view hold a Kotlin object in @State?" icon="eye">
    Yes. `@State` holds a strong Swift reference, so the Kotlin object stays alive while the view does, and the collector frees it on a later collection after the view goes away. For a Kotlin value such as a state snapshot that is all you need. For a Kotlin store, use a shell and a `BridgedStore`: a store in `@State` is never stopped, and [a store with running effects is never freed](#why-does-a-duet-shell-stop-its-store-explicitly).
  </Accordion>

  <Accordion title="Does SKIE add runtime cost?" icon="gauge">
    SKIE adds code size, measured above as 1.5 MB in Foyer's framework and 2.6 MB in Memory Lane's before the link, and one Swift wrapper object per value it wraps: an `onEnum(of:)` result, a `SkieSwiftStateFlow`, an iterator per `for await`. Each delivery of a bridged flow crosses a continuation hop. This page has no CPU measurement of SKIE's wrappers.
  </Accordion>

  <Accordion title="Why does the iPhone build need a JDK?" icon="java">
    The Kotlin/Native compiler runs inside Gradle, which runs on the JVM. The app scheme's build pre-action runs `scripts/assemble_kit.sh`, which runs Gradle to compile the Kotlin core into the XCFramework before Xcode links it. The JDK is a build-machine requirement; the app contains no JVM.
  </Accordion>

  <Accordion title="Can an iPhone app use Duet without the Kotlin runtime?" icon="swift">
    Yes. On the Swift flavor, each feature's reducer is Swift and the iPhone app links no Kotlin/Native framework, so none of this page applies to it. [The Duet overview](/articles/duet#the-two-duet-templates) compares the two flavors. [Duet Twins](/articles/duet-twins), in development, pairs a Swift core in the iPhone app with a Kotlin core in the Android app.
  </Accordion>
</AccordionGroup>

## Sources and further reading

* [Kotlin/Native memory management](https://kotlinlang.org/docs/native-memory-manager.html): the collector, its triggers, the `gc` and `-Xruntime-logs` options and the safepoint signposts
* [Integration with Swift/Objective-C ARC](https://kotlinlang.org/docs/native-arc-integration.html): reclamation timing, main-thread deinitialization, `objcDisposeOnMain` and retain cycles
* [Interoperability with Swift/Objective-C](https://kotlinlang.org/docs/native-objc-interop.html): how Kotlin declarations map to Objective-C, and `@Throws`
* [SKIE: Flows](https://skie.touchlab.co/features/flows), [Sealed classes](https://skie.touchlab.co/features/sealed) and [Suspend functions](https://skie.touchlab.co/features/suspend): the Swift layer SKIE generates
* [Gathering information about memory use](https://developer.apple.com/documentation/xcode/gathering-information-about-memory-use): Xcode's memory graph debugger and `.memgraph` files
* [Understanding hitches in your app](https://developer.apple.com/documentation/xcode/understanding-hitches-in-your-app): how Apple defines a hitch and measures it with Instruments
* [Kotlin/Native GC scheduler, `MutatorAssists.hpp`](https://github.com/JetBrains/kotlin/blob/v2.4.10/kotlin-native/runtime/src/gcScheduler/common/cpp/MutatorAssists.hpp): why threads running Kotlin code wait for a running collection when the heap passes its target
* [The duet-tutorials repository](https://github.com/modaal-agent/duet-tutorials): `tutorial9-complete`, the Foyer tree measured on this page

## Read next

<CardGroup cols={2}>
  <Card title="Tutorial 2: One Behavior, Two Apps" icon="2" href="/tutorials/duet-02-two-apps">
    Build the framework this page measures, the `BridgedStore` mirror, and the shell that stops the Kotlin store.
  </Card>

  <Card title="Duet vs Compose Multiplatform" icon="clone" href="/articles/duet-vs-compose-multiplatform">
    Both stacks run the same Kotlin/Native runtime on iOS; they differ in who draws the screen.
  </Card>

  <Card title="Inside a Duet Android app" icon="folder-tree" href="/articles/duet-android-app-anatomy">
    The same core on Android, where it shares one heap with the Compose shell.
  </Card>

  <Card title="Duet glossary" icon="book" href="/articles/duet-glossary">
    Shell, store, host, mount and the other terms on this page.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.