Skip to content
Open
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 @@ -124,8 +124,9 @@ private static ulong GetId(ref AsyncDispatcherInfo info)
///
/// This creates a rotating pattern of unique return addresses on the native callstack. An OS
/// CPU profiler (e.g., ETW, perf) captures these native IPs in its stack samples. The async
/// profiler emits the wrapper IP table in the metadata event, so a post-processing tool can
/// identify which wrapper IPs appear in a native callstack and correlate them with the
/// profiler emits the wrapper name template and count in the metadata event, so a post-processing
/// tool can format the template with each index (0..COUNT-1) to produce method names, resolve
/// them via symbol data (rundown events or PDB), and correlate native stack IPs with the
/// async resume callstack events emitted at the same logical point. This bridges the gap
/// between synchronous native stack samples and the asynchronous continuation chain.
///
Expand All @@ -138,6 +139,21 @@ private static ulong GetId(ref AsyncDispatcherInfo info)
[StackTraceHidden]
internal static partial class ContinuationWrapper
{
/// <summary>
/// Name template for the continuation wrapper methods. External tools format this template
/// with the wrapper index (0..COUNT-1) to produce method names for identifying wrapper frames in stacks.
/// Must match the actual method names below (e.g., Continuation_Wrapper_0, Continuation_Wrapper_1, ...).
/// </summary>
public const string NameTemplate = "Continuation_Wrapper_{0}";

/// <summary>
/// Pre-encoded UTF8 bytes of <see cref="NameTemplate"/> for zero-allocation metadata emission.
/// </summary>
public static ReadOnlySpan<byte> NameTemplateUtf8 => "Continuation_Wrapper_{0}"u8;

public const byte COUNT = 32;
public const byte COUNT_MASK = COUNT - 1;

public static void InitInfo(ref Info info)
{
info.ContinuationTable = ref Unsafe.As<ContinuationWrapperTable, nint>(ref s_continuationWrappers);
Expand All @@ -154,16 +170,6 @@ public static void InitInfo(ref Info info)
}
}

public static long[] GetContinuationWrapperIPs()
{
long[] ips = new long[COUNT];
for (int i = 0; i < COUNT; i++)
{
ips[i] = Unsafe.Add(ref Unsafe.As<ContinuationWrapperTable, nint>(ref s_continuationWrappers), i);
}
return ips;
}

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
private static unsafe Continuation? Continuation_Wrapper_0(Continuation continuation, ref byte resultLoc)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,17 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context)
{
if (s_metadataRevision != Revision)
{
long[] wrapperIPs = ContinuationWrapper.GetContinuationWrapperIPs();

// Metadata payload:
// [qpcFrequency (compressed uint64)]
// [qpcSync (compressed uint64)]
// [utcSync (compressed uint64)]
// [eventBufferSize (compressed uint32)]
// [wrapperCount byte]
// [wrapperIP0 (compressed uint64)] ... [wrapperIPn (compressed uint64)]
const int MaxStaticEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size + 1;
int maxDynamicEventPayloadSize = wrapperIPs.Length * Serializer.MaxCompressedUInt64Size;
// [wrapperNameTemplateLength (compressed uint32)]
// [wrapperNameTemplate UTF8 bytes]
ReadOnlySpan<byte> templateBytes = ContinuationWrapper.NameTemplateUtf8;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you expect the names of the wrappers to change? Can the names of wrappers be part of the async profiler contract instead of sending a constant string in each metadata payload? It is common for diagnostic tools to have hardcoded managed type and method names.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Probably not, but added it to have the flexibility since it felt bad to enforce the assumption as part of the contract, the metadata event is only emitted once per profiler session, so cheap.

const int MaxStaticEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size + 1 + Serializer.MaxCompressedUInt32Size;
int maxDynamicEventPayloadSize = templateBytes.Length;
Comment thread
lateralusX marked this conversation as resolved.

ref EventBuffer eventBuffer = ref context.EventBuffer;
if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize))
Expand All @@ -132,12 +132,10 @@ public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context)
payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync);
payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan.Slice(payloadSpanIndex), EventBufferSize);

payloadSpan[payloadSpanIndex++] = (byte)wrapperIPs.Length;

for (int i = 0; i < wrapperIPs.Length; i++)
{
payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)wrapperIPs[i]);
}
payloadSpan[payloadSpanIndex++] = ContinuationWrapper.COUNT;
payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan.Slice(payloadSpanIndex), (uint)templateBytes.Length);
templateBytes.CopyTo(payloadSpan.Slice(payloadSpanIndex));
payloadSpanIndex += templateBytes.Length;

Comment thread
lateralusX marked this conversation as resolved.
Comment thread
lateralusX marked this conversation as resolved.
eventBuffer.Index += payloadSpanIndex;

Expand Down Expand Up @@ -857,9 +855,6 @@ public static void EmitEvent(AsyncThreadContext context)

internal static partial class ContinuationWrapper
{
public const byte COUNT = 32;
public const byte COUNT_MASK = COUNT - 1;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void IncrementIndex(ref Info info)
{
Expand Down
Loading
Loading