Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/design/coreclr/botr/r2r-perfmap-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ PerfMapArchitectureToken
ARM64 = 2,
X64 = 3,
X86 = 4,
RiscV64 = 5,
LoongArch64 = 6,
Wasm = 7,
```

```
Expand All @@ -47,6 +50,9 @@ PerfMapOSToken
FreeBSD = 4,
NetBSD = 5,
SunOS = 6,
OpenBSD = 7,
Browser = 8,
Wasi = 9,
```

```
Expand Down
48 changes: 48 additions & 0 deletions docs/design/mono/webcil.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,51 @@ struct IMAGE_BASE_RELOCATION {

Each 2 byte word following `IMAGE_BASE_RELOCATION` is decoded as a `uint16_t` where then lower 12 bits
indicate an offset from the `VirtualAddress` of the block, and the high 4 bits represents the relocation type.

## ReadyToRun perfmap offsets

When crossgen2 compiles an assembly to a ReadyToRun Webcil `.wasm` module (`--obj-format:wasm`) with
`--perfmap --perfmap-format-version:1`, it emits a sidecar `<assembly>.ni.r2rmap` text file for native
symbolication — the same artifact produced for PE/ELF/Mach-O R2R images. Only a small debug-directory
entry pointing at the sidecar is embedded in the module; the method table itself is never part of the
`.wasm` file.

Unlike the ECMA-335 metadata, IL and RVA-addressed data — which live in the Webcil *payload* (data
segment 1) — the R2R-compiled method bodies are emitted as **real WebAssembly functions in the module's
Code section**. The perfmap therefore keys each method by its position in that Code section rather than by
a payload RVA.

### Address space

Every method entry in the perfmap is a line of the form `RVA Length Name`, where:

* `RVA` is the byte offset, **from the start of the `.wasm` module**, of the method's function entry in
the Code section (that is, the entry's LEB128 body-size prefix). It is a file offset into the wasm
module — not a virtual address and not a Webcil payload RVA.
* `Length` is the size in bytes of that function entry (size prefix plus body).

Code entries are laid out contiguously, so a method's `RVA + Length` is the `RVA` of the next entry in the
Code section. The header pseudo-entries carry the target identity: OS token `8` (`Browser`) or `9`
(`Wasi`), architecture token `7` (`Wasm`), and perfmap format version `1`.

### Computing the offsets

The Code section is rewritten during final module emission: the object writer LEB128-shrinks each function
body's relocations to their minimal encoding, which changes both the entry's size prefix and the position
of every function that follows it. The offset captured when a method is first written is therefore a
*pre-shrink* content offset and does not match the final module layout.

To produce correct addresses the writer:

1. records the file offset at which the Code section's function-body content begins in the final module
(after the section id byte, the section size prefix, and the function-count prefix), and
2. remaps each method's pre-shrink entry offset — and the offset of its end — to the corresponding
post-shrink offset, so the reported `RVA` and `Length` reflect the shrunk on-disk layout.

Adding the recorded Code-section file offset to a method's post-shrink entry offset yields the `RVA` written
to the perfmap.

(**Rationale**: keying on the wasm Code-section file offset lets native profilers and tools such as
`dotnet-trace` and PerfView correlate the offsets a WebAssembly engine reports for executing frames with
managed method names — the same role the perfmap plays for native R2R code on other platforms. A payload
RVA would instead point into the metadata image, which is not what executes.)
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public int Compare([AllowNull] OutputItem x, [AllowNull] OutputItem y)
/// <summary>
/// Offset relative to section beginning
/// </summary>
public readonly ulong Offset;
public ulong Offset { get; internal set; }

/// <summary>
/// Item name
Expand All @@ -67,7 +67,7 @@ public sealed class OutputNode : OutputItem
/// Node length (number of bytes). This doesn't include any external alignment
/// applied when concatenating the nodes to form sections.
/// </summary>
public readonly int Length;
public int Length { get; internal set; }

/// <summary>
/// Number of file-level relocations (.reloc section entries) used by the node.
Expand Down Expand Up @@ -164,6 +164,26 @@ public void AddMethod(IMethodNode method, ISymbolDefinitionNode symbol)
_methodSymbolMap.Add(symbol, method);
}

/// <summary>
/// Corrects code-section node offsets and lengths after the wasm writer LEB128-shrinks code
/// relocations during final emission. The map gives the final on-disk content offset for each
/// pre-shrink entry boundary, so a method node's start and end (which both fall on entry
/// boundaries) resolve to its real position and length in the emitted module for the perfmap.
/// </summary>
internal void RemapMethodNodeOffsets(int sectionIndex, IReadOnlyDictionary<ulong, ulong> offsetMap)
{
foreach (OutputNode node in _nodes)
{
if (node.SectionIndex == sectionIndex
&& offsetMap.TryGetValue(node.Offset, out ulong postStart)
&& offsetMap.TryGetValue(node.Offset + (ulong)node.Length, out ulong postEnd))
{
node.Length = (int)(postEnd - postStart);
node.Offset = postStart;
}
}
}

public void Sort()
{
_nodes.Sort(OutputItem.Comparer.Instance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ private protected override void EmitObjectFile(Stream outputFileStream)
*********************************************************************/

EmitWasmHeader(outputFileStream);
int codeSectionIndex = _sections.Contains(ObjectNodeSection.WasmCodeSection.Name)
? _sections.GetSectionIndex(ObjectNodeSection.WasmCodeSection.Name)
: -1;
long codeContentFileOffset = 0;
foreach (int index in SectionEmitOrder)
{
SectionDataEmitter section = _sections[index];
Expand All @@ -388,6 +392,12 @@ private protected override void EmitObjectFile(Stream outputFileStream)
}
}

if (index == codeSectionIndex)
{
// Function bodies begin after the section header and the (externally counted) entry-count prefix.
codeContentFileOffset = outputFileStream.Position + (section.EncodeSize() - section.ContentReadStream.Length);
}

section.EmitToStream(outputFileStream);
}

Expand Down Expand Up @@ -452,8 +462,32 @@ private protected override void EmitObjectFile(Stream outputFileStream)
WasmDataSection dataSection = new WasmDataSection([webcilSizeSegment, webcilContentsSegment], new Utf8String("data"), contentAlign: 4);
dataSection.EmitToStream(outputFileStream);
#endif

if (_outputInfoBuilder is not null)
{
// Populate the output section layout so OutputInfoBuilder.EnumerateMethods can resolve each
// method node's section. The list is index-aligned with the wasm section table; only the
// code section (which holds the method bodies) needs a real file offset for the perfmap.
for (int i = 0; i < _sections.Count; i++)
{
SectionDataEmitter emittedSection = _sections[i];
ulong fileOffset = (i == codeSectionIndex) ? (ulong)codeContentFileOffset : 0;
_outputSectionLayout.Add(new OutputSection(
emittedSection.SectionName.ToString(), fileOffset, fileOffset, (ulong)emittedSection.ContentReadStream.Length));
}

if (codeSectionIndex >= 0)
{
_outputInfoBuilder.RemapMethodNodeOffsets(codeSectionIndex, _codeOffsetMap);
}
}
}

// Maps each code-section entry boundary's pre-shrink content offset to its final (post-shrink)
// offset, populated during ResolveCodeRelocations so method node offsets and lengths can be
// corrected for the R2R perfmap.
private readonly Dictionary<ulong, ulong> _codeOffsetMap = new();

Dictionary<int, List<SymbolicRelocation>> _resolvableRelocations = new();
SortedDictionary<uint, List<ushort>> _baseRelocMap = new();
// We group webcil relocs into 4kb blocks, similar to PE
Expand Down Expand Up @@ -568,6 +602,8 @@ private void ResolveCodeRelocations(int sectionIndex, MemoryStream sectionStream
MemoryStream tempStream = new MemoryStream((int)maxBlobSize);
byte[] relocScratchBuffer = new byte[Relocation.MaxSize];
int[] blobShrink = new int[blobs.Count];
// Post-shrink content offset of each blob's entry (size prefix), used to correct perfmap offsets.
long[] postEntryStart = new long[blobs.Count];

blobs.Sort((a, b) => a.Start.CompareTo(b.Start));
relocs.Sort((a, b) => a.Offset.CompareTo(b.Offset));
Expand All @@ -581,6 +617,8 @@ private void ResolveCodeRelocations(int sectionIndex, MemoryStream sectionStream
for (int b = 0; b < blobs.Count; b++)
{
CodeBlob blob = blobs[b];
// writeCursor is the post-shrink offset where this entry's (new) size prefix will be written.
postEntryStart[b] = writeCursor;
Debug.Assert(writeCursor <= blobs[b].Start, $"Write cursor {writeCursor} is beyond the start of blob {blobs[b].Start}");

bool hasRelocs = relocCursor < relocs.Count && relocs[relocCursor].Offset >= blob.Start && relocs[relocCursor].Offset < blob.End;
Expand Down Expand Up @@ -656,6 +694,21 @@ private void ResolveCodeRelocations(int sectionIndex, MemoryStream sectionStream

sectionStream.Position = 0;

if (_outputInfoBuilder is not null)
{
// Map each entry boundary's pre-shrink content offset (node offsets and lengths land on
// these boundaries) to its final post-shrink offset so the R2R perfmap points at the
// method's real position and length.
for (int b = 0; b < blobs.Count; b++)
{
long preEntryStart = (b == 0) ? 0 : blobs[b - 1].End;
_codeOffsetMap[(ulong)preEntryStart] = (ulong)postEntryStart[b];
}
// Map final boundary (end of the last entry) so the `End` offset of a node ending the section resolves to the post-shrunk end.
long preTotalLength = blobs.Count > 0 ? blobs[blobs.Count - 1].End : 0;
_codeOffsetMap[(ulong)preTotalLength] = (ulong)writeCursor;
}

#if DEBUG
// The number of code blobs should not have changed.
List<CodeBlob> newBlobs = ParseCodeBlobs(sectionStream);
Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/tools/aot/ILCompiler.Diagnostics/PerfMapWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ private static PerfmapTokensForTarget TranslateTargetDetailsToPerfmapConstants(T
TargetOS.NetBSD => PerfMapOSToken.NetBSD,
TargetOS.OpenBSD => PerfMapOSToken.OpenBSD,
TargetOS.SunOS => PerfMapOSToken.SunOS,
TargetOS.Browser => PerfMapOSToken.Browser,
TargetOS.Wasi => PerfMapOSToken.Wasi,
_ => throw new NotImplementedException(details.OperatingSystem.ToString())
};

Expand All @@ -138,6 +140,7 @@ private static PerfmapTokensForTarget TranslateTargetDetailsToPerfmapConstants(T
TargetArchitecture.X86 => PerfMapArchitectureToken.X86,
TargetArchitecture.RiscV64 => PerfMapArchitectureToken.RiscV64,
TargetArchitecture.LoongArch64 => PerfMapArchitectureToken.LoongArch64,
TargetArchitecture.Wasm32 => PerfMapArchitectureToken.Wasm,
_ => throw new NotImplementedException(details.Architecture.ToString())
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum PerfMapArchitectureToken : uint
X86 = 4,
RiscV64 = 5,
LoongArch64 = 6,
Wasm = 7,
}
Comment thread
pavelsavara marked this conversation as resolved.

public enum PerfMapOSToken : uint
Expand All @@ -33,6 +34,8 @@ public enum PerfMapOSToken : uint
NetBSD = 5,
SunOS = 6,
OpenBSD = 7,
Browser = 8,
Wasi = 9,
}

public enum PerfMapAbiToken : uint
Expand Down