Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;

namespace System.Runtime.CompilerServices
{
Expand Down Expand Up @@ -491,6 +492,50 @@ public static void HandleSuspended<T, TOps>(T task) where T : Task, ITaskComplet
}
else if (calledTask != null)
{
if (calledTask is IValueTaskAsTask vtTask)
{
if (!vtTask.IsConfigured)
{
// ValueTaskSource can be configured to use scheduling context or to ignore it.
// The awaiter must inform the source whether it wants to continue on a context,
// but the source may decide to ignore the suggestion. Since the behavior of
// the source takes precedence, we clear the context flags from the awaiting
// continuation (so it will run transparently on what source decides) and tell
// the source that awaiting frame prefers to continue on a context.
// The reason why we do it here is because the continuation chain builds from the
// innermost frame out and when the leaf thunk links the head continuation,
// it does not know if the caller wants to continue in a context. Thus the thunk
// creates an "unconfigured" IValueTaskAsTask and we configure it here.
ValueTaskSourceOnCompletedFlags configFlags = ValueTaskSourceOnCompletedFlags.None;
CorInfoContinuationFlags continuationFlags = headContinuation.Next!.Flags;

const CorInfoContinuationFlags continueOnContextFlags =
CorInfoContinuationFlags.CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_SYNCHRONIZATION_CONTEXT |
CorInfoContinuationFlags.CORINFO_CONTINUATION_CONTINUE_ON_CAPTURED_TASK_SCHEDULER;

if ((continuationFlags & continueOnContextFlags) != 0)
{
// if await has captured some context, inform the source
configFlags |= ValueTaskSourceOnCompletedFlags.UseSchedulingContext;
}

// clear continuation flags, so that continuation runs transparently
headContinuation.Next!.Flags &= ~continueFlags;
vtTask.Configure(configFlags);

if (!calledTask.TryAddCompletionAction(task))
{
// calledTask has already completed and we need to schedule
// the continuation for execution ourselves.
// Restore the continuation flags before doing that.
headContinuation.Next!.Flags = continuationFlags;
ThreadPool.UnsafeQueueUserWorkItemInternal(task, preferLocal: true);
}

return;
}
}

// Runtime async callable wrapper for task returning
// method. This implements the context transparent
// forwarding and makes these wrappers minimal cost.
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/inc/clrconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ RETAIL_CONFIG_DWORD_INFO(EXTERNAL_EnableRiscV64Zbb, W("EnableRiscV64
#endif

// Runtime-async
RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 0, "Enables runtime async method support")
RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 1, "Enables runtime async method support")

///
/// Uncategorized
Expand Down
35 changes: 29 additions & 6 deletions src/coreclr/jit/async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -958,18 +958,25 @@ ContinuationLayout AsyncTransformation::LayOutContinuation(BasicBlock*
if (dsc->TypeIs(TYP_STRUCT) || dsc->IsImplicitByRef())
{
ClassLayout* layout = dsc->GetLayout();
assert(!layout->HasGCByRef());

// TODO: (async) we do not need to save ByRef-containing locals
// as by the spec an await turns them into zero-inited state.
// For now just store/restore as if there are no gc refs.
// This is mostly to handle the "fake" live-across-await byrefs
// in min-opts, since C#-compiled code by itself does not let
// byrefs be live across awaits.
unsigned objCount = layout->HasGCByRef() ? 0 : layout->GetGCPtrCount();
Comment on lines +962 to +968
Copy link
Member

Choose a reason for hiding this comment

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

We will need to figure out what is creating these LIR edges that are live across the await and stop doing that. This fix will replace assert with bad codegen instead.

Copy link
Member Author

@VSadov VSadov Sep 11, 2025

Choose a reason for hiding this comment

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

I typically see the assert when compiling Debug code for Task-returning methods with ref-like parameters.
Ex:

        public static Task WhenAll(params ReadOnlySpan<Task> tasks)
        {

async
This method is not async, but it's thunk will be async and will also have a byref-like parameter.
The part that the edge lives across the await is likely a result of Debug not tracking liveness precisely.

Once we have zeroing of byrefs that live across await, we may not need this.
This is not a fix. It is a workaround. - my goal is to get all Libraries tests pass or find something truly blocking.

Copy link
Member Author

@VSadov VSadov Sep 11, 2025

Choose a reason for hiding this comment

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

Assuming that it is a result of Debug emit, perhaps forcing thunks to always compile optimized might be an alternative workaround, if there is an easy way to force.

I just thought of this workaround first and it seems working well enough.

Copy link
Member

Choose a reason for hiding this comment

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

The part that the edge lives across the await is likely a result of Debug not tracking liveness precisely.

There are two sources of live state across async calls in JIT IR:

  1. Locals. These behave like IL locals and can be multiply defined and multiply used. They are what liveness analysis treat. They are added by AsyncLiveness::GetLiveLocals.
  2. LIR edges. These are single-def single-use (SDSU) values that are defined in one place in a basic block and consumed later in the same basic block. When they overlap an async call (defined before, used after) they must also be preserved on heap. Liveness analysis does not treat these; they are known to be live when they overlap the call. They are added by AsyncTransformation::LiftLIREdges.

The liveness imprecision only comes into play for (1). However, we already ignore byref locals for these. It happens here:

if ((dsc->TypeIs(TYP_BYREF) && !dsc->IsImplicitByRef()) ||
(dsc->TypeIs(TYP_STRUCT) && dsc->GetLayout()->HasGCByRef()))
{
// Even if these are address exposed we expect them to be dead at
// suspension points. TODO: It would be good to somehow verify these
// aren't obviously live, if the JIT creates live ranges that span a
// suspension point then this makes it quite hard to diagnose that.
return false;
}

That just leaves (2). But since these are known to be live, it is (almost?) always going to be a bug that resulted in these.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point! I will log an issue on this - to follow up with real fix.


if (layout->IsCustomLayout())
{
inf.Alignment = 1;
inf.DataSize = layout->GetSize();
inf.GCDataCount = layout->GetGCPtrCount();
inf.GCDataCount = objCount;
}
else
{
inf.Alignment = m_comp->info.compCompHnd->getClassAlignmentRequirement(layout->GetClassHandle());
if ((layout->GetGCPtrCount() * TARGET_POINTER_SIZE) == layout->GetSize())
if ((objCount * TARGET_POINTER_SIZE) == layout->GetSize())
{
inf.DataSize = 0;
}
Expand All @@ -978,7 +985,7 @@ ContinuationLayout AsyncTransformation::LayOutContinuation(BasicBlock*
inf.DataSize = layout->GetSize();
}

inf.GCDataCount = layout->GetGCPtrCount();
inf.GCDataCount = objCount;
}
}
else if (dsc->TypeIs(TYP_REF))
Expand All @@ -987,10 +994,17 @@ ContinuationLayout AsyncTransformation::LayOutContinuation(BasicBlock*
inf.DataSize = 0;
inf.GCDataCount = 1;
}
else if (dsc->TypeIs(TYP_BYREF))
{
// TODO: (async) ByRefs do not need to be saved at all.
// For now pretend they are unmanaged data.
// See the note on `layout->HasGCByRef()` case for justification.
inf.Alignment = TARGET_POINTER_SIZE;
inf.DataSize = TARGET_POINTER_SIZE;
inf.GCDataCount = 0;
}
else
{
assert(!dsc->TypeIs(TYP_BYREF));

inf.Alignment = genTypeAlignments[dsc->TypeGet()];
inf.DataSize = genTypeSize(dsc);
inf.GCDataCount = 0;
Expand Down Expand Up @@ -1695,6 +1709,15 @@ void AsyncTransformation::CreateCheckAndSuspendAfterCall(BasicBlock*
*remainder = m_comp->fgSplitBlockAfterNode(block, jtrue);
JITDUMP(" Remainder is " FMT_BB "\n", (*remainder)->bbNum);

// HACK: Not sure why it can happen, but we may see the end IL for the block
// to increasing after splitting off its tail.
// This tweak is just to avoid asserts later on.
// This is not a real fix.
if (block->bbCodeOffsEnd > (*remainder)->bbCodeOffs)
{
block->bbCodeOffsEnd = (*remainder)->bbCodeOffs;
}

FlowEdge* retBBEdge = m_comp->fgAddRefPred(suspendBB, block);
block->SetCond(retBBEdge, block->GetTargetEdge());

Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/vm/asyncthunks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pAsyncOtherVariant, MetaSig& m

MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK__GET_ISCOMPLETED);
MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK__THROW_IF_COMPLETED_UNSUCCESSFULLY);
MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK__AS_TASK);
MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK__AS_UNCONFIGURED_TASK);

isCompletedToken = pCode->GetToken(pMDValueTaskIsCompleted);
completionResultToken = pCode->GetToken(pMDCompletionResult);
Expand All @@ -604,7 +604,7 @@ void MethodDesc::EmitAsyncMethodThunk(MethodDesc* pAsyncOtherVariant, MetaSig& m

MethodDesc* pMDValueTaskIsCompleted = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_ISCOMPLETED);
MethodDesc* pMDCompletionResult = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__GET_RESULT);
MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__AS_TASK);
MethodDesc* pMDAsTask = CoreLibBinder::GetMethod(METHOD__VALUETASK_1__AS_UNCONFIGURED_TASK);

pMDValueTaskIsCompleted = FindOrCreateAssociatedMethodDesc(pMDValueTaskIsCompleted, pMTValueTask, FALSE, Instantiation(), FALSE);
pMDCompletionResult = FindOrCreateAssociatedMethodDesc(pMDCompletionResult, pMTValueTask, FALSE, Instantiation(), FALSE);
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ DEFINE_METHOD(THREAD_START_EXCEPTION,EX_CTOR, .ctor,
DEFINE_CLASS(VALUETASK_1, Tasks, ValueTask`1)
DEFINE_METHOD(VALUETASK_1, GET_ISCOMPLETED, get_IsCompleted, NoSig)
DEFINE_METHOD(VALUETASK_1, GET_RESULT, get_Result, NoSig)
DEFINE_METHOD(VALUETASK_1, AS_TASK, AsTask, IM_RetTaskOfT)
DEFINE_METHOD(VALUETASK_1, AS_UNCONFIGURED_TASK, AsUnconfiguredTask, IM_RetTaskOfT)

DEFINE_CLASS(VALUETASK, Tasks, ValueTask)
DEFINE_METHOD(VALUETASK, FROM_EXCEPTION, FromException, SM_Exception_RetValueTask)
Expand All @@ -355,7 +355,7 @@ DEFINE_METHOD(VALUETASK, FROM_RESULT_T, FromResult, GM_T_RetValueTaskOfT)
DEFINE_METHOD(VALUETASK, GET_COMPLETED_TASK, get_CompletedTask, SM_RetValueTask)
DEFINE_METHOD(VALUETASK, GET_ISCOMPLETED, get_IsCompleted, NoSig)
DEFINE_METHOD(VALUETASK, THROW_IF_COMPLETED_UNSUCCESSFULLY, ThrowIfCompletedUnsuccessfully, NoSig)
DEFINE_METHOD(VALUETASK, AS_TASK, AsTask, IM_RetTask)
DEFINE_METHOD(VALUETASK, AS_UNCONFIGURED_TASK, AsUnconfiguredTask, IM_RetTask)

DEFINE_CLASS(TASK_1, Tasks, Task`1)
DEFINE_METHOD(TASK_1, GET_RESULTONSUCCESS, get_ResultOnSuccess, NoSig)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method)]
internal sealed class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute
{
public bool RuntimeAsync { get; } = runtimeAsync;
}
5 changes: 5 additions & 0 deletions src/libraries/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,9 @@
<CoverageIncludeDirectory Include="shared\Microsoft.NETCore.App\$(ProductVersion)" />
</ItemGroup>

<PropertyGroup>
<NoWarn>$(NoWarn);xUnit1013;CS1998;SYSLIB5007</NoWarn>
<Features Condition="'$(RuntimeFlavor)' != 'mono' and '$(TestBuildMode)' != 'nativeaot' and '$(TargetArchitecture)' != 'wasm'">$(Features);runtime-async=on</Features>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-browser;$(NetFrameworkCurrent)</TargetFrameworks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<NoWarn>NU1511</NoWarn>
<NoWarn>$(NoWarn);NU1511</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetOS)' == 'browser'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-browser;$(NetFrameworkCurrent)</TargetFrameworks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<NoWarn>NU1511</NoWarn>
<NoWarn>$(NoWarn);NU1511</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetOS)' == 'browser'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-browser;$(NetFrameworkCurrent)</TargetFrameworks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<NoWarn>NU1511</NoWarn>
<NoWarn>$(NoWarn);NU1511</NoWarn>
<EventSourceSupport Condition="'$(TestNativeAot)' == 'true'">true</EventSourceSupport>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetOS)' == 'browser'">
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Compile Include="System\Net\Quic\**\*.cs" Exclude="System\Net\Quic\*.Unsupported.cs"/>
<!-- System.Net common -->
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs" Link="Common\DisableRuntimeMarshalling.cs" />
<Compile Include="$(CommonPath)System\Runtime\CompilerServices\RuntimeAsyncMethodGenerationAttribute.cs" Link="Common\System\Runtime\CompilerServices\RuntimeAsyncMethodGenerationAttribute.cs" />
<Compile Include="$(CommonPath)System\AppContextSwitchHelper.cs" Link="Common\System\AppContextSwitchHelper.cs" />
<Compile Include="$(CommonPath)System\Net\SafeHandleCache.cs" Link="Common\System\Net\SafeHandleCache.cs" />
<Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="Common\System\Net\ArrayBuffer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public SslConnectionOptions(QuicConnection connection, bool isClient,
_certificateChainPolicy = certificateChainPolicy;
}

// Roslyn NYI - async in structs. Remove opt-out once supported.
[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)]
internal async Task<bool> StartAsyncCertificateValidation(IntPtr certificatePtr, IntPtr chainPtr)
{
//
Expand Down
Loading
Loading