Skip to content

[NativeAOT] Prototype guarded incremental compilation - #132962

Draft
awakecoding wants to merge 2 commits into
dotnet:mainfrom
awakecoding:copilot/nativeaot-incremental-prototype
Draft

[NativeAOT] Prototype guarded incremental compilation#132962
awakecoding wants to merge 2 commits into
dotnet:mainfrom
awakecoding:copilot/nativeaot-incremental-prototype

Conversation

@awakecoding

@awakecoding awakecoding commented Aug 31, 2026

Copy link
Copy Markdown

Summary

Adds a disabled-by-default, internal NativeAOT incremental-compilation prototype for Windows x64 COFF. A single ILC process retains its completed dependency graph, recompiles a prevalidated finite set of changed method bodies, and creates each updated object by patching a copy of the immutable baseline object.

The prototype adds no public API. Incremental rejection is explicit: ILC prints ILC_INCREMENTAL_REJECTED, exits with code 85, removes outputs it created, and requires the caller to start a fresh clean compilation. Ordinary compiler failures are not classified as clean-fallback requests.

Tracking issue: #132977

Implementation

  • Captures the effective IL provider chain, including generated P/Invoke stubs.
  • Validates all update assemblies before baseline object emission.
  • Resets the mutable MethodCodeNode code, GC, EH, debug, local, and dependency state required for recompilation.
  • Recompiles the union of methods changed by the current and previous updates, so sequential edits and reverts always derive from the original baseline.
  • Verifies ordered static and conditional dependencies, reasons, marked state, GC info, frame/unwind data, EH state, debug state, symbols, relocations/addends, alignment, COMDAT state, and object locations after code generation.
  • Records selected COFF fragment locations per compilation and binds the baseline assembly, configuration, and object with SHA-256.
  • Copies from the same verified baseline handle, validates every non-relocation byte, stages unique same-directory files, flushes them to disk, and publishes without overwrite.
  • Poisons retained state after any post-mutation failure so a failed attempt cannot be reused.

Supported envelope

Incremental compilation is accepted only for:

  • A Windows host targeting Windows x64 NativeAOT COFF, single-file compilation, and one primary input.
  • OptimizationMode.None with exactly one compiler thread.
  • Scanner, preinitialization, custom inlining, method folding, native debug info, profile/order layout, custom JIT configuration, CFG, resilience, dehydration, dependency logs, exports, maps, metadata/SourceLink output, reachability modes, and other side outputs disabled.
  • Baseline and updated PE files with equal length, MVID, metadata method count, and complete non-body content after masking only timestamp, checksum, debug-directory, strong-name payload, and encoded method-body ranges.
  • Method bodies with unchanged encoded size, max stack, init-locals, local signature, and EH shape.
  • Non-constructor, non-generic leaf methods with exactly one marked, non-canonical, non-unboxing, non-foldable MethodCodeNode, no conditional dependencies or EH, and overlayable ECMA IL.
  • Identical allowed opcode streams where only explicit integer or floating-point constant operands change.
  • Selected non-COMDAT COFF fragments whose size, alignment, symbols, relocations, addends, GC/frame/EH/debug state, location, bounds, and overlap checks remain valid.

All other cases request a clean compilation.

RDM build-performance impact

Measured on one eligible edit to reachable Program.BuildModuleViewIndex in the RDM RdmNativeAotFast=true workload:

RDM step Clean path Retained incremental path Impact
ILC object generation/update 640.395 s 0.291837 s 2,194.36x faster; 99.9544% less wall time
Native link 36.55 s 36.55 s unchanged
ILC + native link developer loop 676.945 s (11m 16.9s) 36.842 s 18.37x faster; 94.56% less wall time
Time saved per eligible edit/link iteration 640.103 s (10m 40.1s) derived from the rows above

The 3,744,339,247-byte incremental object exactly matched the clean object with SHA-256 B3140045782498DC4A06F712C2DAA329B732D6340DFCD3D80AD4181E17844206. The update reused 13,455,307 of 13,455,308 object nodes, patched one byte, and allocated 1,568,736 managed bytes.

These are measured component timings. A separate clean RDM publish measured 951.9 s (15m 51.9s), but a comparable complete incremental dotnet publish was not timed. The defensible practical result is the ~36.8-second edit-and-link loop instead of ~10–11 minutes, not a 36.8-second full build.

The first request still pays for the clean compilation and retains roughly 34–36 GiB. The result applies only to edits that pass the narrow safety gate.

Validation

  • build.cmd clr+libs+host baseline: succeeded with 0 warnings/errors.
  • build.cmd clr.aot+libs -rc Release -lc Release: succeeded with 0 warnings/errors.
  • ILCompiler.Compiler.Tests Release: 80 passed, 0 failed, 0 skipped.
  • Focused IncrementalCompilationTests: 58 passed, 0 failed, 0 skipped.
  • Dedicated priority-0 Windows-x64 NativeAOT incremental smoke test: runs automatically during BuildNativeAot and succeeded with 0 warnings/errors.
    • Incremental edited object and independent clean edited object: 0BE481A1B058F826D4FCD0E013DEFC544BF4382E16CE8C5549EF94AE1F73666F.
    • Reverted object and baseline object: 0CB70EB6CAA77ABC4C6F120AE64C1AF3F6370A37AEC4FD54FBF8A96179E44497.
    • Explicit exit-85 rejection logging was validated under Windows PowerShell; CFG legs skip the unsupported differential path.
    • Build-only comparison objects are written under the intermediate tree and excluded from Helix payloads.

Limitations

Normal compiler-driven source edits commonly change the MVID and therefore request a clean compilation. Productization would require a supported command/API contract, complete cross-process content keys for references/resources/toolchain/JIT/environment, request isolation, eviction and crash recovery, broader invalidation for optimized/inlined/preinitialized/reflection/generic/global facts, discovery of newly dirty nodes, validation or regeneration of linker-affecting side outputs, and compile-time-checked internal seams across the compiler assemblies.

Note

This pull request description was generated with GitHub Copilot.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Aug 31, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

@jkotas

jkotas commented Aug 31, 2026

Copy link
Copy Markdown
Member

What are you trying to achieve with this?

It would be better to start with an issue discussing the experience you would like to see.

@jkoritzinsky

Copy link
Copy Markdown
Member

I feel like the existing "multifile" mode that we haven't productized is a better approach here (one object file per assembly, would allow incremental ILC execution at the assembly boundary).

@awakecoding

Copy link
Copy Markdown
Author

What are you trying to achieve with this?

It would be better to start with an issue discussing the experience you would like to see.

I am trying to improve incremental build performance for NativeAOT on very large projects like Remote Desktop Manager. It currently takes a good 15 minutes to make a change and rebuild, and memory peaks over 32GB in ILC. I've been told you're supposed to just make managed builds with the warnings for NativeAOT to figure out what to fix for NativeAOT and then just wait on the NativeAOT build, trusting that it works, but this prevents actually trying out the NativeAOT build and repeatedly iterate on it. I know my project (RDM) is huge, but that doesn't mean we can't optimize the build tooling to introduce incremental builds

@awakecoding

Copy link
Copy Markdown
Author

I feel like the existing "multifile" mode that we haven't productized is a better approach here (one object file per assembly, would allow incremental ILC execution at the assembly boundary).

Where is this documented?

@jkoritzinsky

Copy link
Copy Markdown
Member

You can set the IlcMultiModule property to true to try it out. Once again, no guarantees, this is unsupported, etc.

I also don't know if we actually have the MSBuild targets set up correctly to make this run in an incremental manner so this may need more work for that front.

@awakecoding

Copy link
Copy Markdown
Author

@jkotas @jkoritzinsky I have created an issue here for NativeAOT incremental compilation support: #132977

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-NativeAOT-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants