Fix NativeAOT resource string inlining for async IL - #133970
Conversation
Preserve ECMA module identity for async IL wrappers and recognize it when injecting and resolving resource string tokens. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf8623a5-a7a4-4c7d-8371-a97b53478abe
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🔵 Needs a closer look
Add regression coverage for async resource-string inlining and verify the resource is not rooted.
Pull request overview
Fixes NativeAOT resource-string inlining for runtime-async IL by preserving ECMA module identity.
Changes:
- Adds an ECMA-aware async IL wrapper.
- Updates resource token injection and resolution for ECMA-backed IL.
File summaries
| File | Summary |
|---|---|
src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs |
Preserves module identity for async IL wrappers. |
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs |
Enables resource-string token handling for ECMA-backed async IL. |
Review details
Suppressed comments (2)
src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs:374
- Could you add a NativeAOT regression test that exercises a real runtime-async method containing an
SRresource-property access? The existingFrameworkStringscoverage only exercises resource-string substitution from synchronous IL, so it would still pass if this wrapper failed to preserve its ECMA module. The assertion should verify the resource is not rooted (or inspect the resulting size/dependency output), not only that the async call returns the expected string, since the old implementation can still return the value by retaining the resource.
return wrappedIL is EcmaMethodIL ecmaIL
? new EcmaAsyncMethodIL(asyncVariantImpl, ecmaIL)
: new AsyncMethodIL(asyncVariantImpl, wrappedIL);
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs:635
- Please add a NativeAOT regression test for this path. The existing runtime-async tests do not exercise
SubstitutedILProvider's resource-string inlining, so a future regression in the async wrapper's ECMA-module identity or injected-token resolution could recreate the size/rooting issue without failing tests. The test should compile an async method containing a generatedSRaccessor and verify that the accessor is inlined and its resource is not rooted.
[!NOTE]
This review comment was created by GitHub Copilot.
if (hasGetResourceStringCall && method.GetMethodILDefinition() is IEcmaMethodIL ecmaMethodIL)
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
|
/backport to release/11.0 |
|
Started backporting to |
…134065) Backport of #133970 to release/11.0 /cc @jakobbotsch ## Customer Impact - [x] Customer reported - [ ] Found internally NativeAOT fails to fold some string resource lookups when used from within async methods. This leads to pulling in a large amount of unnecessary resource machinery, bloating app sizes using string resources for NativeAOT. Reported by customer in #133954. ## Regression - [ ] Yes - [X] No ## Testing Manual testing on customer's application. NativeAOT outerloop pipeline run. ## Risk Low. Co-authored-by: Jakob Botsch Nielsen <Jakob.botsch.nielsen@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf8623a5-a7a4-4c7d-8371-a97b53478abe
|
I think overall effect is amotrized. e.g. using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
Todo[] sampleTodos =
[
new(1, "Walk the dog"),
new(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
new(3, "Do the laundry", DateOnly.FromDateTime(DateTime.Now.AddDays(1))),
new(4, "Clean the bathroom"),
new(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
];
var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos).WithName("GetTodos");
todosApi.MapGet("/{id}", Results<Ok<Todo>, NotFound> (int id) =>
sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
? TypedResults.Ok(todo)
: TypedResults.NotFound())
.WithName("GetTodoById");
// --- START OF AMORTIZATION TEST MIXED ASYNC BAG ---
var testApi = app.MapGroup("/performance-test");
// Generate 50 distinct async endpoints with unpredictable execution branches
for (int i = 1; i <= 50; i++)
{
int currentId = i;
testApi.MapGet($"/metric-{currentId}", async Task<Results<Ok<AsyncPayload>, BadRequest<string>>> (int factor) =>
{
// Branching state machine execution path to prevent aggressive optimization
if (factor < 0)
{
await Task.Delay(1); // Forces suspension point
return TypedResults.BadRequest("Factor must be positive");
}
// Simulating data crunching with real async yields
await Task.Yield();
long calculationResult = 0;
for (int j = 0; j < 100; j++)
{
calculationResult += (currentId * factor) + j;
}
// Multi-stage suspension logic
await Task.Delay(TimeSpan.FromMilliseconds(5));
var payload = new AsyncPayload(
Id: currentId,
Hash: $"Step_{currentId}_{calculationResult}",
Timestamp: DateTime.UtcNow,
IsProcessed: true
);
return TypedResults.Ok(payload);
}).WithName($"GetMetric-{currentId}");
}
// --- END OF AMORTIZATION TEST MIXED ASYNC BAG ---
app.Run();
public record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);
public record AsyncPayload(int Id, string Hash, DateTime Timestamp, bool IsProcessed);
[JsonSerializable(typeof(Todo[]))]
[JsonSerializable(typeof(AsyncPayload))] // Required for AOT Native serialization
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}then |
Preserve ECMA module identity for async IL wrappers and recognize it when injecting and resolving resource string tokens.
Fix #133954
The remaining size difference is coming from
runtime/src/libraries/Common/src/System/Threading/AsyncOverSyncWithIoCancellation.cs
Lines 84 to 86 in 9b84671