diff --git a/docs/design/coreclr/botr/r2r-perfmap-format.md b/docs/design/coreclr/botr/r2r-perfmap-format.md index 56516dd6c5ec34..88e65dfb2f1c71 100644 --- a/docs/design/coreclr/botr/r2r-perfmap-format.md +++ b/docs/design/coreclr/botr/r2r-perfmap-format.md @@ -36,6 +36,9 @@ PerfMapArchitectureToken ARM64 = 2, X64 = 3, X86 = 4, + RiscV64 = 5, + LoongArch64 = 6, + Wasm = 7, ``` ``` @@ -47,6 +50,9 @@ PerfMapOSToken FreeBSD = 4, NetBSD = 5, SunOS = 6, + OpenBSD = 7, + Browser = 8, + Wasi = 9, ``` ``` diff --git a/docs/design/mono/webcil.md b/docs/design/mono/webcil.md index 455139124b8dba..42c8dce54dd356 100644 --- a/docs/design/mono/webcil.md +++ b/docs/design/mono/webcil.md @@ -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 `.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.) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/OutputInfoBuilder.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/OutputInfoBuilder.cs index d3b62804d7efec..39ad6ba3059ae0 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/OutputInfoBuilder.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/OutputInfoBuilder.cs @@ -43,7 +43,7 @@ public int Compare([AllowNull] OutputItem x, [AllowNull] OutputItem y) /// /// Offset relative to section beginning /// - public readonly ulong Offset; + public ulong Offset { get; internal set; } /// /// Item name @@ -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. /// - public readonly int Length; + public int Length { get; internal set; } /// /// Number of file-level relocations (.reloc section entries) used by the node. @@ -164,6 +164,26 @@ public void AddMethod(IMethodNode method, ISymbolDefinitionNode symbol) _methodSymbolMap.Add(symbol, method); } + /// + /// 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. + /// + internal void RemapMethodNodeOffsets(int sectionIndex, IReadOnlyDictionary 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); diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs index 5867f3f3ae424c..a14c2ab4d89850 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs @@ -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]; @@ -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); } @@ -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 _codeOffsetMap = new(); + Dictionary> _resolvableRelocations = new(); SortedDictionary> _baseRelocMap = new(); // We group webcil relocs into 4kb blocks, similar to PE @@ -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)); @@ -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; @@ -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 newBlobs = ParseCodeBlobs(sectionStream); diff --git a/src/coreclr/tools/aot/ILCompiler.Diagnostics/PerfMapWriter.cs b/src/coreclr/tools/aot/ILCompiler.Diagnostics/PerfMapWriter.cs index a5774d7f37b8a6..86fdf190fd18cd 100644 --- a/src/coreclr/tools/aot/ILCompiler.Diagnostics/PerfMapWriter.cs +++ b/src/coreclr/tools/aot/ILCompiler.Diagnostics/PerfMapWriter.cs @@ -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()) }; @@ -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()) }; diff --git a/src/coreclr/tools/aot/ILCompiler.Diagnostics/ReadyToRunDiagnosticsConstants.cs b/src/coreclr/tools/aot/ILCompiler.Diagnostics/ReadyToRunDiagnosticsConstants.cs index 449b80add0a458..f03f85f26619de 100644 --- a/src/coreclr/tools/aot/ILCompiler.Diagnostics/ReadyToRunDiagnosticsConstants.cs +++ b/src/coreclr/tools/aot/ILCompiler.Diagnostics/ReadyToRunDiagnosticsConstants.cs @@ -21,6 +21,7 @@ public enum PerfMapArchitectureToken : uint X86 = 4, RiscV64 = 5, LoongArch64 = 6, + Wasm = 7, } public enum PerfMapOSToken : uint @@ -33,6 +34,8 @@ public enum PerfMapOSToken : uint NetBSD = 5, SunOS = 6, OpenBSD = 7, + Browser = 8, + Wasi = 9, } public enum PerfMapAbiToken : uint