Repository navigation
AOT (7/7): canary in CI with a warning baseline, buildTransitive .targets for automatic AOT/trim opt-in, Native AOT docs - #8608
Merged
Aaronontheweb merged 4 commits intoSep 25, 2026
Conversation
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
from
September 23, 2026 02:46
8881230 to
566bc29
Compare
Aaronontheweb
changed the base branch from
feature/aot-io-dns-and-buffer-pool-not-configurable
to
aot/io-dns-and-buffer-pool-not-configurable
September 23, 2026 02:46
Aaronontheweb
added this pull request to stack #8607
September 23, 2026 02:57
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
from
September 23, 2026 14:19
8a80090 to
efa85fa
Compare
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
from
September 23, 2026 14:47
efa85fa to
0c4552f
Compare
This was referenced Sep 23, 2026
Aaronontheweb
added a commit
that referenced
this pull request
Sep 23, 2026
…in tables for formatter/stdout logger/scheduler (#8601) * Add the Akka.DynamicTypeLoading feature switch Adds internal AkkaFeatures.IsDynamicTypeLoadingSupported behind the Akka.DynamicTypeLoading feature switch, which defaults to on. An ordinary JIT application therefore behaves exactly as it does today - core keeps resolving HOCON type names through Type.GetType. A trimmed or Native AOT application turns it off in its project file: <RuntimeHostConfigurationOption Include="Akka.DynamicTypeLoading" Value="false" Trim="true" /> With Trim="true" ILLink substitutes the property with the constant false, so the reflection branches guarded by it become unreachable and are removed. Turning the switch off is also useful on the JIT, where it acts as a strict mode: a HOCON type name that would not survive trimming fails at startup instead of at publish time. Both attribute families are needed and they do different jobs. [FeatureSwitchDefinition] is what lets ILLink/ILC substitute the getter and delete the dead branches. [FeatureGuard] is what stops the Roslyn trim/AOT analyzer reporting IL2026/IL3050 at the guarded call sites - measured with a probe: with only the switch definition, a call site inside if (IsDynamicTypeLoadingSupported) still warns IL2026. The cost of [FeatureGuard] is IL4000: the analyzer only accepts a constant false or another recognized check such as RuntimeFeature.IsDynamicCodeSupported, so it cannot verify a guard whose value comes from AppContext and says so. It is a complaint about what the analyzer can prove, not about whether the guard works - ILC does substitute the getter and does drop the branches, and the publish carries no IL2026. Akka.dll does not enable EnableTrimAnalyzer/EnableAotAnalyzer, so nothing reports IL4000 today; the trade-off is written up on the property for whoever turns them on. AkkaFeatures.NotBuiltIn builds the ConfigurationException message every call site shares, so the wording cannot drift as more sites are converted. The doc also records the conventions a call site has to follow: two keys per type in a BuiltIn* table and StripAssemblyIdentity on the configured value, never a third versioned key; and no Trim() of the value (Akka's HOCON parser already returns every string trimmed) or of the keys (a trimmed key can collide with one already in the dictionary). Brings forward the Akka.Util.TypeExtensions hunk the table lookups need: one internal StripAssemblyIdentity that removes Version, Culture, PublicKeyToken, ProcessorArchitecture, Retargetable and ContentType from a type name, which TypeQualifiedName now also calls. It replaces a private RegexOptions.Compiled regex - compiled regexes emit IL at runtime, which Native AOT cannot do, so under AOT they silently degraded to the interpreter - with a [GeneratedRegex]. Internal, so no public API change. TypeQualifiedName() output is a wire manifest, so that rewrite was measured rather than eyeballed: the new stripping was compared against the old regex over 5,544 real AssemblyQualifiedNames - every type in Akka.dll, System.Private.CoreLib, System.Linq and Akka.Tests, plus hand-picked closed generics, jagged arrays and nested generics - and the output was byte-identical in every case. The property reads AppContext on every call rather than caching, so a spec can flip it at runtime; every call site is actor system startup or a config reload, so nothing hot pays for it. On a trimmed app the getter is already a constant and AppContext.SetSwitch has no effect on it. Also fixes src/xunitSettings.props, which anchored xunit.runner.json on $(SolutionDir): that is only defined for a solution build, so building a test project by its own path never copied the file and the runner silently ran with parallel collections ON, which would let the switch specs here race the rest of the suite. Anchored on $(MSBuildThisFileDirectory) instead - verified that bin/.../xunit.runner.json now exists and the runner reports "parallel test collections = off" (and "on [8 threads]" with the file removed). No resolution call sites yet - the specs here cover the switch and the name normalization. * Resolve the log formatter, stdout logger and scheduler from built-in tables Three settings in core name a type that core itself ships: akka.stdout-logger-class and akka.logger-formatter (read by Settings) and akka.scheduler.implementation (read by ActorSystemImpl.ConfigureScheduler). All three went through Type.GetType, so the trimmer could not tell which type was loaded and dropped it - the first unrooted AOT publish died in Settings..ctor with "Could not load type of Akka.Event.SemanticLogMessageFormatter, Akka for ILogMessageFormatter", and after that in ConfigureScheduler with "Could not resolve type Akka.Actor.HashedWheelTimerScheduler in assembly Akka". Each site now consults a BuiltIn* table first and constructs the type directly. A name that is not in the table falls back to the old reflection code, moved into a private static method marked [RequiresUnreferencedCode], and that fallback only runs while dynamic type loading is on; with the switch off the site throws a ConfigurationException naming the setting, the value and the switch, built by AkkaFeatures.NotBuiltIn so the three messages cannot drift apart. Every table carries two spellings of each name it knows - the bare "Ns.T" (how akka.conf writes the scheduler) and "Ns.T, Akka" (how akka.conf writes the log formatter) - and the lookup runs the configured value through TypeExtensions.StripAssemblyIdentity first. That is what makes a full AssemblyQualifiedName match, which matters because Akka.Hosting's LoggerConfigBuilder writes one into HOCON: normalizing the value handles any version, where a third key spelled typeof(T).AssemblyQualifiedName would only ever have matched the version of the build that emitted it. A value that still misses the table has nowhere to go once the reflection branch is trimmed away, which is why neither spelling may be removed. The values are used exactly as Config.GetString returns them - no Trim() anywhere, because Akka's HOCON parser already strips leading and trailing whitespace from every string it hands back, in every form (quoted, unquoted, triple-quoted, concatenated, substituted), with a whitespace-only value coming back empty or null. Verified rather than assumed. Two behavior differences with the switch on, both recorded in BREAKING_CHANGES_V1.6.md: a built-in type is now constructed directly, so a constructor that rejects its config reports its own exception rather than the TargetInvocationException Activator.CreateInstance wrapped it in; and a built-in name written with the assembly-qualified name of a different Akka version now resolves where Type.GetType rejected the version mismatch - measured on dev as ArgumentException for the two Settings sites and FileNotFoundException for the scheduler, whose Type.GetType passes throwOnError: true. Custom names are unchanged: the reflection fallback still gets the raw value. * Add the AOT canary app src/aot/Akka.AOT.App is a Native AOT canary for core. It boots a local ActorSystem twice - once from the bare default config, once through an empty BootstrapSetup, which is the path Akka.Hosting and the DI integrations take - round-trips a message through one UntypedActor and one ReceiveActor with Ask, checks that what core resolves from HOCON actually got built, then terminates under a bounded 30s cap. A silent boot is not proof of anything, because Serialization and Mailboxes log-and-continue when a configured type name does not resolve: an ActorSystem will come up looking healthy with no serializers registered at all. So exit 0 plus "[canary] OK" now requires two further things. 1. No warnings. Each run installs a LogFilterSetup whose filter records every WARNING and ERROR the stdout logger is asked to print, and fails the run with the collected messages. That is what catches "The type name for serializer 'json' did not resolve to an actual Type", "Serialization binding to non existing serializer" and "Mailbox Requirement mapping [...] is not an actual type". 2. Positive assertions. Serialization.FindSerializerFor returns a serializer for a string and for a byte[], Scheduler is a HashedWheelTimerScheduler, Settings.LogFormatter is a SemanticLogMessageFormatter, and Mailboxes.Lookup("akka.actor.default-mailbox") is an UnboundedMailbox. AppDomain.UnhandledException prints the same failure block, so a crash on a pool thread cannot exit quietly. The project references only src/core/Akka and sets Akka.DynamicTypeLoading to false with Trim="true", so ILLink removes core's reflection fallbacks and the app genuinely has to boot without them. IlcTreatWarningsAsErrors is off because the repo-wide warnings-as-errors would otherwise turn every IL2xxx/IL3xxx warning ILC emits into a failed publish, and printing that list is the app's whole job; the app's own C# still builds with warnings as errors. PublishAot is gated on a RuntimeIdentifier so a plain solution build does not restore the ILCompiler or emit a self-contained output, and so that it never becomes a global MSBuild property that would switch the trim/AOT analyzers on for Akka.csproj as a side effect. -p:RootAkka=true roots the Akka assembly so ILC analyzes all of core rather than just the paths these two toy actors reach. Registered in Akka.slnx under /AOT/, and deliberately not wired into CI yet: the AOT publish does not reach "[canary] OK" until the remaining reflection sites are converted. Running the same program on the JIT does, which is how the pass condition is checked for not being vacuous. * CI: run PR validation for pull requests that target the stacked aot/* branches Azure's pr trigger filters on the TARGET branch. The AOT milestone-1 stack (#8601-#8608) bases each PR on the previous branch under aot/, so without this entry every stacked PR shows 'PR Validation: skipping' and only the bottom PR is validated. * Tighten StripAssemblyIdentity test and expose SwitchName for reuse Should_strip_assembly_identity_From_a_qualified_type_name used BeOneOf(bare, "Ns.T, Akka"), which let a wrong result pass for any input. Switch to InlineData(input, expected) pairs and assert Be on the exact expected value for each case. Also widen SwitchName from private to internal so later specs in other test projects/files within this collection can reference it instead of redeclaring the switch name string themselves.
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
2 times, most recently
from
September 23, 2026 21:13
af572c8 to
dca22ef
Compare
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
2 times, most recently
from
September 24, 2026 03:31
dcc0fa1 to
8746f23
Compare
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
2 times, most recently
from
September 24, 2026 15:26
6c019fc to
4d312bb
Compare
Aaronontheweb
removed this pull request from stack #8607
September 24, 2026 16:42
Aaronontheweb
added this pull request to stack #8632
September 24, 2026 16:43
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
from
September 24, 2026 16:44
4d312bb to
97b0f12
Compare
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
from
September 24, 2026 17:00
97b0f12 to
b54ab53
Compare
Adds an "AOT canary (Linux)" job to PR validation. It publishes src/aot/Akka.AOT.App twice, because the two publishes prove different things and neither is sufficient alone. The UNROOTED publish is the real-application proof: ILC keeps only what the entry point reaches, and the binary then has to boot two local ActorSystems and print `[canary] OK`. It is the only step that can catch a type that got trimmed away and is needed at runtime - rooting the assembly would keep that type alive and mask exactly that bug. A silent exit 0 is not a pass either, because core logs-and-continues when a configured type name will not resolve, so the job greps for the string the canary's own watchdog and assertions produce. The ROOTED publish (-p:RootAkka=true) makes ILC analyse every path in the library rather than the handful two toy actors reach, and its log is what the baseline check reads. The unrooted publish sees 4 sites; the rooted one sees 12. The 8 it adds - Props.TypeName, PropsSurrogate.FromSurrogate, DynamicAccess.CreateInstanceFor, IpExtensions.GetInstanceField and four in NewtonSoftJsonSerializer - are precisely where milestone 2 works, so gating on the unrooted list alone would leave that work unwatched. scripts/CheckAotWarnings.cs is a .NET 10 file-based app (no project to maintain, runs on the SDK the job already installs). It keys each warning on code + file + owning member + message, deliberately not on the line number, so editing code above a warning site does not churn the baseline. Member and message are both in the key because neither alone is enough: one member can hold two warnings of the same code that differ only by the callee. It folds the Roslyn analyzer's member-less form into the ILC/ILLink form of the same warning, and normalizes away the whitespace the two tools disagree about. Three ways the check could have measured nothing and still reported OK are now hard failures: a warm obj/ that makes MSBuild skip ILC, a repo root that pushes every path out of scope, and a PDB-less log where no warning carries a file. Paths are made relative to an explicit --repo-root rather than cut at the first "/src/" segment, which was wrong for any checkout living under a directory called src. A final step feeds the checker a committed three-line log holding one warning that is not in the baseline and fails unless the checker rejects it, so the red path is proven on every run. Both publish logs and the run log are published as an artifact with condition: always(). Linux only, blocking from day one, and not routed through azure-pipeline.template.yaml - that template opens with a full solution build, and `dotnet publish` of the canary needs src/core/Akka and nothing else. Relates to #7246.
…f for trimmed and AOT publishes Consumers of the Akka package should not have to know the feature switch exists. The package now ships buildTransitive/Akka.targets, which adds `RuntimeHostConfigurationOption Akka.DynamicTypeLoading = false` with Trim="true" whenever the consuming project sets PublishAot or PublishTrimmed. The file name has to match the package id or NuGet ignores it. buildTransitive/ is the only copy. The older build/ folder is for NuGet clients before 5.0, and none of those can consume a net10.0 package in the first place. The item is added inside a target rather than at evaluation time, which matters more than it looks. NuGet imports the package's .targets from Microsoft.Common.targets, ahead of Directory.Build.targets, so an evaluation-time guard cannot see an opt-out declared there and both items end up in RuntimeHostConfigurationOption. That is worse than losing: ILLink's _TrimmerFeatureSettings takes the item carrying Trim="true" (ours, false) while runtimeconfig.json takes the last writer (theirs, true), so the app reads the switch as on while the trimmer has already baked it off and deleted the fallbacks - the hand-rooted type still throws and nothing says why. Verified: the evaluation-time form produces two items, the target produces one. BeforeTargets names PrepareForILLink, which populates _TrimmerFeatureSettings for ILLink and, via IlcCompileDependsOn, for ILC too; and _GenerateRuntimeConfigurationFilesInputCache, which hashes the option items - being before GenerateBuildRuntimeConfigurationFiles alone is not enough, because that target depends on the cache one. An app that declares the option itself wins, in either direction and from anywhere MSBuild reads - csproj, Directory.Build.props, Directory.Build.targets, a publish profile or -p. Ordinary untrimmed builds add nothing at all. Verified against the packed nupkg from apps outside the repo: PublishAot with the switch untouched, PublishTrimmed with the switch untouched, PublishAot with the app setting it to true in its csproj, PublishAot with the app setting it to true in Directory.Build.targets, and PublishAot coming from a .pubxml publish profile. The defaulted cases land `"Akka.DynamicTypeLoading": false` in runtimeconfig.json and their unbound types throw as designed; the opt-out cases land `true`, keep reflection and resolve the Newtonsoft serializer. Relates to #7246.
Adds docs/articles/deployment/native-aot.md, registered in the deployment toc: what works under Native AOT and trimming today, how to opt in (nothing to do if you reference the package, one csproj item otherwise), what the Akka.DynamicTypeLoading switch changes when it is off, and what is still missing - Akka.Remote and Akka.Cluster, custom mailboxes/loggers/extensions pending the Setup APIs, and Newtonsoft being trimmed away. Relates to #7246.
Aaronontheweb
force-pushed
the
aot/m1-f-canary-ci-and-targets
branch
from
September 25, 2026 00:46
b54ab53 to
f648c13
Compare
Aaronontheweb
commented
Sep 25, 2026
Aaronontheweb
left a comment
Member
Author
There was a problem hiding this comment.
LGTM - mostly docs and AOT packaging for nuget distribution
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
Stack: PR 7 of 7 for AOT milestone 1. Base is PR #8606; this PR shows only its own three commits. Do not merge this ahead of the rest of the stack: its CI job requires the canary to be green, which is only true with #8601–#8606 applied.
build-system/pr-validation.yaml, Linux only). Two publishes ofsrc/aot/Akka.AOT.App: an unrooted one whose binary is run and must exit 0 and print[canary] OK(the real-app boot proof: rooting would mask a trimmed-away type), and a rooted one (TrimmerRootAssembly=Akka) whoseIL2xxx/IL3xxxwarnings undersrc/core/Akka/are diffed against a checked-in baseline (src/aot/Akka.AOT.App/aot-warnings.baseline.txt, 12 entries: the four on the boot path plusDynamicAccess,Props.TypeName/PropsSurrogate,IpExtensions, and four inNewtonSoftJsonSerializer.cs). New warnings fail the job with a diff; removed ones pass with a notice naming the lines to delete. The checker is a .NET 10 file-based app,scripts/CheckAotWarnings.cs, keyed oncode|file|member|normalized messageso line drift doesn't churn the baseline; it fails rather than passes when it parses nothing (warmobj/, wrong repo root, PDB-less log), and the job self-tests it against a committed fixture with one extra warning. Publish and run logs are uploaded as artifacts.buildTransitive/Akka.targetspacked with theAkkapackage: when a consumer setsPublishAotorPublishTrimmed, a target running beforePrepareForILLinkand the runtimeconfig input cache adds<RuntimeHostConfigurationOption Include="Akka.DynamicTypeLoading" Value="false" Trim="true" />unless the app already declares that option. Proven against the packed nupkg in five consumer variants:PublishAot;PublishTrimmed; app opts out in its csproj; app opts out inDirectory.Build.targets(the ordering case an evaluation-time item gets wrong);PublishAotfrom a.pubxml. The opt-in is therefore automatic, and the app's own value wins wherever it's declared.docs/articles/deployment/native-aot.md: what works, how the opt-in happens, what the switch-off behavior is, and what isn't supported yet (classic DotNetty remoting; custom mailboxes/loggers/extensions until the Setup APIs land; Newtonsoft off under AOT so message types need registered serializers).scripts/test-aot-compatibility.ps1andbuild-system/azure.aot.template.yamlfrom #7458 never landed ondev, so there is nothing to retire.Ledger: the
.targetsis a behavior change for any trimmed or AOT publish that hand-rooted a HOCON-named type and relied onType.GetTypefinding it; escape hatch is declaring the option withValue="true".Design and measurements: epic #7246.
Checklist