Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b7bb968
Set target method desc for resumption stubs
jakobbotsch Oct 1, 2025
d5d0864
Add JIT-EE boilerplate
jakobbotsch Oct 1, 2025
d7277fb
Add debug information for runtime async information
jakobbotsch Oct 1, 2025
6addd0e
Update managed view
jakobbotsch Oct 1, 2025
6a5bf19
Remove TODOs
jakobbotsch Oct 1, 2025
262260a
Leaf transition
jakobbotsch Oct 1, 2025
4e2a829
Comment
jakobbotsch Oct 1, 2025
820afa9
Delete unused enum
jakobbotsch Oct 1, 2025
3621eae
Restore comment
jakobbotsch Oct 1, 2025
f597424
Fix osx build
jakobbotsch Oct 1, 2025
6bb6cee
Fix GCC build
jakobbotsch Oct 1, 2025
f580e3f
Change sentinel value, fix contract
jakobbotsch Oct 2, 2025
55b9522
Bump R2R
jakobbotsch Oct 2, 2025
c9c64be
Clean up
jakobbotsch Oct 2, 2025
db77446
Expose async debug info accessor APIs
jakobbotsch Oct 2, 2025
c808390
Missed bumping R2R version for naot
jakobbotsch Oct 2, 2025
3ec5160
Fix reverse mapping to IL local nums
jakobbotsch Oct 2, 2025
bb12c77
Fix monotonicity for async vars
jakobbotsch Oct 2, 2025
bf3364b
Code style
jakobbotsch Oct 2, 2025
b49f38f
Fix JIT-EE prompt tools from Egor's instructions, allow use of experi…
jakobbotsch Oct 2, 2025
d1bf49f
Address feedback, make comment less misleading
jakobbotsch Oct 3, 2025
579714e
Add AsyncContinuationVarInfo.GCIndex
jakobbotsch Oct 6, 2025
c056793
Publish NextContinuation in TLS
jakobbotsch Oct 8, 2025
9679f91
Rename ThunkTask -> RuntimeAsyncTask
jakobbotsch Oct 8, 2025
7a34475
Merge branch 'main' of github.com:dotnet/runtime into jit-async-diagn…
jakobbotsch Oct 9, 2025
e839409
Report native offsets instead
jakobbotsch Oct 9, 2025
d8ad0c1
Run jit-format
jakobbotsch Oct 9, 2025
9fdd8a7
Print reported async debug info, always report it
jakobbotsch Oct 9, 2025
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
26 changes: 26 additions & 0 deletions src/coreclr/inc/cordebuginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,30 @@ class ICorDebugInfo
// Source information about the IL instruction in the inlinee
SourceTypes Source;
};

Copy link
Member

Choose a reason for hiding this comment

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

Has there been any discussion about the managed return value feature? I'm fine if its out of scope for this PR and maybe it doesn't even land in .NET 11, but didn't want it to be a surprise if it came up later. The feature works for synchronous code but I don't think it is supported for async V1 code. Its another example where the debugging experience currently gets worse when users switch their code from sync to async. I'm not aware of anything that prevents it from working - its just work that has never been done.

At the JIT-EE level managed return value consists of reporting the variable home for the return value of an IL call, even if that return value is only pushed on the IL stack and never assigned an explicit IL local. In async code the natural extension would be reporting the return value storage location of a call that had to be awaited.

Copy link
Member Author

Choose a reason for hiding this comment

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

In async code the natural extension would be reporting the return value storage location of a call that had to be awaited.

I think if the debugger breaks on the join point of the synchronous/resumption paths, then it can read the return value from the same place as normal. That join point will be the native offset I am working on encoding for the suspension points. So we may already have enough information available after that.

struct AsyncContinuationVarInfo
{
// IL number of variable (or one of the special IL numbers, like UNKNOWN_ILNUM)
uint32_t VarNumber;
// Offset in continuation's data where this variable is stored
uint32_t Offset;
Copy link
Member Author

Choose a reason for hiding this comment

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

We probably need to include GCData offset here too, but I expect that we will end up with customized continuation layouts during .NET 11 that gets rid of the Data/GCData split, so I designed some of these types around that expectation. It also should be possible to derive the GCData offset by iterating the vars linearly, so we can probably make do with that for now.

Copy link
Member

Choose a reason for hiding this comment

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

Storing variables in arrays may work well for some scenarios, like AOT, which is sensitive to generation of types or GC layouts.

I think uint32_t Offset; may still work if it means "if Offset is greater than GCData length, subtract the length of GCData and continue in non-GC data"

Just in case we may end up using more than one shape, perhaps add some versioning/flavor marker in the AsyncSuspensionPoint?

Copy link
Member Author

@jakobbotsch jakobbotsch Oct 3, 2025

Choose a reason for hiding this comment

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

I think uint32_t Offset; may still work if it means "if Offset is greater than GCData length, subtract the length of GCData and continue in non-GC data"

For some values we have data stored in both Data and GCData arrays, and to reconstruct the value we need both offsets. Perhaps we can just do 2x 16-bit offsets in this field until we get tailored continuation layouts. I think it's something to investigate further once we look at actually implementing the diagnostics that access the continuation values.

Just in case we may end up using more than one shape, perhaps add some versioning/flavor marker in the AsyncSuspensionPoint?

I expect we stabilize on something during .NET 11, so if we change it after I think we can take an R2R version update at the same time as well. If we do it like that we can version based on R2R module version (it's already what we do in DebugInfo.cs).

Copy link
Member

Choose a reason for hiding this comment

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

I agree with adding GCData offset. Just wonder if 16-bit is enough. Is that just 256 locals each with 256 fields, or some variation of that?

Copy link
Member Author

Choose a reason for hiding this comment

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

You would need to have a suspension point with more than 2^16 GC refs stored for it. That seems quite unlikely, so 16 bits would likely be enough. We might hit a hard cap somewhere else at that point, but I am not totally sure. Still, this is just a transfer format, so no need to optimize storage.

};

struct AsyncSuspensionPoint
{
// IL offset in the root method that resulted in the creation of this suspension point.
uint32_t RootILOffset;
// Index of inline tree node containing the IL offset (0 for root)
uint32_t Inlinee;
// IL offset that resulted in the creation of the suspension point.
uint32_t ILOffset;
// Count of AsyncContinuationVarInfo in array of locals
uint32_t NumContinuationVars;
};

struct AsyncInfo
{
// Number of suspension points in the method.
uint32_t NumSuspensionPoints;
};
};
10 changes: 10 additions & 0 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -2912,6 +2912,16 @@ class ICorStaticInfo
uint32_t numMappings // [IN] Number of rich mappings
) = 0;

// Report async debug information to EE.
// The arrays are expected to be allocated with allocateArray
// and ownership is transferred to the EE with this call.
virtual void reportAsyncDebugInfo(
ICorDebugInfo::AsyncInfo* asyncInfo, // [IN] Async method information
ICorDebugInfo::AsyncSuspensionPoint* suspensionPoints, // [IN] Array of async suspension points, indexed by state number
ICorDebugInfo::AsyncContinuationVarInfo* vars, // [IN] Array of async continuation variable info
uint32_t numVars // [IN] Number of continuation variables
) = 0;

// Report back some metadata about the compilation to the EE -- for
// example, metrics about the compilation.
virtual void reportMetadata(
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/inc/icorjitinfoimpl_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,12 @@ void reportRichMappings(
ICorDebugInfo::RichOffsetMapping* mappings,
uint32_t numMappings) override;

void reportAsyncDebugInfo(
ICorDebugInfo::AsyncInfo* asyncInfo,
ICorDebugInfo::AsyncSuspensionPoint* suspensionPoints,
ICorDebugInfo::AsyncContinuationVarInfo* vars,
uint32_t numVars) override;

void reportMetadata(
const char* key,
const void* value,
Expand Down
10 changes: 5 additions & 5 deletions src/coreclr/inc/jiteeversionguid.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@

#include <minipal/guid.h>

constexpr GUID JITEEVersionIdentifier = { /* 3d2bdd20-eced-4a07-b9fb-227ce7f55fcd */
0x3d2bdd20,
0xeced,
0x4a07,
{0xb9, 0xfb, 0x22, 0x7c, 0xe7, 0xf5, 0x5f, 0xcd}
constexpr GUID JITEEVersionIdentifier = { /* e1c4ef4f-62a1-4055-813e-23837fdfdffa */
0xe1c4ef4f,
0x62a1,
0x4055,
{0x81, 0x3e, 0x23, 0x83, 0x7f, 0xdf, 0xdf, 0xfa}
};

#endif // JIT_EE_VERSIONING_GUID_H
1 change: 1 addition & 0 deletions src/coreclr/jit/ICorJitInfo_names_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ DEF_CLR_API(setBoundaries)
DEF_CLR_API(getVars)
DEF_CLR_API(setVars)
DEF_CLR_API(reportRichMappings)
DEF_CLR_API(reportAsyncDebugInfo)
DEF_CLR_API(reportMetadata)
DEF_CLR_API(allocateArray)
DEF_CLR_API(freeArray)
Expand Down
11 changes: 11 additions & 0 deletions src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,17 @@ void WrapICorJitInfo::reportRichMappings(
API_LEAVE(reportRichMappings);
}

void WrapICorJitInfo::reportAsyncDebugInfo(
ICorDebugInfo::AsyncInfo* asyncInfo,
ICorDebugInfo::AsyncSuspensionPoint* suspensionPoints,
ICorDebugInfo::AsyncContinuationVarInfo* vars,
uint32_t numVars)
{
API_ENTER(reportAsyncDebugInfo);
wrapHnd->reportAsyncDebugInfo(asyncInfo, suspensionPoints, vars, numVars);
API_LEAVE(reportAsyncDebugInfo);
}

void WrapICorJitInfo::reportMetadata(
const char* key,
const void* value,
Expand Down
74 changes: 74 additions & 0 deletions src/coreclr/jit/async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,8 @@ PhaseStatus AsyncTransformation::Run()

m_comp->fgInvalidateDfsTree();

ReportDebugInfo();

return PhaseStatus::MODIFIED_EVERYTHING;
}

Expand Down Expand Up @@ -818,6 +820,8 @@ void AsyncTransformation::Transform(
BasicBlock* resumeBB = CreateResumption(block, *remainder, call, callDefInfo, stateNum, layout);

m_resumptionBBs.push_back(resumeBB);

CreateDebugInfoForSuspensionPoint(call, layout);
}

//------------------------------------------------------------------------
Expand Down Expand Up @@ -2202,6 +2206,76 @@ GenTreeStoreInd* AsyncTransformation::StoreAtOffset(GenTree* base, unsigned offs
return store;
}

//------------------------------------------------------------------------
// AsyncTransformation::CreateDebugInfoForSuspensionPoint:
// Create debug info for the specific suspension point we just created.
//
// Parameters:
// asyncCall - Call node resulting in the suspension point
// stateNum - State number that was assigned to the suspension point
// layout - Layout of continuation
//
void AsyncTransformation::CreateDebugInfoForSuspensionPoint(GenTreeCall* asyncCall, const ContinuationLayout& layout)
{
if (!m_comp->opts.compDbgInfo)
{
return;
}

uint32_t numLocals = 0;
for (const LiveLocalInfo& local : layout.Locals)
{
unsigned ilVarNum = m_comp->compMap2ILvarNum(local.LclNum);
if (ilVarNum == ICorDebugInfo::UNKNOWN_ILNUM)
{
continue;
}

ICorDebugInfo::AsyncContinuationVarInfo varInf;
varInf.VarNumber = ilVarNum;
varInf.Offset = local.DataOffset;
m_dbgContinuationVars.push_back(varInf);
numLocals++;
}

ICorDebugInfo::AsyncSuspensionPoint suspensionPoint;
const DebugInfo& di = asyncCall->GetAsyncInfo().DebugInfo;
suspensionPoint.RootILOffset = di.GetRoot().GetLocation().GetOffset();
suspensionPoint.Inlinee = di.GetInlineContext()->GetOrdinal();
suspensionPoint.ILOffset = di.GetLocation().GetOffset();
suspensionPoint.NumContinuationVars = numLocals;

m_dbgSuspensionPoints.push_back(suspensionPoint);
}

//------------------------------------------------------------------------
// AsyncTransformation::ReportDebugInfo:
// Report debug info back to EE.
//
void AsyncTransformation::ReportDebugInfo()
{
if (!m_comp->opts.compDbgInfo)
Copy link
Member

@lateralusX lateralusX Oct 8, 2025

Choose a reason for hiding this comment

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

If we would need this information to resolve continuation -> native IP of suspension point and potentially native IP of resumption point, then we would always need it to do stack walks, maybe not all, but at least that information.

Copy link
Member

Choose a reason for hiding this comment

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

We would also need the corresponding native->IL records for asyncv2 methods so we can emit them as part of rundown in EventPipe sessions. That should make it possible to go from the native IP's included in EventPipe callstacks, to IL offsets representing the suspension point in user code.

Copy link
Member Author

@jakobbotsch jakobbotsch Oct 9, 2025

Choose a reason for hiding this comment

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

The native<->IL mappings will just go through the normal path. There is nothing special about those provided we store native offsets as discussed above.

I agree if we have async stackwalking as part of normal stackwalking then we need the async debug information always, so we cannot let the user control whether it is produced.

{
return;
}

ICorDebugInfo::AsyncInfo asyncInfo;
asyncInfo.NumSuspensionPoints = static_cast<uint32_t>(m_dbgSuspensionPoints.size());

ICorDebugInfo::AsyncSuspensionPoint* hostSuspensionPoints =
static_cast<ICorDebugInfo::AsyncSuspensionPoint*>(m_comp->info.compCompHnd->allocateArray(
m_dbgSuspensionPoints.size() * sizeof(ICorDebugInfo::AsyncSuspensionPoint)));
std::copy(m_dbgSuspensionPoints.begin(), m_dbgSuspensionPoints.end(), hostSuspensionPoints);

ICorDebugInfo::AsyncContinuationVarInfo* hostVars =
static_cast<ICorDebugInfo::AsyncContinuationVarInfo*>(m_comp->info.compCompHnd->allocateArray(
m_dbgContinuationVars.size() * sizeof(ICorDebugInfo::AsyncContinuationVarInfo)));
std::copy(m_dbgContinuationVars.begin(), m_dbgContinuationVars.end(), hostVars);

m_comp->info.compCompHnd->reportAsyncDebugInfo(&asyncInfo, hostSuspensionPoints, hostVars,
static_cast<uint32_t>(m_dbgContinuationVars.size()));
}

//------------------------------------------------------------------------
// AsyncTransformation::GetDataArrayVar:
// Create a new local to hold the data array of the continuation object. This
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/jit/async.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class AsyncTransformation
BasicBlock* m_lastResumptionBB = nullptr;
BasicBlock* m_sharedReturnBB = nullptr;

// Debug info
jitstd::vector<ICorDebugInfo::AsyncContinuationVarInfo> m_dbgContinuationVars;
jitstd::vector<ICorDebugInfo::AsyncSuspensionPoint> m_dbgSuspensionPoints;

bool IsLive(unsigned lclNum);
void Transform(BasicBlock* block,
GenTreeCall* call,
Expand Down Expand Up @@ -132,6 +136,9 @@ class AsyncTransformation
GenTreeFlags indirFlags = GTF_IND_NONFAULTING);
GenTreeStoreInd* StoreAtOffset(GenTree* base, unsigned offset, GenTree* value, var_types storeType);

void CreateDebugInfoForSuspensionPoint(GenTreeCall* asyncCall, const ContinuationLayout& layout);
void ReportDebugInfo();

unsigned GetDataArrayVar();
unsigned GetGCDataArrayVar();
unsigned GetResultBaseVar();
Expand All @@ -147,6 +154,8 @@ class AsyncTransformation
: m_comp(comp)
, m_liveLocalsScratch(comp->getAllocator(CMK_Async))
, m_resumptionBBs(comp->getAllocator(CMK_Async))
, m_dbgContinuationVars(comp->getAllocator(CMK_Async))
, m_dbgSuspensionPoints(comp->getAllocator(CMK_Async))
{
}

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4601,7 +4601,7 @@ class Compiler
CORINFO_CALL_INFO* callInfo,
IL_OFFSET rawILOffset);

void impSetupAndSpillForAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned prefixFlags);
void impSetupAndSpillForAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned prefixFlags, const DebugInfo& callInstDI);

void impInsertAsyncContinuationForLdvirtftnCall(GenTreeCall* call);

Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/jit/gentree.h
Original file line number Diff line number Diff line change
Expand Up @@ -4400,6 +4400,9 @@ struct AsyncCallInfo
bool SaveAndRestoreSynchronizationContextField = false;
bool HasSuspensionIndicatorDef = false;
unsigned SynchronizationContextLclNum = BAD_VAR_NUM;

// Exact debug info of call IL instruction
DebugInfo DebugInfo;
};

// Return type descriptor of a GT_CALL node.
Expand Down
11 changes: 8 additions & 3 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ var_types Compiler::impImportCall(OPCODE opcode,

if (sig->isAsyncCall())
{
impSetupAndSpillForAsyncCall(call->AsCall(), opcode, prefixFlags);
impSetupAndSpillForAsyncCall(call->AsCall(), opcode, prefixFlags, di);
}

impPopCallArgs(sig, call->AsCall());
Expand Down Expand Up @@ -691,7 +691,7 @@ var_types Compiler::impImportCall(OPCODE opcode,

if (sig->isAsyncCall())
{
impSetupAndSpillForAsyncCall(call->AsCall(), opcode, prefixFlags);
impSetupAndSpillForAsyncCall(call->AsCall(), opcode, prefixFlags, di);
}

// Now create the argument list.
Expand Down Expand Up @@ -6800,10 +6800,15 @@ void Compiler::impCheckForPInvokeCall(
// call - The call
// opcode - The IL opcode for the call
// prefixFlags - Flags containing context handling information from IL
// callInstDI - Debug info for the exact call instruction
//
void Compiler::impSetupAndSpillForAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned prefixFlags)
void Compiler::impSetupAndSpillForAsyncCall(GenTreeCall* call,
OPCODE opcode,
unsigned prefixFlags,
const DebugInfo& callInstDI)
{
AsyncCallInfo asyncInfo;
asyncInfo.DebugInfo = callInstDI;

if ((prefixFlags & PREFIX_IS_TASK_AWAIT) != 0)
{
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3215,6 +3215,15 @@ private void reportRichMappings(InlineTreeNode* inlineTree, uint numInlineTree,
Marshal.FreeHGlobal((IntPtr)mappings);
}

#pragma warning disable CA1822 // Mark members as static
private void reportAsyncDebugInfo(AsyncInfo* asyncInfo, AsyncSuspensionPoint* suspensionPoints, AsyncContinuationVarInfo* vars, uint numVars)
#pragma warning restore CA1822 // Mark members as static
{
Marshal.FreeHGlobal((IntPtr)asyncInfo);
Marshal.FreeHGlobal((IntPtr)suspensionPoints);
Marshal.FreeHGlobal((IntPtr)vars);
}

#pragma warning disable CA1822 // Mark members as static
private void reportMetadata(byte* key, void* value, nuint length)
#pragma warning restore CA1822 // Mark members as static
Expand Down
Loading
Loading