| |

Dart 94 🎯 Internal Architecture: Deep Dive into Dart AOT Snapshot Binary Format

⏱️ Estimated read: 14 min | |

The Dart AOT snapshot is not a traditional executable. It is a serialized representation of the Dart VM heap graph, written to disk by the AOT compiler and mapped into memory at startup by the precompiled runtime. The format is deliberately low-level and optimized for fast startup: it is essentially a list of objects to create and instructions for how to connect them together . The binary contains two distinct parts—a data section holding strings, constants, class metadata, and other heap objects, and a code section holding the machine code generated from Dart source. The code section is the part that makes AOT snapshots different from ordinary snapshots: it does not require deserialization. It is laid out in a way that allows it to become part of the heap directly after being mapped into memory .

This chapter covers the snapshot’s two-part structure, the clustered serialization format that organizes objects by type, the instructions table that maps program counters to stack maps and code objects, and the loader’s role in reconstructing the heap without parsing or compiling.

Key point: The AOT snapshot must contain executable code for every function that could be invoked, because the precompiled runtime has no JIT compiler to generate code at runtime. This constraint is what forces the AOT compiler to perform global static analysis before writing the snapshot, and it is what distinguishes the AOT binary format from the JIT snapshot format.


Why the AOT snapshot format exists

The no-JIT constraint problem. On platforms where JIT compilation is impossible—iOS being the canonical example—the VM cannot generate machine code at runtime. The snapshot must therefore contain all the code the application will ever need. This is the inverse of the JIT model, where the runtime compiles functions lazily as they become hot. The AOT snapshot format exists to package that precompiled code along with the heap state required to execute it .

The startup time problem. Even on platforms where JIT is available, the warm-up cost of parsing, compiling, and optimizing can be significant for short-lived command-line tools. An AOT snapshot bypasses this entirely: the code is already compiled, and the heap structures are already serialized. The VM maps the snapshot into memory and begins executing almost immediately . The format is designed for this: the code section requires no deserialization, and the data section is organized into clusters that can be read sequentially.

The code deduplication problem. Bitwise-equivalent objects can be shared in the snapshot. Two functions with identical bodies may end up using the same machine code object. This reduces snapshot size, but it also means that attributing every byte of the snapshot to a particular source location is not always possible . The format accommodates deduplication through reference-based serialization: objects are written once and referenced by index elsewhere.

The stack walking problem. The garbage collector and the exception unwinder need to map a program counter value to the stack map that describes the frame’s pointer layout. In earlier snapshot versions, this mapping was constructed at load time. A later change baked a binary search table into the read-only data section of the snapshot, mapping PC ranges to stack maps and code objects. This reduced load time for large applications by eliminating the need to construct the table on every startup .


a. The Data Section and Clustered Serialization

The data section of an AOT snapshot holds the non-code heap objects: strings, constant instances, class metadata, library and function objects, and runtime metadata . These objects are organized into clusters, a serialization format that groups objects of the same type together. The clustered format is similar to the techniques described in the Parcels and Fuel papers, and it allows the loader to read objects of a given type sequentially rather than navigating a general-purpose object graph .

Each cluster has a reader and a writer that understand the specific layout of the objects it contains. A OneByteString cluster, for example, writes a header followed by the string’s payload. A Type cluster writes the type’s class ID, its flags word, and its parameter types. The layout of these objects is not stable across Dart versions: a change to the UntaggedAbstractType layout in Dart 2.19.0 changed the position of the type_class_id within the packed flags word, and a later change in Dart 3.5.0 shifted it again . The snapshot format is therefore version-specific, and a snapshot produced by one version of the SDK may not load in another.

The data section is written as a sequence of clusters. Each cluster begins with a header identifying its kind and its length, followed by the serialized objects. The loader reads the clusters in order, reconstructing the heap graph as it goes. References between objects are resolved through an index-based mechanism: when an object references another object, the writer records the target’s index, and the reader resolves the index to the reconstructed object.


b. The Code Section and the Instructions Table

The code section of the AOT snapshot contains the machine code generated by the AOT compiler. Unlike the data section, it does not require deserialization. The code is laid out in a way that allows it to be mapped directly into memory and used as-is . This is the key performance advantage of the AOT format: the code is already in the form the CPU will execute.

The instructions table is the metadata that makes the code section usable. It maps program counter ranges to the corresponding stack maps and, where still present, the Code objects that own the instructions. In a change that reduced Flutter application startup time on a slow Android device from roughly 223 ms to 178 ms, the binary search table was baked into the read-only data section of the snapshot instead of being constructed at load time. The change also packed the CompressedStackMaps next to the table itself, allowing them to be addressed via offsets rather than pointers .

The instructions table has a specific binary layout. Its data header contains a canonical stack map offset, a length, and a first_entry_with_code index, followed by padding. The entries themselves are pairs of 32-bit values: a PC offset and a stack map offset. The stack map offset is relative to the data struct, matching the SDK’s StackMapAt implementation . This layout is what allows the runtime to find the stack map for a given PC without a hash lookup or a pointer chase.


c. The Loader and the Precompiled Runtime

The AOT snapshot is loaded by the precompiled runtime, a variant of the Dart VM that excludes the JIT compiler and dynamic code loading facilities . The loader’s job is to map the snapshot into memory, reconstruct the heap graph from the data section, and make the code section available for execution. Because the code section requires no deserialization, the loader’s work is concentrated on the data section.

The loading process is not a full deserialization in the traditional sense. The snapshot is a “list of objects to create and instructions on how to connect them together” . The loader reads the cluster data, allocates the objects, and patches the references. The code section is mapped into an executable region of memory, and the instructions table is used to associate code addresses with their metadata.

The distinction between the data and code sections matters for the loader’s design. The data section is mutable after loading—the application will modify heap objects during execution. The code section is immutable: it is machine code, and it is mapped as read-only and executable. This separation is what allows the code section to be shared between isolates in the same isolate group, and it is what makes the AOT snapshot suitable for platforms that enforce W^X memory protections.


Complete Example Session

// ============================================
// PART 1: THE AOT SNAPSHOT OUTPUT
// ============================================
// Produced by the AOT compiler
// dart compile aot-snapshot bin/myapp.dart
// Output: myapp.aot (architecture-specific)
// Run with: dartaotruntime myapp.aot


// ============================================
// PART 2: DATA SECTION CONTENTS
// ============================================
// - Strings (OneByteString, TwoByteString)
// - Constant instances
// - Class metadata (name, superclass, vtable)
// - Library and function objects
// - Type objects
// Organized into clusters by object type


// ============================================
// PART 3: CODE SECTION CONTENTS
// ============================================
// - Machine code for every reachable function
// - Stubs for runtime calls
// - Instructions table (PC → stack map mapping)
// No deserialization required at load time
// Mapped directly into executable memory


// ============================================
// PART 4: CLUSTERED SERIALIZATION
// ============================================
// Objects of the same type are written together
// Each cluster has a reader and writer
// References between objects are index-based
// Layout is version-specific (e.g., Type layout changed in 2.19.0 and 3.5.0)


// ============================================
// PART 5: INSTRUCTIONS TABLE LAYOUT
// ============================================
// Header: {canon_offset u32, length u32, first_entry_with_code u32, padding u32}
// Entries: {pc_offset u32, stack_map_offset u32}
// Stack map offsets are relative to the table data
// Baked into read-only data section at compile time


// ============================================
// PART 6: THE LOADER'S WORK
// ============================================
// 1. Map snapshot into memory
// 2. Read data clusters sequentially
// 3. Allocate objects, patch references
// 4. Map code section as executable
// 5. Use instructions table for PC → stack map lookup


// ============================================
// PART 7: PRECOMPILED RUNTIME
// ============================================
// A variant of the Dart VM without JIT
// No dynamic code loading
// Cannot compile new functions at runtime
// Relies entirely on the code in the snapshot


// ============================================
// PART 8: NO DESERIALIZATION FOR CODE
// ============================================
// The code section is not "read" in the traditional sense
// It is mapped into memory and used directly
// This is the source of AOT's fast startup


// ============================================
// PART 9: DEDUPLICATION
// ============================================
// Identical machine code is written once
// Multiple Code objects may reference the same Instructions
// This reduces size but complicates byte attribution


// ============================================
// PART 10: VERSION SENSITIVITY
// ============================================
// Snapshot layout changes between Dart versions
// Type flags word shift changed in 3.5.0
// A snapshot from one SDK version may not load in another

The ten parts trace the snapshot’s two sections, the clustered serialization format, the instructions table layout, and the loader’s role. The critical distinction is between the data section, which must be deserialized into heap objects, and the code section, which is mapped directly and used without deserialization.


Quick Reference

Snapshot Sections

SectionContentsDeserialization
DataStrings, constants, metadata, typesYes, cluster by cluster
CodeMachine code, instructions tableNo, mapped directly

Clustered Serialization

AspectDetail
GroupingObjects of same type written together
ReferencesIndex-based, resolved at load
Layout stabilityVersion-specific; changes between SDK releases
Example changeType class ID shift moved from 4 to 3 in Dart 3.5.0

Instructions Table

FieldWidthPurpose
canon_offset32 bitsCanonical stack map offset
length32 bitsNumber of entries
first_entry_with_code32 bitsIndex of first entry with Code object
Entry PC offset32 bitsProgram counter range
Entry stack map offset32 bitsRelative to table data

AOT vs. JIT Snapshot

AspectAOT SnapshotJIT Snapshot
Code includedYes, all reachable functionsYes, compiled during training run
JIT available at runtimeNoYes
Startup speedFastestFaster than source, slower than AOT
Platform restrictionNone (works where JIT is banned)Requires JIT support

Best Practices

✅ Do This:

// Use AOT snapshots for production deployments
dart compile aot-snapshot bin/myapp.dart                                // ✅
// Run AOT snapshots with the precompiled runtime
dartaotruntime bin/myapp.aot                                           // ✅
// Understand that code section is mapped, not deserialized
// Startup is faster because of this                                     // ✅

❌ Don’t Do This:

// Attempt to load an AOT snapshot in a JIT-enabled VM
// The precompiled runtime is required                                     // ❌
// Assume snapshot layout is stable across SDK versions
// Type layout changed in 2.19.0 and 3.5.0                               // ❌
// Expect AOT to match JIT peak performance
// AOT has no runtime profiling; JIT can optimize hot paths               // ❌

Common Pitfalls

PitfallWhy It HappensFix
Snapshot fails to loadSDK version mismatchRecompile with matching SDK
Larger than expected sizeCode deduplication not appliedCheck compiler flags; deduplication is automatic
Slower than expected startupInstructions table constructed at loadUse recent SDK; table baked into rodata
Missing function at runtimeTFA failed to include reachable functionReview entry points; ensure all paths are reachable

Real-World Examples

1. Compile AOT Snapshot

dart compile aot-snapshot bin/myapp.dart

2. Run AOT Snapshot

dartaotruntime bin/myapp.aot

3. Data Section Cluster

Cluster header + serialized objects of one type
Read sequentially by the loader

4. Code Section Mapping

Machine code mapped directly into executable memory
No deserialization

5. Instructions Table Lookup

PC offset → binary search → stack map offset

6. Type Layout Version Change

Dart 3.5.0: TypeClassIDShift changed from 4 to 3

7. Deduplicated Code

Two identical functions → one Instructions object

8. Precompiled Runtime

No JIT, no dynamic code loading
Relies entirely on snapshot code

9. Stack Map Resolution

StackMapAt(offset) = table_data + offset

10. Snapshot Size Impact

Instructions table baking reduced SO size by ~1.5%

Visual

Snapshot Binary Layout

┌──────────────────────────────────────────────────────────────┐
│  AOT SNAPSHOT (.aot)                                         │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ DATA SECTION                                            ││
│  │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐        ││
│  │ │ String  │ │ Type    │ │ Class   │ │ Function│        ││
│  │ │ cluster │ │ cluster │ │ cluster │ │ cluster │        ││
│  │ └─────────┘ └─────────┘ └─────────┘ └─────────┘        ││
│  │                                                         ││
│  │ Requires deserialization at load time                   ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ CODE SECTION                                            ││
│  │ ┌─────────────────────────────────────────────────────┐││
│  │ │ Machine code (functions, stubs)                     │││
│  │ ├─────────────────────────────────────────────────────┤││
│  │ │ Instructions table (PC → stack map)                 │││
│  │ └─────────────────────────────────────────────────────┘││
│  │                                                         ││
│  │ Mapped directly, no deserialization                     ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  Data is mutable after load.                                 │
│  Code is read-only and executable.                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Clustered Serialization Flow

┌──────────────────────────────────────────────────────────────┐
│  COMPILER                              LOADER                │
│                                                              │
│  ┌─────────────────┐                  ┌─────────────────┐   │
│  │ Group objects   │                  │ Read cluster    │   │
│  │ by type         │                  │ header          │   │
│  └────────┬────────┘                  └────────┬────────┘   │
│           ▼                                    ▼            │
│  ┌─────────────────┐                  ┌─────────────────┐   │
│  │ Write cluster   │                  │ Allocate objects│   │
│  │ header + objects│                  │ sequentially    │   │
│  └────────┬────────┘                  └────────┬────────┘   │
│           ▼                                    ▼            │
│  ┌─────────────────┐                  ┌─────────────────┐   │
│  │ Record refs as  │                  │ Resolve refs    │   │
│  │ indices         │                  │ by index        │   │
│  └────────┬────────┘                  └────────┬────────┘   │
│           ▼                                    ▼            │
│  ┌─────────────────┐                  ┌─────────────────┐   │
│  │ Next cluster    │                  │ Next cluster    │   │
│  └─────────────────┘                  └─────────────────┘   │
│                                                              │
│  Objects of the same type are contiguous in the binary.      │
│  The loader reads them in order.                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Instructions Table Structure

┌──────────────────────────────────────────────────────────────┐
│  INSTRUCTIONS TABLE DATA                                     │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Header:                                                 ││
│  │   canon_offset (u32)                                    ││
│  │   length (u32)                                          ││
│  │   first_entry_with_code (u32)                           ││
│  │   padding (u32)                                         ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Entry 0: {pc_offset, stack_map_offset}                  ││
│  │ Entry 1: {pc_offset, stack_map_offset}                  ││
│  │ Entry 2: {pc_offset, stack_map_offset}                  ││
│  │ ...                                                     ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  Stack map offset is relative to table data.                 │
│  Binary search on pc_offset finds the stack map.             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

AOT vs JIT Snapshot

┌──────────────────────────────────────────────────────────────┐
│  AOT SNAPSHOT                    JIT SNAPSHOT                │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ All reachable   │            │ Code compiled   │         │
│  │ functions       │            │ during training │         │
│  │ precompiled     │            │ run             │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ No JIT at       │            │ JIT still       │         │
│  │ runtime         │            │ available       │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Fastest startup │            │ Faster startup  │         │
│  │                 │            │ than source     │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Cannot adapt to │            │ Can recompile   │         │
│  │ runtime profile │            │ hot functions   │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  AOT trades peak performance for startup speed and           │
│  platform compatibility.                                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
AOT snapshotSerialized VM heap graph + machine code
Data sectionStrings, constants, metadata; requires deserialization
Code sectionMachine code, instructions table; mapped directly
Serialization formatClustered; objects grouped by type
Instructions tablePC offset → stack map offset; baked into rodata
Precompiled runtimeVM variant without JIT or dynamic code loading
DeduplicationIdentical code written once; shared by reference
Version sensitivityLayout changes between SDK releases

Key takeaways:

  • The AOT snapshot is a heap graph plus precompiled code. The data section holds the objects the application needs, organized into clusters by type. The code section holds the machine code for every reachable function. The loader reconstructs the heap from the data section and maps the code section directly into executable memory .
  • The code section requires no deserialization. This is the source of AOT’s fast startup. The machine code is already in the form the CPU executes; the loader maps it and the instructions table provides the metadata needed for stack walking .
  • The instructions table is baked into read-only data. Earlier versions constructed the PC-to-stack-map mapping at load time. A later change wrote a binary search table into the snapshot’s rodata section, reducing startup time for large applications and reducing the overall shared object size .
  • Clustered serialization groups objects by type. Strings are written with strings, types with types, classes with classes. The loader reads them sequentially. The layout of individual objects is version-specific; a change to the Type layout in Dart 3.5.0 shifted the class ID within the flags word .
  • The precompiled runtime is a stripped-down VM. It has no JIT compiler and no dynamic code loading. It relies entirely on the code in the snapshot. This is what makes AOT suitable for platforms where JIT is banned, and it is why the AOT compiler must prove that every reachable function is included .

Remember: The AOT snapshot format is the binary contract between the AOT compiler and the precompiled runtime. The compiler’s job is to determine which functions are reachable, compile them, and serialize the heap state required to execute them. The runtime’s job is to map the snapshot, reconstruct the heap graph from the data clusters, and use the code section and instructions table to execute. The format is not a general-purpose executable; it is a VM-specific artifact that depends on the exact version of the SDK that produced it. Understanding its two-part structure—data to be deserialized and code to be mapped—explains why AOT startup is fast, why snapshot size is dominated by code, and why a snapshot from one SDK version cannot be loaded by another.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!