Jitdump contains only 2 stub records and no methods on .NET 10 (Linux x64) — writer silently disabled after truncated stub-block record; regression from .NET 9
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
On Linux x64 with `DOTNET_PerfMapEnabled=2` (or `1`), .NET 10.0.11 produces a jitdump file (`/tmp/jit-.dump`) containing exactly 2 records — both startup stubs, the second truncated — and **no JIT_CODE_LOAD records for any managed method**. This breaks `perf inject --jit`, samply symbolication, and Intel PT decode of managed code. The perfmap text file (`PerfMapEnabled=1`) is unaffected and lists all methods.
I searched open issues for "jitdump" and found no existing report of this. **Upd:** it's #120419 which is closed, hence the miss.
### Reproduction Steps
```
dotnet new console -o hello && cd hello && dotnet build -c Release
DOTNET_PerfMapEnabled=2 ./bin/Release/net10.0/hello
ls -la /tmp/jit-*.dump
```
Count the records (any jitdump parser; quick check below):
```python
import struct
data = open("/tmp/jit-.dump","rb").read()
off, counts = 40, {}
while off + 16 <= len(data):
rid, tsz = struct.unpack("", 272 code bytes — intact;
2. `JIT_CODE_LOAD` "stub ReportStubBlock" whose header claims **65,536 code bytes**, but the file ends at 33,281 bytes — the record is **truncated mid-write**;
3. nothing further, ever, although the same run with `PerfMapEnabled=1` lists hundreds of methods in the perfmap.
### Regression?
Yes — bisected across shipped runtimes (same machine, same hello-world binary via `dotnet exec --fx-version`):
| Runtime | jitdump | Verdict |
|---|---|---|
| 8.0.30 | 540,982 B / 1,360 records | GOOD |
| 9.0.0 | 95,217 B / 291 records | GOOD |
| 9.0.19 | 93,836 B / 287 records | GOOD |
| 10.0.0-preview.1.25080.5 | 100,687 B / 308 records | GOOD |
| 10.0.0-preview.2.25163.2 | 99,666 B / 305 records | GOOD |
| **10.0.0-preview.3.25171.5** | 99,647 B / 305 records | **GOOD** |
| **10.0.0-preview.4.25258.110** | 33,251 B / 2 records | **BAD** |
| 10.0.0-preview.5 … rc.2 | 33,251–33,281 B / 2 records | BAD |
| 10.0.0 (GA), 10.0.11 | 33,281 B / 2 records | BAD |
The only commit touching `src/coreclr/vm/perfmap.cpp` in the preview.3→preview.4 window is **a45396c2 "Enhance logging of stubs through the PerfMap on Linux" (#113943)** — which introduced the stub-block logging that produces the truncated `ReportStubBlock` record observed above, and also introduced the `PerfMapStubGranularity` config used in the workaround below.
### Known Workarounds
`DOTNET_PerfMapStubGranularity=4` (disable stub logging) fully restores method logging: the same hello-world produces 93,364 bytes / 280 records; a JIT-heavy app produces 1,758 records including OSR/tier versions.
### Configuration
- .NET 10.0.11 (regression vs 8.0.30), installed via dotnet-install script
- Ubuntu 24.04 (kernel 6.8.0-138-generic), x86_64 (Xeon W-2295), bare metal
- Reproduces with and without `DOTNET_EnableWriteXorExecute=0`
### Other information
## Analysis (hypothesis, from reading release/10.0 sources)
- `PerfMap::LogStubs` (reached via `ReportStubBlock`) logs an entire stub code block *including its code bytes* through `PAL_PerfJitDump_LogMethod`, which `writev`s `{ &record, symbol, pCode/codeSize }` (`src/coreclr/pal/src/misc/perfjitdump.cpp`).
- The 64KB stub block appears not to be fully readable (reserved > committed?), so the gather write stops short and then fails — consistent with the truncated record observed.
- The failure path calls `FatalError()`, which sets `enabled = false` **permanently**; every subsequent method record is silently dropped. The perfmap path never includes code bytes, which is why it is unaffected.
Suggested directions (happy to discuss/test): treat a failed record as skippable rather than fatal (or at least log the failure once); and/or avoid including code bytes for block-granularity stub records, or validate readability of the range first.
Contributor guide
Assessment
This issue has not been assessed yet.