Skip to content

Commit 49bb6a0

Browse files
lateralusXCopilot
andauthored
Make Async Profiler tests compatible with single threaded runtime. (#127762)
Initial async profiler tests used some techniques to isolate tests running on thread pool, but that doesn't work on single threaded platforms like WASM. * Split tests into tests that can run without multithreaded support and tests that must have thread pool. * Harden test to only look for specific events using a Task id mapping to the marker frame used by the test. * Fix failures on Native AOT due to missing native IP -> Method Name. * Remove wrapper IP's from metadata event. With JIT they point to the pre-stub, instead parsers needs rundown events and can create the wrapper IP table through that. The wrapper count is still in the metadata event, but the wrapper name is now part of the contract, so parsers can use rundown events to locate the IP's and build wrapper table. With these changes we now have all tests running on CoreCLR, NativeAOT and 38 out of 48 tests running on CoreCLR WASM single threaded configuration. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent ad815e7 commit 49bb6a0

3 files changed

Lines changed: 1211 additions & 913 deletions

File tree

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,12 @@ private static ulong GetId(ref AsyncDispatcherInfo info)
123123
/// through the wrapper at index (ContinuationIndex &amp; COUNT_MASK), then increments the index.
124124
///
125125
/// This creates a rotating pattern of unique return addresses on the native callstack. An OS
126-
/// CPU profiler (e.g., ETW, perf) captures these native IPs in its stack samples. The async
127-
/// profiler emits the wrapper IP table in the metadata event, so a post-processing tool can
128-
/// identify which wrapper IPs appear in a native callstack and correlate them with the
129-
/// async resume callstack events emitted at the same logical point. This bridges the gap
130-
/// between synchronous native stack samples and the asynchronous continuation chain.
126+
/// CPU profiler (e.g., ETW, perf) captures these native IPs in its stack samples. A post-processing
127+
/// tool uses the wrapper name template and count (defined by the async profiler contract) to
128+
/// format method names, resolve them via symbol data (rundown events), and correlate
129+
/// native stack IPs with the async resume callstack events emitted at the same logical point.
130+
/// This bridges the gap between synchronous native stack samples and the asynchronous
131+
/// continuation chain.
131132
///
132133
/// Every COUNT (32) continuations, a ResetAsyncContinuationWrapperIndex event is emitted
133134
/// so the tool knows the index has wrapped around and can correctly map subsequent samples.
@@ -138,6 +139,20 @@ private static ulong GetId(ref AsyncDispatcherInfo info)
138139
[StackTraceHidden]
139140
internal static partial class ContinuationWrapper
140141
{
142+
/// <summary>
143+
/// Name template for the continuation wrapper methods, defined by contract.
144+
/// External tools format this template with the wrapper index (0..COUNT-1) to produce
145+
/// method names for identifying wrapper frames in stacks.
146+
/// Must match the actual method names below (e.g., Continuation_Wrapper_0, Continuation_Wrapper_1, ...).
147+
/// </summary>
148+
public const string NameTemplate = "Continuation_Wrapper_{0}";
149+
150+
/// <summary>
151+
/// Number of distinct wrapper methods. The wrapper index rotates modulo this value.
152+
/// </summary>
153+
public const byte COUNT = 32;
154+
public const byte COUNT_MASK = COUNT - 1;
155+
141156
public static void InitInfo(ref Info info)
142157
{
143158
info.ContinuationTable = ref Unsafe.As<ContinuationWrapperTable, nint>(ref s_continuationWrappers);
@@ -154,16 +169,6 @@ public static void InitInfo(ref Info info)
154169
}
155170
}
156171

157-
public static long[] GetContinuationWrapperIPs()
158-
{
159-
long[] ips = new long[COUNT];
160-
for (int i = 0; i < COUNT; i++)
161-
{
162-
ips[i] = Unsafe.Add(ref Unsafe.As<ContinuationWrapperTable, nint>(ref s_continuationWrappers), i);
163-
}
164-
return ips;
165-
}
166-
167172
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
168173
private static unsafe Continuation? Continuation_Wrapper_0(Continuation continuation, ref byte resultLoc)
169174
{

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,37 +107,28 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context)
107107
{
108108
if (s_metadataRevision != Revision)
109109
{
110-
long[] wrapperIPs = ContinuationWrapper.GetContinuationWrapperIPs();
111-
112110
// Metadata payload:
113111
// [qpcFrequency (compressed uint64)]
114112
// [qpcSync (compressed uint64)]
115113
// [utcSync (compressed uint64)]
116114
// [eventBufferSize (compressed uint32)]
117115
// [wrapperCount byte]
118-
// [wrapperIP0 (compressed uint64)] ... [wrapperIPn (compressed uint64)]
119116
const int MaxStaticEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size + 1;
120-
int maxDynamicEventPayloadSize = wrapperIPs.Length * Serializer.MaxCompressedUInt64Size;
121117

122118
ref EventBuffer eventBuffer = ref context.EventBuffer;
123-
if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize))
119+
if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize))
124120
{
125121
SyncClock(out long utcTimeSync, out long qpcSync);
126122

127-
Span<byte> payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize);
123+
Span<byte> payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxStaticEventPayloadSize);
128124
int payloadSpanIndex = 0;
129125

130126
payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)Stopwatch.Frequency);
131127
payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)qpcSync);
132128
payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync);
133129
payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan.Slice(payloadSpanIndex), EventBufferSize);
134130

135-
payloadSpan[payloadSpanIndex++] = (byte)wrapperIPs.Length;
136-
137-
for (int i = 0; i < wrapperIPs.Length; i++)
138-
{
139-
payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)wrapperIPs[i]);
140-
}
131+
payloadSpan[payloadSpanIndex++] = ContinuationWrapper.COUNT;
141132

142133
eventBuffer.Index += payloadSpanIndex;
143134

@@ -857,9 +848,6 @@ public static void EmitEvent(AsyncThreadContext context)
857848

858849
internal static partial class ContinuationWrapper
859850
{
860-
public const byte COUNT = 32;
861-
public const byte COUNT_MASK = COUNT - 1;
862-
863851
[MethodImpl(MethodImplOptions.AggressiveInlining)]
864852
public static void IncrementIndex(ref Info info)
865853
{

0 commit comments

Comments
 (0)