Skip to content

[CoreCLR/NativeAOT] Use a no-GC region during Android startup - #12782

Open
simonrozsival wants to merge 9 commits into
mainfrom
simonrozsival-coreclr-startup-no-gc
Open

simonrozsival wants to merge 9 commits into
mainfrom
simonrozsival-coreclr-startup-no-gc

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Sep 14, 2026

Copy link
Copy Markdown
Member

Rationale

CoreCLR and NativeAOT Android startup can incur managed GC work before the first frame is fully drawn. Reserve a short startup allocation budget so this work is deferred until the app reports that startup is complete.

Implementation

  • Start a no-GC region at the beginning of managed runtime initialization for CoreCLR and NativeAOT with a 24 MiB budget and disallowFullBlockingGC: true.
  • End the region once after the first managed Activity.ReportFullyDrawn() completes.
  • Preserve the existing public, virtual, and JNI registration contract by removing only the generated method and providing the equivalent binding in the existing Activity partial class.
  • Leave Mono startup unchanged.
  • Bound apps that never call ReportFullyDrawn() with a 10-second one-shot timer.
  • Treat start failure, allocation exhaustion, and an induced collection as expected best-effort outcomes.

Benchmark evidence

Samsung A16, five matched CoreCLR trimmable launches:

Metric Baseline Startup no-GC Change
Fully drawn median 1,782 ms 1,705 ms -77 ms (-4.3%)
Scheduler-observed startup wall 1,859.7 ms 1,774.6 ms -4.6%
App-process CPU 1,907.8 ms 1,835.6 ms -3.8%
Main-thread CPU 1,674.7 ms 1,642.0 ms -2.0%
Managed pause before fully drawn 12.7 ms 0 ms -12.7 ms
GC/finalizer CPU 9.4 ms 1.4 ms -8.0 ms

The startup request itself performed a near-empty preparation collection at about 120 KiB allocated. The Android GC bridge's explicit ART collection moved until after fully drawn.

Risks and fallback

The region is process-wide by design and intentionally short-lived. It ends on the first managed fully-drawn report or after 10 seconds. The runtime automatically leaves the region if the 24 MiB budget is exhausted or a collection is induced. All start and end paths are best effort and one-shot.

Validation

  • Mono.Android API 37 build passed.
  • Microsoft.Android.Runtime.NativeAOT build passed.
  • A .NET 11 NativeAOT-published probe successfully started and ended a 24 MiB no-GC region with full blocking GC disallowed.
  • API 36 binding generation passed and confirmed the generated ReportFullyDrawn method is removed in favor of the manual binding.
  • Built assembly inspection confirms Activity.ReportFullyDrawn() remains public, virtual, supported from Android 19, and registered as reportFullyDrawn, ()V, GetReportFullyDrawnHandler.
  • The in-repo device test project could not be run locally because the local repo SDK is absent and the installed Android workload requires JDK 21; this machine only has JDK 23.

Start a bounded 24 MiB no-GC region during CoreCLR initialization and end it after Activity.ReportFullyDrawn. Preserve the generated binding contract manually and use a timeout plus collection tracking for safe fallback.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 14, 2026 11:11
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12782

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved moderate API, dispatch, and no-GC ownership issues remain.

Review tier: Lite
Findings: None

What changed in this PR

Adds a CoreCLR-only Android startup no-GC region, ending after ReportFullyDrawn() or a timeout.

Changes:

  • Adds bounded no-GC lifecycle management and startup initialization.
  • Provides a manual ReportFullyDrawn() JNI binding.
  • Updates metadata, project files, and lifecycle tests.
File Summary
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Mono.Android.NET-Tests.csproj Registers the new test coverage.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Android.Runtime/​StartupNoGCRegionTest.cs Tests lifecycle, timeout, collection, and registration behavior.
src/​Mono.Android/​Mono.Android.csproj Includes the startup region implementation.
src/​Mono.Android/​metadata Removes the generated ReportFullyDrawn binding.
src/​Mono.Android/​Android.Runtime/​StartupNoGCRegion.cs Implements bounded startup region management; a concurrency race remains.
src/​Mono.Android/​Android.Runtime/​JNIEnvInit.cs Starts the region during CoreCLR initialization.
src/​Mono.Android/​Android.App/​Activity.cs Adds the manual binding and end hook; API metadata, virtual dispatch, and integration coverage issues remain.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Reject

Findings: 2 errors, 0 warnings, 0 suggestions.

The measured startup improvement is promising, and the manual Activity.ReportFullyDrawn() binding preserves the public/JNI shape. However, the implementation unconditionally takes ownership of the process-wide no-GC facility and its collection-count heuristic cannot guarantee that it will not end a customer replacement region. Both compatibility hazards need resolution before merge.

CI: Azure DevOps build 1595487 was still in progress when reviewed. The completed Android Tools/Java.Interop lanes and CLA check were green; no failures were reported yet.

Generated by Android PR Reviewer for #12782 · copilot · gpt56 · 186.2 AIC · ⌖ 11.3 AIC · ⊞ 21K
Comment /review to run again

Comment thread src/Mono.Android/Android.Runtime/StartupNoGCRegion.cs Outdated
Comment thread src/Mono.Android/Android.Runtime/JNIEnvInit.cs Outdated
simonrozsival and others added 2 commits September 14, 2026 14:32
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [CoreCLR] Use a no-GC region during Android startup [CoreCLR/NativeAOT] Use a no-GC region during Android startup Sep 14, 2026
simonrozsival and others added 6 commits September 14, 2026 15:43
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12782

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Reject

Findings: 2 errors, 0 warnings, 0 suggestions.

The startup measurements are encouraging, and the manual Activity.ReportFullyDrawn() binding preserves the existing public/JNI shape. However, the current revision reintroduces two process-wide GC ownership hazards: it provides no compatibility escape hatch for applications that manage their own no-GC region, and a delayed completion can terminate a replacement region after ownership has been lost.

CI: Azure DevOps build 1595775 is still in progress. At review time, 17 checks had passed, 25 were running, 1 was queued, and dotnet-android (MAUI Tests MAUI Integration) was failing. The available integration could not retrieve the Azure failure details, so I could not determine whether that lane is related to this change.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12782 · copilot · gpt56 · 140.1 AIC · ⌖ 8.65 AIC · ⊞ 21.8K
Comment /review to run again

timer?.Dispose ();

try {
GC.EndNoGCRegion ();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ❌ Resource managementGC.EndNoGCRegion() has no ownership token. If this region is ended implicitly by budget exhaustion or an induced collection, application code can start a replacement region before this timeout/ReportFullyDrawn() path runs; this call will then terminate the application-owned region. The current lock only serializes this helper and cannot protect against external GC callers. Please use a design that cannot end a replacement region (or add runtime support for conditional ownership), and restore deterministic coverage for this interleaving.

Rule: Thread safety of shared state

throw new NotSupportedException ("Internal error: exactly one of RuntimeFeature.IsMonoRuntime or RuntimeFeature.IsCoreClrRuntime must be enabled.");
}

StartupNoGCRegion.Start ();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ❌ API design — This again unconditionally occupies the process-wide no-GC facility before RunStartupHooksIfNeeded(). A customer startup hook or early library that uses GC.TryStartNoGCRegion() will now fail until this region ends, and the opt-out introduced in the earlier revision has been removed. Please restore a documented runtime/MSBuild escape hatch (or make the optimization opt-in) so existing applications can preserve their prior GC behavior.

Rule: Behavior-safe defaults

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants