From ffe226820813af77c38963260c9adb569e0e3e33 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 14:22:56 -0700 Subject: [PATCH 1/8] Add coreclr-logging-test skill for running tests with CLR logging Add a new Copilot skill that guides running any coreclr or libraries test with the CoreCLR diagnostic logging subsystem enabled. The skill documents all available log facilities (LF_*), extended facilities (LF2_*), log levels, and output control environment variables, and provides step-by-step instructions for capturing log files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/coreclr-logging-test/SKILL.md | 234 +++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 .github/skills/coreclr-logging-test/SKILL.md diff --git a/.github/skills/coreclr-logging-test/SKILL.md b/.github/skills/coreclr-logging-test/SKILL.md new file mode 100644 index 00000000000000..bd230829f6a317 --- /dev/null +++ b/.github/skills/coreclr-logging-test/SKILL.md @@ -0,0 +1,234 @@ +--- +name: coreclr-logging-test +description: > + Run any coreclr or libraries test with CoreCLR diagnostic logging enabled, + producing log files for analysis. Use when asked to "run a test with CLR + logging", "enable CoreCLR logs", "capture runtime logs", "debug with LOG + facility", or "get diagnostic output from the runtime". Helps diagnose + runtime issues by enabling the built-in CLR logging infrastructure. +--- + +# Running Tests with CoreCLR Logging Enabled + +Run any test (coreclr or libraries) with the CoreCLR diagnostic logging subsystem enabled, producing text log files for analysis. + +> 🚨 **IMPORTANT**: CoreCLR logging is only available in **Debug** or **Checked** builds of the runtime. It is compiled out of Release builds. You must use a Debug or Checked CoreCLR (`-rc debug` or `-rc checked`). If the user has not already built a Debug/Checked runtime, build one first. + +## Background + +The CoreCLR runtime has a built-in logging infrastructure controlled by environment variables. In code, the `LOG((facility, level, fmt, ...))` and `LOG2((facility2, level, fmt, ...))` macros generate log output. These are defined in `src/coreclr/inc/log.h` and the facilities are defined in `src/coreclr/inc/loglf.h`. + +## Step 1: Determine What to Log + +Ask the user (if not already specified) which subsystems they want to log. The logging is controlled by two bitmask environment variables: + +### LogFacility (DOTNET_LogFacility) — Primary Facilities + +These are hex bitmask values that can be OR'd together: + +| Facility | Hex Value | Description | +|---|---|---| +| `LF_GC` | `0x00000001` | Garbage collector | +| `LF_GCINFO` | `0x00000002` | GC info encoding | +| `LF_STUBS` | `0x00000004` | Stub code generation | +| `LF_JIT` | `0x00000008` | JIT compiler | +| `LF_LOADER` | `0x00000010` | Assembly/module loader | +| `LF_METADATA` | `0x00000020` | Metadata operations | +| `LF_SYNC` | `0x00000040` | Synchronization primitives | +| `LF_EEMEM` | `0x00000080` | Execution engine memory | +| `LF_GCALLOC` | `0x00000100` | GC allocation tracking | +| `LF_CORDB` | `0x00000200` | Debugger (managed debugging) | +| `LF_CLASSLOADER` | `0x00000400` | Class/type loader | +| `LF_CORPROF` | `0x00000800` | Profiling API | +| `LF_DIAGNOSTICS_PORT` | `0x00001000` | Diagnostics port | +| `LF_DBGALLOC` | `0x00002000` | Debug allocator | +| `LF_EH` | `0x00004000` | Exception handling | +| `LF_ENC` | `0x00008000` | Edit and Continue | +| `LF_ASSERT` | `0x00010000` | Assertions | +| `LF_VERIFIER` | `0x00020000` | IL verifier | +| `LF_THREADPOOL` | `0x00040000` | Thread pool | +| `LF_GCROOTS` | `0x00080000` | GC root tracking | +| `LF_INTEROP` | `0x00100000` | Interop / P/Invoke | +| `LF_MARSHALER` | `0x00200000` | Data marshalling | +| `LF_TIEREDCOMPILATION` | `0x00400000` | Tiered compilation | +| `LF_ZAP` | `0x00800000` | Native images (R2R) | +| `LF_STARTUP` | `0x01000000` | Startup / shutdown | +| `LF_APPDOMAIN` | `0x02000000` | AppDomain | +| `LF_CODESHARING` | `0x04000000` | Code sharing | +| `LF_STORE` | `0x08000000` | Storage | +| `LF_SECURITY` | `0x10000000` | Security | +| `LF_LOCKS` | `0x20000000` | Lock operations | +| `LF_BCL` | `0x40000000` | Base Class Library | +| `LF_ALWAYS` | `0x80000000` | Always log (level-gated only) | + +To log everything, use `0xFFFFFFFF`. + +### LogFacility2 (DOTNET_LogFacility2) — Extended Facilities + +| Facility | Hex Value | Description | +|---|---|---| +| `LF2_MULTICOREJIT` | `0x00000001` | Multicore JIT | +| `LF2_INTERPRETER` | `0x00000002` | Interpreter | + +### LogLevel (DOTNET_LogLevel) — Verbosity + +| Level | Value | Expected Volume | +|---|---|---| +| `LL_ALWAYS` | `0` | Essential messages only | +| `LL_FATALERROR` | `1` | Fatal errors | +| `LL_ERROR` | `2` | Errors | +| `LL_WARNING` | `3` | Warnings | +| `LL_INFO10` | `4` | ~10 messages per run | +| `LL_INFO100` | `5` | ~100 messages per run | +| `LL_INFO1000` | `6` | ~1,000 messages per run | +| `LL_INFO10000` | `7` | ~10,000 messages per run | +| `LL_INFO100000` | `8` | ~100,000 messages per run | +| `LL_INFO1000000` | `9` | ~1,000,000 messages per run | +| `LL_EVERYTHING` | `10` | All messages | + +**Default recommendation**: Start with level `6` (LL_INFO1000) for a manageable amount of output. Use `10` only if you need exhaustive detail and are prepared for very large log files. + +## Step 2: Prepare the Log Output Directory + +Create a directory to hold the log output files: + +```powershell +$logDir = Join-Path (Get-Location) "clr-logs" +New-Item -ItemType Directory -Path $logDir -Force | Out-Null +``` + +## Step 3: Set Environment Variables and Run the Test + +Set the following environment variables before running the test. All logging knobs use the `DOTNET_` prefix. + +### Required Variables + +| Variable | Description | +|---|---| +| `DOTNET_LogEnable` | Set to `1` to enable logging | +| `DOTNET_LogFacility` | Hex bitmask of primary facilities to log | +| `DOTNET_LogLevel` | Verbosity level (0–10) | + +### Output Control Variables + +| Variable | Description | +|---|---| +| `DOTNET_LogToFile` | Set to `1` to write logs to a file | +| `DOTNET_LogFile` | Path to the log file (e.g., `C:\clr-logs\clr.log`) | +| `DOTNET_LogWithPid` | Set to `1` to append PID to filename (useful for multi-process) | +| `DOTNET_LogFlushFile` | Set to `1` to flush on each write (slower but crash-safe) | +| `DOTNET_LogToConsole` | Set to `1` to also write logs to console | +| `DOTNET_LogFileAppend` | Set to `1` to append to existing log file instead of overwriting | + +### Optional Extended Facilities + +| Variable | Description | +|---|---| +| `DOTNET_LogFacility2` | Hex bitmask for extended facilities (LF2_*) | + +### Running a Libraries Test + +```powershell +$logDir = Join-Path (Get-Location) "clr-logs" +New-Item -ItemType Directory -Path $logDir -Force | Out-Null + +$env:DOTNET_LogEnable = "1" +$env:DOTNET_LogFacility = "0x00000008" # LF_JIT — adjust as needed +$env:DOTNET_LogLevel = "6" # LL_INFO1000 +$env:DOTNET_LogToFile = "1" +$env:DOTNET_LogFile = Join-Path $logDir "clr.log" +$env:DOTNET_LogWithPid = "1" +$env:DOTNET_LogFlushFile = "1" + +# Run the test (example — adapt the path to the specific library) +dotnet build /t:Test src\libraries\\tests\.Tests.csproj + +# Clean up environment variables after the test +Remove-Item Env:\DOTNET_LogEnable +Remove-Item Env:\DOTNET_LogFacility +Remove-Item Env:\DOTNET_LogLevel +Remove-Item Env:\DOTNET_LogToFile +Remove-Item Env:\DOTNET_LogFile +Remove-Item Env:\DOTNET_LogWithPid +Remove-Item Env:\DOTNET_LogFlushFile +``` + +### Running a CoreCLR Test + +For CoreCLR tests, run the test executable directly with the environment variables set: + +```powershell +$logDir = Join-Path (Get-Location) "clr-logs" +New-Item -ItemType Directory -Path $logDir -Force | Out-Null + +$env:DOTNET_LogEnable = "1" +$env:DOTNET_LogFacility = "0x00000008" # LF_JIT — adjust as needed +$env:DOTNET_LogLevel = "6" # LL_INFO1000 +$env:DOTNET_LogToFile = "1" +$env:DOTNET_LogFile = Join-Path $logDir "clr.log" +$env:DOTNET_LogWithPid = "1" +$env:DOTNET_LogFlushFile = "1" + +# Build the test if needed +src\tests\build.cmd x64 checked tree JIT\Regression\JitBlue\Runtime_99391 + +# Run it via the generated runner script or directly +& "artifacts\tests\coreclr\windows.x64.Checked\JIT\Regression\JitBlue\Runtime_99391\Runtime_99391.cmd" + +# Clean up environment variables after the test +Remove-Item Env:\DOTNET_LogEnable +Remove-Item Env:\DOTNET_LogFacility +Remove-Item Env:\DOTNET_LogLevel +Remove-Item Env:\DOTNET_LogToFile +Remove-Item Env:\DOTNET_LogFile +Remove-Item Env:\DOTNET_LogWithPid +Remove-Item Env:\DOTNET_LogFlushFile +``` + +## Step 4: Examine the Log Output + +After the test completes, log files will be in the `$logDir` directory. When `DOTNET_LogWithPid=1`, each process produces a separate log file with the PID appended to the filename (e.g., `clr.log.1234`). + +```powershell +# List log files +Get-ChildItem $logDir + +# View the tail of a log file +Get-Content (Get-ChildItem $logDir -Filter "clr.log*" | Select-Object -First 1).FullName -Tail 100 + +# Search for specific content +Select-String -Path "$logDir\clr.log*" -Pattern "keyword" +``` + +## Common Facility Combinations + +Here are useful pre-built facility masks for common debugging scenarios: + +| Scenario | LogFacility | LogFacility2 | Description | +|---|---|---|---| +| JIT debugging | `0x00000008` | — | JIT compiler only | +| GC investigation | `0x00080103` | — | GC + GCINFO + GCALLOC + GCROOTS | +| Class loading | `0x00000410` | — | LOADER + CLASSLOADER | +| Exception handling | `0x00004000` | — | EH only | +| Interop / marshalling | `0x00300000` | — | INTEROP + MARSHALER | +| Tiered compilation | `0x00400008` | — | TIEREDCOMPILATION + JIT | +| Startup issues | `0x01000000` | — | STARTUP | +| Thread pool | `0x00040000` | — | THREADPOOL | +| Everything | `0xFFFFFFFF` | `0xFFFFFFFF` | All facilities (very verbose!) | + +## Step 5: Clean Up + +After analysis, remove the log directory to avoid consuming disk space: + +```powershell +Remove-Item -Recurse -Force $logDir +``` + +## Tips + +- **Start narrow**: Begin with a specific facility and low log level, then widen if needed. Logging everything at level 10 can produce gigabytes of output. +- **Use `LogFlushFile=1`**: If the runtime crashes during the test, unflushed logs are lost. Enable flush for crash investigations. +- **Use `LogWithPid=1`**: Always recommended. Tests may spawn child processes, and separate files per process make analysis easier. +- **Checked builds are preferred**: They include both logging and runtime assertions, giving the most diagnostic information without the full performance cost of Debug builds. +- **Libraries tests need a Checked runtime**: Build with `build.cmd clr -rc checked` (or `build.sh clr -rc checked`) so the runtime used to execute library tests has logging compiled in. +- **Log file location**: Use an absolute path for `DOTNET_LogFile` to ensure logs land in a predictable location regardless of working directory changes during test execution. From b65c47fef25fea4f2f249bbab8ebae5a331d64c9 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 16:09:15 -0700 Subject: [PATCH 2/8] Add LOG calls for vtable slot updates during tiered compilation Add logging to the vtable slot backpatching code path so that each entry point update is visible in CLR logs when LF_TIEREDCOMPILATION is enabled. The new LOG calls cover: - MethodDesc::SetCodeEntryPoint: logs which path is taken (BackpatchSlots, Precode, StableEntryPoint) - MethodDesc::TryBackpatchEntryPointSlots: logs entry point transitions and early-exit reasons - MethodDescBackpatchInfoTracker::Backpatch_Locked: logs the method and count of slots backpatched - EntryPointSlots::Backpatch_Locked: logs each individual slot write with slot type (Normal, Vtable, Executable, ExecutableRel32) All logging uses LF_TIEREDCOMPILATION at LL_INFO10000, matching existing tiered compilation log patterns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/method.cpp | 21 +++++++++++++++++++++ src/coreclr/vm/methoddescbackpatchinfo.cpp | 17 ++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 6fe925b719bb5f..75bec43916f40d 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -3184,14 +3184,25 @@ FORCEINLINE bool MethodDesc::TryBackpatchEntryPointSlots( PCODE previousEntryPoint = GetEntryPointToBackpatch_Locked(); if (previousEntryPoint == entryPoint) { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "MethodDesc::TryBackpatchEntryPointSlots pMD=%p (%s::%s) entryPoint=" FMT_ADDR " - no change (same as previous)\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); return true; } if (onlyFromPrestubEntryPoint && previousEntryPoint != GetPrestubEntryPointToBackpatch()) { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "MethodDesc::TryBackpatchEntryPointSlots pMD=%p (%s::%s) entryPoint=" FMT_ADDR " - skipped (not from prestub)\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); return false; } + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "MethodDesc::TryBackpatchEntryPointSlots pMD=%p (%s::%s) entryPoint=" FMT_ADDR " previousEntryPoint=" FMT_ADDR " isVtableBackpatch=%d\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint), DBG_ADDR(previousEntryPoint), + IsVersionableWithVtableSlotBackpatch())); + if (IsVersionableWithVtableSlotBackpatch()) { // Backpatch the func ptr stub if it was created @@ -3250,11 +3261,15 @@ void MethodDesc::SetCodeEntryPoint(PCODE entryPoint) _ASSERTE(entryPoint != (PCODE)NULL); #ifdef FEATURE_PORTABLE_ENTRYPOINTS + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, "MethodDesc::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR " path=StableEntryPoint(portable)\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); SetStableEntryPointInterlocked(entryPoint); #else // !FEATURE_PORTABLE_ENTRYPOINTS if (MayHaveEntryPointSlotsToBackpatch()) { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, "MethodDesc::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR " path=BackpatchSlots\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); BackpatchEntryPointSlots(entryPoint); return; } @@ -3262,6 +3277,8 @@ void MethodDesc::SetCodeEntryPoint(PCODE entryPoint) if (IsVersionable()) { _ASSERTE(IsVersionableWithPrecode()); + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, "MethodDesc::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR " path=Precode(versionable)\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); GetOrCreatePrecode()->SetTargetInterlocked(entryPoint, FALSE /* fOnlyRedirectFromPrestub */); // SetTargetInterlocked() would return false if it lost the race with another thread. That is fine, this thread @@ -3275,12 +3292,16 @@ void MethodDesc::SetCodeEntryPoint(PCODE entryPoint) // Use this path if there already exists a Precode, OR if RequiresStableEntryPoint is set. // // RequiresStableEntryPoint currently requires that the entrypoint must be a Precode + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, "MethodDesc::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR " path=Precode(stable)\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); GetOrCreatePrecode()->SetTargetInterlocked(entryPoint); return; } if (!HasStableEntryPoint()) { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, "MethodDesc::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR " path=StableEntryPoint\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint))); SetStableEntryPointInterlocked(entryPoint); return; } diff --git a/src/coreclr/vm/methoddescbackpatchinfo.cpp b/src/coreclr/vm/methoddescbackpatchinfo.cpp index 13ca493a64fcf9..f58b876353c5ce 100644 --- a/src/coreclr/vm/methoddescbackpatchinfo.cpp +++ b/src/coreclr/vm/methoddescbackpatchinfo.cpp @@ -25,6 +25,13 @@ void EntryPointSlots::Backpatch_Locked(TADDR slot, SlotType slotType, PCODE entr _ASSERTE(entryPoint != (PCODE)NULL); _ASSERTE(IS_ALIGNED((SIZE_T)slot, GetRequiredSlotAlignment(slotType))); + static const char *slotTypeNames[] = { "Normal", "Vtable", "Executable", "ExecutableRel32" }; + static_assert(ARRAY_SIZE(slotTypeNames) == SlotType_Count, "Update slotTypeNames when adding new SlotTypes"); + + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "EntryPointSlots::Backpatch_Locked slot=" FMT_ADDR " slotType=%s entryPoint=" FMT_ADDR "\n", + DBG_ADDR(slot), slotTypeNames[slotType], DBG_ADDR(entryPoint))); + switch (slotType) { case SlotType_Normal: @@ -78,7 +85,9 @@ void MethodDescBackpatchInfoTracker::Backpatch_Locked(MethodDesc *pMethodDesc, P _ASSERTE(pMethodDesc != nullptr); bool fReadyToPatchExecutableCode = false; - auto lambda = [&entryPoint, &fReadyToPatchExecutableCode](LoaderAllocator *pLoaderAllocatorOfSlot, MethodDesc *pMethodDesc, UINT_PTR slotData) + DWORD slotCount = 0; + + auto lambda = [&entryPoint, &fReadyToPatchExecutableCode, &slotCount](LoaderAllocator *pLoaderAllocatorOfSlot, MethodDesc *pMethodDesc, UINT_PTR slotData) { TADDR slot; @@ -99,11 +108,17 @@ void MethodDescBackpatchInfoTracker::Backpatch_Locked(MethodDesc *pMethodDesc, P fReadyToPatchExecutableCode = true; } EntryPointSlots::Backpatch_Locked(slot, slotType, entryPoint); + slotCount++; return true; // Keep walking }; m_backpatchInfoHash.VisitValuesOfKey(pMethodDesc, lambda); + + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "MethodDescBackpatchInfoTracker::Backpatch_Locked pMD=%p (%s::%s) entryPoint=" FMT_ADDR " slots=%u\n", + pMethodDesc, pMethodDesc->m_pszDebugClassName, pMethodDesc->m_pszDebugMethodName, + DBG_ADDR(entryPoint), slotCount)); } void MethodDescBackpatchInfoTracker::AddSlotAndPatch_Locked(MethodDesc *pMethodDesc, LoaderAllocator *pLoaderAllocatorOfSlot, TADDR slot, EntryPointSlots::SlotType slotType, PCODE currentEntryPoint) From 03f68ef9ad9580134f6617eb0edc6ca3101b547a Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 16:12:17 -0700 Subject: [PATCH 3/8] Reapply "Eliminate forwarder stubs by reusing method precodes for call counting indirection" (#125285) This reverts commit 5db8084c9d7bb9f50ce2457d56df626d051a5e40. --- src/coreclr/vm/callcounting.cpp | 171 ++++++++++++++------------------ src/coreclr/vm/callcounting.h | 41 +++----- src/coreclr/vm/method.cpp | 2 +- src/coreclr/vm/prestub.cpp | 17 +++- 4 files changed, 100 insertions(+), 131 deletions(-) diff --git a/src/coreclr/vm/callcounting.cpp b/src/coreclr/vm/callcounting.cpp index dea3750dea0aef..adef9ca678b97c 100644 --- a/src/coreclr/vm/callcounting.cpp +++ b/src/coreclr/vm/callcounting.cpp @@ -400,29 +400,6 @@ void CallCountingManager::CallCountingStubAllocator::EnumerateHeapRanges(CLRData #endif // DACCESS_COMPILE -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// CallCountingManager::MethodDescForwarderStubHashTraits - -CallCountingManager::MethodDescForwarderStubHashTraits::key_t -CallCountingManager::MethodDescForwarderStubHashTraits::GetKey(const element_t &e) -{ - WRAPPER_NO_CONTRACT; - return e->GetMethodDesc(); -} - -BOOL CallCountingManager::MethodDescForwarderStubHashTraits::Equals(const key_t &k1, const key_t &k2) -{ - WRAPPER_NO_CONTRACT; - return k1 == k2; -} - -CallCountingManager::MethodDescForwarderStubHashTraits::count_t -CallCountingManager::MethodDescForwarderStubHashTraits::Hash(const key_t &k) -{ - WRAPPER_NO_CONTRACT; - return (count_t)(size_t)k; -} - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingManagerHashTraits @@ -618,6 +595,14 @@ bool CallCountingManager::SetCodeEntryPoint( // Call counting is disabled, complete, or pending completion. The pending completion stage here would be // relatively rare, let it be handled elsewhere. methodDesc->SetCodeEntryPoint(codeEntryPoint); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + // Reset the precode target to prestub. For backpatchable methods, the precode must always + // point to prestub (not native code) so that new vtable slots flow through DoBackpatch() + // for discovery and recording. SetCodeEntryPoint() above handles recorded slots via + // BackpatchEntryPointSlots() without touching the precode. + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } return true; } @@ -656,6 +641,12 @@ bool CallCountingManager::SetCodeEntryPoint( ->AsyncPromoteToTier1(activeCodeVersion, createTieringBackgroundWorkerRef); } methodDesc->SetCodeEntryPoint(codeEntryPoint); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + // Reset the precode target to prestub so that new vtable slots flow through DoBackpatch() + // for discovery and recording. The call counting stub is no longer needed. + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); return true; } while (false); @@ -718,51 +709,41 @@ bool CallCountingManager::SetCodeEntryPoint( PCODE callCountingCodeEntryPoint = callCountingStub->GetEntryPoint(); if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // The call counting stub should not be the entry point that is called first in the process of a call - // - Stubs should be deletable. Many methods will have call counting stubs associated with them, and although the memory - // involved is typically insignificant compared to the average memory overhead per method, by steady-state it would - // otherwise be unnecessary memory overhead serving no purpose. - // - In order to be able to delete a stub, the jitted code of a method cannot be allowed to load the stub as the entry - // point of a callee into a register in a GC-safe point that allows for the stub to be deleted before the register is - // reused to call the stub. On some processor architectures, perhaps the JIT can guarantee that it would not load the - // entry point into a register before the call, but this is not possible on arm32 or arm64. Rather, perhaps the - // region containing the load and call would not be considered GC-safe. Calls are considered GC-safe points, and this - // may cause many methods that are currently fully interruptible to have to be partially interruptible and record - // extra GC info instead. This would be nontrivial and there would be tradeoffs. - // - For any method that may have an entry point slot that would be backpatched with the call counting stub's entry - // point, a small forwarder stub (precode) is created. The forwarder stub has loader allocator lifetime and forwards to - // the larger call counting stub. This is a simple solution for now and seems to have negligible impact. - // - Reusing FuncPtrStubs was considered. FuncPtrStubs are currently not used as a code entry point for a virtual or - // interface method and may be bypassed. For example, a call may call through the vtable slot, or a devirtualized call - // may call through a FuncPtrStub. The target of a FuncPtrStub is a code entry point and is backpatched when a - // method's active code entry point changes. Mixing the current use of FuncPtrStubs with the use as a forwarder for - // call counting does not seem trivial and would likely complicate its use. There may not be much gain in reusing - // FuncPtrStubs, as typically, they are created for only a small percentage of virtual/interface methods. - - MethodDescForwarderStubHash &methodDescForwarderStubHash = callCountingManager->m_methodDescForwarderStubHash; - Precode *forwarderStub = methodDescForwarderStubHash.Lookup(methodDesc); - if (forwarderStub == nullptr) - { - AllocMemTracker forwarderStubAllocationTracker; - forwarderStub = - Precode::Allocate( - methodDesc->GetPrecodeType(), - methodDesc, - methodDesc->GetLoaderAllocator(), - &forwarderStubAllocationTracker); - methodDescForwarderStubHash.Add(forwarderStub); - forwarderStubAllocationTracker.SuppressRelease(); - } - - forwarderStub->SetTargetInterlocked(callCountingCodeEntryPoint, false); - callCountingCodeEntryPoint = forwarderStub->GetEntryPoint(); + // For methods that may have entry point slots to backpatch, redirect the method's temporary entry point + // (precode) to the call counting stub. This reuses the method's own precode as the stable indirection, + // avoiding the need to allocate separate forwarder stubs. + // + // The call counting stub should not be the entry point stored directly in vtable slots: + // - Stubs should be deletable without leaving dangling pointers in vtable slots + // - On some architectures (e.g. arm64), jitted code may load the entry point into a register at a GC-safe + // point, and the stub could be deleted before the register is used for the call + // + // Ensure vtable slots point to the temporary entry point (precode) so calls flow through + // precode → call counting stub → native code. Vtable slots may have been backpatched to native code + // during the initial publish or tiering delay. BackpatchToResetEntryPointSlots() also sets + // GetMethodEntryPoint() to the temporary entry point, which we override below. + // + // There is a benign race window between resetting vtable slots and setting the precode target: a thread + // may briefly see vtable slots pointing to the precode while the precode still points to its previous + // target (prestub or native code). This results in at most one uncounted call, which is acceptable since + // call counting is a heuristic. + methodDesc->BackpatchToResetEntryPointSlots(); + + // Keep GetMethodEntryPoint() set to the native code entry point rather than the temporary entry point. + // DoBackpatch() (prestub.cpp) skips slot recording when GetMethodEntryPoint() == GetTemporaryEntryPoint(), + // interpreting it as "method not yet published". By keeping GetMethodEntryPoint() at native code, we + // ensure that after the precode reverts to prestub (when call counting stubs are deleted), new vtable + // slots discovered by DoBackpatch() will be properly recorded for future backpatching. + methodDesc->SetMethodEntryPoint(codeEntryPoint); + Precode *precode = Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()); + precode->SetTargetInterlocked(callCountingCodeEntryPoint, FALSE); } else { _ASSERTE(methodDesc->IsVersionableWithPrecode()); + methodDesc->SetCodeEntryPoint(callCountingCodeEntryPoint); } - methodDesc->SetCodeEntryPoint(callCountingCodeEntryPoint); callCountingInfo->SetStage(CallCountingInfo::Stage::StubMayBeActive); return true; } @@ -932,6 +913,13 @@ void CallCountingManager::CompleteCallCounting() if (activeCodeVersion == codeVersion) { methodDesc->SetCodeEntryPoint(activeCodeVersion.GetNativeCode()); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + // Reset the precode target to prestub so that new vtable slots flow through + // DoBackpatch() for discovery and recording. The call counting stub will be + // deleted by DeleteAllCallCountingStubs(). + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } break; } @@ -947,11 +935,19 @@ void CallCountingManager::CompleteCallCounting() if (activeNativeCode != 0) { methodDesc->SetCodeEntryPoint(activeNativeCode); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } break; } } methodDesc->ResetCodeEntryPoint(); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } } while (false); callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); @@ -1089,7 +1085,15 @@ void CallCountingManager::StopAllCallCounting(TieredCompilationManager *tieredCo // The intention is that all call counting stubs will be deleted shortly, and only methods that are called again // will cause stubs to be recreated, so reset the code entry point - codeVersion.GetMethodDesc()->ResetCodeEntryPoint(); + MethodDesc *methodDesc = codeVersion.GetMethodDesc(); + methodDesc->ResetCodeEntryPoint(); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + // ResetCodeEntryPoint() for backpatchable methods resets recorded slots but does not touch the + // precode target. Reset the precode target to prestub so that new vtable slots flow through the + // prestub for slot discovery and recording. + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } callCountingInfo->SetStage(newCallCountingStage); } @@ -1109,14 +1113,6 @@ void CallCountingManager::StopAllCallCounting(TieredCompilationManager *tieredCo EX_SWALLOW_NONTERMINAL; } } - - // Reset forwarder stubs, they are not in use anymore - MethodDescForwarderStubHash &methodDescForwarderStubHash = callCountingManager->m_methodDescForwarderStubHash; - for (auto itEnd = methodDescForwarderStubHash.End(), it = methodDescForwarderStubHash.Begin(); it != itEnd; ++it) - { - Precode *forwarderStub = *it; - forwarderStub->ResetTargetInterlocked(); - } } } @@ -1144,7 +1140,6 @@ void CallCountingManager::DeleteAllCallCountingStubs() _ASSERTE(callCountingManager->m_callCountingInfosPendingCompletion.IsEmpty()); // Clear the call counting stub from call counting infos and delete completed infos - MethodDescForwarderStubHash &methodDescForwarderStubHash = callCountingManager->m_methodDescForwarderStubHash; CallCountingInfoByCodeVersionHash &callCountingInfoByCodeVersionHash = callCountingManager->m_callCountingInfoByCodeVersionHash; for (auto itEnd = callCountingInfoByCodeVersionHash.End(), it = callCountingInfoByCodeVersionHash.Begin(); @@ -1169,14 +1164,14 @@ void CallCountingManager::DeleteAllCallCountingStubs() continue; } - // Currently, tier 0 is the last code version that is counted, and the method is typically not counted anymore. - // Remove the forwarder stub if one exists, a new one will be created if necessary, for example, if a profiler adds - // an IL code version for the method. - Precode *const *forwarderStubPtr = - methodDescForwarderStubHash.LookupPtr(callCountingInfo->GetCodeVersion().GetMethodDesc()); - if (forwarderStubPtr != nullptr) + // Ensure the precode target is prestub for backpatchable methods whose call counting has completed. + // CompleteCallCounting() should have already reset the precode to prestub; this is a safety net to + // guarantee the invariant that the precode always points to prestub when call counting is not active, + // so that new vtable slots can be discovered and recorded by DoBackpatch(). + MethodDesc *methodDesc = callCountingInfo->GetCodeVersion().GetMethodDesc(); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - methodDescForwarderStubHash.RemovePtr(const_cast(forwarderStubPtr)); + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); } callCountingInfoByCodeVersionHash.Remove(it); @@ -1227,24 +1222,6 @@ void CallCountingManager::TrimCollections() } EX_SWALLOW_NONTERMINAL } - - count = m_methodDescForwarderStubHash.GetCount(); - capacity = m_methodDescForwarderStubHash.GetCapacity(); - if (count == 0) - { - if (capacity != 0) - { - m_methodDescForwarderStubHash.RemoveAll(); - } - } - else if (count <= capacity / 4) - { - EX_TRY - { - m_methodDescForwarderStubHash.Reallocate(count * 2); - } - EX_SWALLOW_NONTERMINAL - } } #endif // !DACCESS_COMPILE diff --git a/src/coreclr/vm/callcounting.h b/src/coreclr/vm/callcounting.h index 59071aa51f140b..fcc7bc834226e7 100644 --- a/src/coreclr/vm/callcounting.h +++ b/src/coreclr/vm/callcounting.h @@ -19,11 +19,18 @@ When starting call counting for a method (see CallCountingManager::SetCodeEntryP - A CallCountingStub is created. It contains a small amount of code that decrements the remaining call count and checks for zero. When nonzero, it jumps to the code version's native code entry point. When zero, it forwards to a helper function that handles tier promotion. -- For tiered methods that don't have a precode (virtual and interface methods when slot backpatching is enabled), a forwarder - stub (a precode) is created and it forwards to the call counting stub. This is so that the call counting stub can be safely - and easily deleted. The forwarder stubs are only used when counting calls, there is one per method (not per code version), and - they are not deleted. -- The method's code entry point is set to the forwarder stub or the call counting stub to count calls to the code version +- For tiered methods that do not normally use a MethodDesc precode as the stable entry point (virtual and interface methods + when slot backpatching is enabled), the method's own temporary-entrypoint precode is redirected to the call counting stub, + and vtable slots are reset to point to the temporary entry point. This ensures calls flow through temporary-entrypoint + precode -> call counting stub -> native code, and the call counting stub can be safely deleted since vtable slots don't + point to it directly. GetMethodEntryPoint() is kept at the native code entry point (not the temporary entry point) so that + DoBackpatch() can still record new vtable slots after the precode reverts to prestub. During call counting, there is a + bounded window where new vtable slots may not be recorded because the precode target is the call counting stub rather than + the prestub. These slots are corrected once call counting stubs are deleted and the precode reverts to prestub. + When call counting ends, the precode is always reset to prestub (never to native code), preserving the invariant + that new vtable slots can be discovered and recorded by DoBackpatch(). +- For methods with a precode (or when slot backpatching is disabled), the method's code entry point is set to the call + counting stub directly. When the call count threshold is reached (see CallCountingManager::OnCallCountThresholdReached): - The helper call enqueues completion of call counting for background processing @@ -36,8 +43,6 @@ After all work queued for promotion is completed and methods transitioned to opt - All call counting stubs are deleted. For code versions that have not completed counting, the method's code entry point is reset such that call counting would be reestablished on the next call. - Completed call counting infos are deleted -- For methods that no longer have any code versions that need to be counted, the forwarder stubs are no longer tracked. If a - new IL code version is added thereafter (perhaps by a profiler), a new forwarder stub may be created. Miscellaneous ------------- @@ -286,27 +291,6 @@ class CallCountingManager DISABLE_COPY(CallCountingStubAllocator); }; - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // CallCountingManager::MethodDescForwarderStub - -private: - class MethodDescForwarderStubHashTraits : public DefaultSHashTraits - { - private: - typedef DefaultSHashTraits Base; - public: - typedef Base::element_t element_t; - typedef Base::count_t count_t; - typedef MethodDesc *key_t; - - public: - static key_t GetKey(const element_t &e); - static BOOL Equals(const key_t &k1, const key_t &k2); - static count_t Hash(const key_t &k); - }; - - typedef SHash MethodDescForwarderStubHash; - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingManagerHashTraits @@ -341,7 +325,6 @@ class CallCountingManager private: CallCountingInfoByCodeVersionHash m_callCountingInfoByCodeVersionHash; CallCountingStubAllocator m_callCountingStubAllocator; - MethodDescForwarderStubHash m_methodDescForwarderStubHash; SArray m_callCountingInfosPendingCompletion; public: diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 75bec43916f40d..4714de07b10f63 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -3165,7 +3165,7 @@ void MethodDesc::RecordAndBackpatchEntryPointSlot_Locked( backpatchTracker->AddSlotAndPatch_Locked(this, slotLoaderAllocator, slot, slotType, currentEntryPoint); } -FORCEINLINE bool MethodDesc::TryBackpatchEntryPointSlots( +bool MethodDesc::TryBackpatchEntryPointSlots( PCODE entryPoint, bool isPrestubEntryPoint, bool onlyFromPrestubEntryPoint) diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index cae9b57021f60c..297c3e84e33053 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -106,10 +106,19 @@ PCODE MethodDesc::DoBackpatch(MethodTable * pMT, MethodTable *pDispatchingMT, bo // MethodDesc::BackpatchEntryPointSlots() // Backpatching the temporary entry point: - // The temporary entry point is never backpatched for methods versionable with vtable slot backpatch. New vtable - // slots inheriting the method will initially point to the temporary entry point and it must point to the prestub - // and come here for backpatching such that the new vtable slot can be discovered and recorded for future - // backpatching. + // The temporary entry point is not directly backpatched for methods versionable with vtable slot backpatch. + // New vtable slots inheriting the method will initially point to the temporary entry point. During call + // counting, the temporary entry point's precode target may be temporarily redirected to a call counting + // stub, but it must revert to the prestub when call counting ends (not to native code). This ensures new + // vtable slots will come here for backpatching so they can be discovered and recorded for future + // backpatching. The precode for backpatchable methods should only ever point to: + // 1. The prestub (default, and when call counting is not active) + // 2. A call counting stub (during active call counting only) + // It must never point directly to native code, as that would permanently bypass slot recording. + // + // To enable slot recording after the precode reverts to prestub, GetMethodEntryPoint() must be set to the + // native code entry point (not the temporary entry point) during call counting. This prevents the + // pExpected == pTarget check above from short-circuiting slot recording. _ASSERTE(!HasNonVtableSlot()); } From eebb19d825bbe39c0c69821ae69c70c55fe1e4d5 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 17:02:22 -0700 Subject: [PATCH 4/8] Avoid eager vtable slot backpatching during non-final tier transitions For backpatchable methods, avoid calling SetCodeEntryPoint (which eagerly backpatches all recorded vtable slots) during non-final tier transitions. Instead, use SetMethodEntryPoint to update the method entry point and SetTargetInterlocked to redirect the precode to the new code, keeping vtable slots pointing to the precode. For final tier activation, reset the precode to prestub so that calls through vtable slots trigger DoBackpatch() for lazy slot discovery, recording, and patching to the final tier code. This eliminates the PreStub lock contention that caused a 12-13% regression in PR #124664, while preserving the forwarder stub elimination benefits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/callcounting.cpp | 104 ++++++++++++++++++++------- src/coreclr/vm/tieredcompilation.cpp | 14 +++- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/coreclr/vm/callcounting.cpp b/src/coreclr/vm/callcounting.cpp index adef9ca678b97c..af3ffce1d5c099 100644 --- a/src/coreclr/vm/callcounting.cpp +++ b/src/coreclr/vm/callcounting.cpp @@ -565,19 +565,30 @@ bool CallCountingManager::SetCodeEntryPoint( _ASSERTE(!wasMethodCalled || createTieringBackgroundWorkerRef != nullptr); _ASSERTE(createTieringBackgroundWorkerRef == nullptr || !*createTieringBackgroundWorkerRef); - if (!methodDesc->IsEligibleForTieredCompilation() || - ( - // For a default code version that is not tier 0, call counting will have been disabled by this time (checked - // below). Avoid the redundant and not-insignificant expense of GetOptimizationTier() on a default code version. - !activeCodeVersion.IsDefaultVersion() && - activeCodeVersion.IsFinalTier() - ) || - !g_pConfig->TieredCompilation_CallCounting()) + if (!methodDesc->IsEligibleForTieredCompilation() || !g_pConfig->TieredCompilation_CallCounting()) { methodDesc->SetCodeEntryPoint(codeEntryPoint); return true; } + if (!activeCodeVersion.IsDefaultVersion() && activeCodeVersion.IsFinalTier()) + { + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + // Final tier for backpatchable methods: set the method entry point to the final code and reset the + // precode to prestub. Vtable slots continue to point to the precode (temporary entry point). Calls + // through those slots will go through prestub -> DoBackpatch(), which lazily discovers, records, and + // patches each slot to point directly to the final tier code. + methodDesc->SetMethodEntryPoint(codeEntryPoint); + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } + else + { + methodDesc->SetCodeEntryPoint(codeEntryPoint); + } + return true; + } + const CallCountingStub *callCountingStub; CallCountingManager *callCountingManager = methodDesc->GetLoaderAllocator()->GetCallCountingManager(); CallCountingInfoByCodeVersionHash &callCountingInfoByCodeVersionHash = @@ -594,14 +605,29 @@ bool CallCountingManager::SetCodeEntryPoint( { // Call counting is disabled, complete, or pending completion. The pending completion stage here would be // relatively rare, let it be handled elsewhere. - methodDesc->SetCodeEntryPoint(codeEntryPoint); if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Reset the precode target to prestub. For backpatchable methods, the precode must always - // point to prestub (not native code) so that new vtable slots flow through DoBackpatch() - // for discovery and recording. SetCodeEntryPoint() above handles recorded slots via - // BackpatchEntryPointSlots() without touching the precode. - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + methodDesc->SetMethodEntryPoint(codeEntryPoint); + if (activeCodeVersion.IsFinalTier()) + { + // Final tier: reset precode to prestub for lazy vtable slot backpatching via + // DoBackpatch(). Calls through vtable slots will go through precode -> prestub -> + // DoBackpatch() which records and patches each slot to final tier code. + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) + ->ResetTargetInterlocked(); + } + else + { + // Non-final tier: set precode target to the code entry point. Vtable slots remain + // pointing to the temporary entry point (precode), so calls flow through + // precode -> code without going through the prestub. No vtable slot backpatching. + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) + ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); + } + } + else + { + methodDesc->SetCodeEntryPoint(codeEntryPoint); } return true; } @@ -640,12 +666,17 @@ bool CallCountingManager::SetCodeEntryPoint( ->GetTieredCompilationManager() ->AsyncPromoteToTier1(activeCodeVersion, createTieringBackgroundWorkerRef); } - methodDesc->SetCodeEntryPoint(codeEntryPoint); if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Reset the precode target to prestub so that new vtable slots flow through DoBackpatch() - // for discovery and recording. The call counting stub is no longer needed. - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + // Non-final tier: set precode target to the code entry point. Vtable slots stay on the + // temporary entry point (precode). The call counting stub is no longer needed. + methodDesc->SetMethodEntryPoint(codeEntryPoint); + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) + ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); + } + else + { + methodDesc->SetCodeEntryPoint(codeEntryPoint); } callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); return true; @@ -912,13 +943,20 @@ void CallCountingManager::CompleteCallCounting() { if (activeCodeVersion == codeVersion) { - methodDesc->SetCodeEntryPoint(activeCodeVersion.GetNativeCode()); if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Reset the precode target to prestub so that new vtable slots flow through - // DoBackpatch() for discovery and recording. The call counting stub will be - // deleted by DeleteAllCallCountingStubs(). - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + // Non-final tier (call counting just completed, tier 1 promotion queued): set + // precode target to the code entry point. Vtable slots remain pointing to the + // temporary entry point (precode). The call counting stub will be deleted by + // DeleteAllCallCountingStubs(). + PCODE nativeCode = activeCodeVersion.GetNativeCode(); + methodDesc->SetMethodEntryPoint(nativeCode); + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) + ->SetTargetInterlocked(nativeCode, FALSE /* fOnlyRedirectFromPrestub */); + } + else + { + methodDesc->SetCodeEntryPoint(activeCodeVersion.GetNativeCode()); } break; } @@ -934,10 +972,26 @@ void CallCountingManager::CompleteCallCounting() PCODE activeNativeCode = activeCodeVersion.GetNativeCode(); if (activeNativeCode != 0) { - methodDesc->SetCodeEntryPoint(activeNativeCode); if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + methodDesc->SetMethodEntryPoint(activeNativeCode); + if (activeCodeVersion.IsFinalTier()) + { + // Final tier: reset precode to prestub for lazy vtable slot backpatching + // via DoBackpatch(). + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) + ->ResetTargetInterlocked(); + } + else + { + // Non-final tier: set precode target to the code entry point. + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) + ->SetTargetInterlocked(activeNativeCode, FALSE /* fOnlyRedirectFromPrestub */); + } + } + else + { + methodDesc->SetCodeEntryPoint(activeNativeCode); } break; } diff --git a/src/coreclr/vm/tieredcompilation.cpp b/src/coreclr/vm/tieredcompilation.cpp index 5a66f26ef328b3..65596604cb06c7 100644 --- a/src/coreclr/vm/tieredcompilation.cpp +++ b/src/coreclr/vm/tieredcompilation.cpp @@ -253,7 +253,19 @@ bool TieredCompilationManager::TrySetCodeEntryPointAndRecordMethodForCallCountin // Set the code entry point before recording the method for call counting to avoid a race. Otherwise, the tiering delay may // expire and enable call counting for the method before the entry point is set here, in which case calls to the method // would not be counted anymore. - pMethodDesc->SetCodeEntryPoint(codeEntryPoint); + if (pMethodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + // Non-final tier: set precode target to the code entry point. Vtable slots remain pointing to the + // temporary entry point (precode), so calls flow through precode -> code without going through the + // prestub. No vtable slot backpatching is needed during non-final tiers. + pMethodDesc->SetMethodEntryPoint(codeEntryPoint); + Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) + ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); + } + else + { + pMethodDesc->SetCodeEntryPoint(codeEntryPoint); + } _ASSERTE(m_methodsPendingCountingForTier1 != nullptr); m_methodsPendingCountingForTier1->Append(pMethodDesc); return true; From baf19a8cc653a4accc01b3bad9b99540f8940d7d Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 17:35:33 -0700 Subject: [PATCH 5/8] Eliminate vtable slot oscillations during non-final tier transitions During non-final tiers, keep GetMethodEntryPoint() at the temporary entry point (precode) instead of setting it to native code. This causes DoBackpatch() to short-circuit early, preventing vtable slot recording and patching during non-final tier transitions. The precode target is still redirected to native code or call counting stubs via SetTargetInterlocked, so calls through the precode work correctly. When the final tier is activated, SetMethodEntryPoint() is set to the final code and the precode is reset to prestub. The next call through a vtable slot triggers DoBackpatch(), which records and patches the slot to point directly to the final tier code. Subsequent calls bypass the precode entirely. Key changes: - PublishVersionedMethodCode: skip TrySetInitialCodeEntryPointForVersionableMethod for non-final tiers with call counting; just redirect the precode target - CallCountingManager::SetCodeEntryPoint: remove SetMethodEntryPoint and BackpatchToResetEntryPointSlots calls for non-final tier backpatchable methods - TrySetCodeEntryPointAndRecordMethodForCallCounting: remove SetMethodEntryPoint - CompleteCallCounting: remove SetMethodEntryPoint for non-final tiers - DoBackpatch comments: updated to describe the new invariant Results (TryBackpatchEntryPointSlots / BackpatchSlots / slot writes): - Baseline (forwarder stubs): 335 / 91 / - - Previous optimization: 281 / 0 / 60 - After this change: 0 / 0 / 0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/callcounting.cpp | 55 ++++++++++------------------ src/coreclr/vm/codeversion.cpp | 19 ++++++++-- src/coreclr/vm/prestub.cpp | 26 +++++++------ src/coreclr/vm/tieredcompilation.cpp | 4 +- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/coreclr/vm/callcounting.cpp b/src/coreclr/vm/callcounting.cpp index af3ffce1d5c099..67627fc1aa41d9 100644 --- a/src/coreclr/vm/callcounting.cpp +++ b/src/coreclr/vm/callcounting.cpp @@ -607,20 +607,19 @@ bool CallCountingManager::SetCodeEntryPoint( // relatively rare, let it be handled elsewhere. if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - methodDesc->SetMethodEntryPoint(codeEntryPoint); if (activeCodeVersion.IsFinalTier()) { - // Final tier: reset precode to prestub for lazy vtable slot backpatching via - // DoBackpatch(). Calls through vtable slots will go through precode -> prestub -> - // DoBackpatch() which records and patches each slot to final tier code. + // Final tier: set the method entry point to the final code and reset precode to + // prestub for lazy vtable slot backpatching via DoBackpatch(). + methodDesc->SetMethodEntryPoint(codeEntryPoint); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->ResetTargetInterlocked(); } else { // Non-final tier: set precode target to the code entry point. Vtable slots remain - // pointing to the temporary entry point (precode), so calls flow through - // precode -> code without going through the prestub. No vtable slot backpatching. + // pointing to the temporary entry point (precode). Do NOT set GetMethodEntryPoint() + // to prevent DoBackpatch() from recording vtable slots during non-final tiers. Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); } @@ -668,9 +667,9 @@ bool CallCountingManager::SetCodeEntryPoint( } if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Non-final tier: set precode target to the code entry point. Vtable slots stay on the - // temporary entry point (precode). The call counting stub is no longer needed. - methodDesc->SetMethodEntryPoint(codeEntryPoint); + // Non-final tier (call counting threshold just reached): set precode target to the code + // entry point. Vtable slots stay pointing to the temporary entry point (precode). Do NOT + // set GetMethodEntryPoint() to prevent DoBackpatch() from recording vtable slots. Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); } @@ -749,23 +748,9 @@ bool CallCountingManager::SetCodeEntryPoint( // - On some architectures (e.g. arm64), jitted code may load the entry point into a register at a GC-safe // point, and the stub could be deleted before the register is used for the call // - // Ensure vtable slots point to the temporary entry point (precode) so calls flow through - // precode → call counting stub → native code. Vtable slots may have been backpatched to native code - // during the initial publish or tiering delay. BackpatchToResetEntryPointSlots() also sets - // GetMethodEntryPoint() to the temporary entry point, which we override below. - // - // There is a benign race window between resetting vtable slots and setting the precode target: a thread - // may briefly see vtable slots pointing to the precode while the precode still points to its previous - // target (prestub or native code). This results in at most one uncounted call, which is acceptable since - // call counting is a heuristic. - methodDesc->BackpatchToResetEntryPointSlots(); - - // Keep GetMethodEntryPoint() set to the native code entry point rather than the temporary entry point. - // DoBackpatch() (prestub.cpp) skips slot recording when GetMethodEntryPoint() == GetTemporaryEntryPoint(), - // interpreting it as "method not yet published". By keeping GetMethodEntryPoint() at native code, we - // ensure that after the precode reverts to prestub (when call counting stubs are deleted), new vtable - // slots discovered by DoBackpatch() will be properly recorded for future backpatching. - methodDesc->SetMethodEntryPoint(codeEntryPoint); + // Vtable slots already point to the temporary entry point (precode) since we never backpatch vtable slots + // during non-final tiers. GetMethodEntryPoint() is kept at the temporary entry point to prevent + // DoBackpatch() from recording vtable slots during non-final tiers. Precode *precode = Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()); precode->SetTargetInterlocked(callCountingCodeEntryPoint, FALSE); } @@ -947,12 +932,11 @@ void CallCountingManager::CompleteCallCounting() { // Non-final tier (call counting just completed, tier 1 promotion queued): set // precode target to the code entry point. Vtable slots remain pointing to the - // temporary entry point (precode). The call counting stub will be deleted by - // DeleteAllCallCountingStubs(). - PCODE nativeCode = activeCodeVersion.GetNativeCode(); - methodDesc->SetMethodEntryPoint(nativeCode); + // temporary entry point (precode). Do NOT set GetMethodEntryPoint() to prevent + // DoBackpatch() from recording vtable slots during non-final tiers. Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(nativeCode, FALSE /* fOnlyRedirectFromPrestub */); + ->SetTargetInterlocked(activeCodeVersion.GetNativeCode(), + FALSE /* fOnlyRedirectFromPrestub */); } else { @@ -974,17 +958,18 @@ void CallCountingManager::CompleteCallCounting() { if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - methodDesc->SetMethodEntryPoint(activeNativeCode); if (activeCodeVersion.IsFinalTier()) { - // Final tier: reset precode to prestub for lazy vtable slot backpatching - // via DoBackpatch(). + // Final tier: set the method entry point to the final code and reset + // precode to prestub for lazy vtable slot backpatching via DoBackpatch(). + methodDesc->SetMethodEntryPoint(activeNativeCode); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->ResetTargetInterlocked(); } else { - // Non-final tier: set precode target to the code entry point. + // Non-final tier: set precode target to the code entry point. Do NOT set + // GetMethodEntryPoint() to prevent DoBackpatch() from recording slots. Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(activeNativeCode, FALSE /* fOnlyRedirectFromPrestub */); } diff --git a/src/coreclr/vm/codeversion.cpp b/src/coreclr/vm/codeversion.cpp index 0333d2d7941422..fbcb53c5c523ec 100644 --- a/src/coreclr/vm/codeversion.cpp +++ b/src/coreclr/vm/codeversion.cpp @@ -1845,9 +1845,22 @@ PCODE CodeVersionManager::PublishVersionableCodeIfNecessary( if (doPublish) { bool mayHaveEntryPointSlotsToBackpatch2 = pMethodDesc->MayHaveEntryPointSlotsToBackpatch(); - MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder2( - mayHaveEntryPointSlotsToBackpatch2); - pMethodDesc->TrySetInitialCodeEntryPointForVersionableMethod(pCode, mayHaveEntryPointSlotsToBackpatch2); + if (handleCallCountingForFirstCall && mayHaveEntryPointSlotsToBackpatch2) + { + // Non-final tier for backpatchable methods: redirect the precode to the code, but do NOT + // set GetMethodEntryPoint() or backpatch vtable slots. Vtable slots stay pointing to the + // precode (temporary entry point). Keeping GetMethodEntryPoint() == GetTemporaryEntryPoint() + // causes DoBackpatch() to return early, preventing slot recording during non-final tiers. + // Slots will only be recorded and backpatched when the final tier is activated. + Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) + ->SetTargetInterlocked(pCode, TRUE /* fOnlyRedirectFromPrestub */); + } + else + { + MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder2( + mayHaveEntryPointSlotsToBackpatch2); + pMethodDesc->TrySetInitialCodeEntryPointForVersionableMethod(pCode, mayHaveEntryPointSlotsToBackpatch2); + } } else { diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index 297c3e84e33053..d7b6aedbf34f65 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -107,18 +107,22 @@ PCODE MethodDesc::DoBackpatch(MethodTable * pMT, MethodTable *pDispatchingMT, bo // Backpatching the temporary entry point: // The temporary entry point is not directly backpatched for methods versionable with vtable slot backpatch. - // New vtable slots inheriting the method will initially point to the temporary entry point. During call - // counting, the temporary entry point's precode target may be temporarily redirected to a call counting - // stub, but it must revert to the prestub when call counting ends (not to native code). This ensures new - // vtable slots will come here for backpatching so they can be discovered and recorded for future - // backpatching. The precode for backpatchable methods should only ever point to: - // 1. The prestub (default, and when call counting is not active) - // 2. A call counting stub (during active call counting only) - // It must never point directly to native code, as that would permanently bypass slot recording. + // New vtable slots inheriting the method will initially point to the temporary entry point. During non-final + // tiers, GetMethodEntryPoint() is kept at the temporary entry point, causing the pExpected == pTarget check + // above to short-circuit and return early. This prevents vtable slot recording and backpatching during + // non-final tiers, avoiding oscillation between native code and precode on each tier transition. // - // To enable slot recording after the precode reverts to prestub, GetMethodEntryPoint() must be set to the - // native code entry point (not the temporary entry point) during call counting. This prevents the - // pExpected == pTarget check above from short-circuiting slot recording. + // During non-final tiers, the precode target may point to: + // 1. Non-final tier native code (set via SetTargetInterlocked) + // 2. A call counting stub (during active call counting) + // 3. The prestub (default, or after call counting stubs are deleted) + // In all cases, vtable slots point to the precode and calls flow through: + // vtable -> precode -> (native code / call counting stub / prestub) + // + // When the final tier is activated, GetMethodEntryPoint() is set to the final tier code and the precode is + // reset to prestub. The next call through the vtable goes through precode -> prestub -> DoBackpatch(), which + // discovers, records, and patches the vtable slot to point directly to the final tier code. Subsequent calls + // bypass the precode entirely: vtable -> final tier code. _ASSERTE(!HasNonVtableSlot()); } diff --git a/src/coreclr/vm/tieredcompilation.cpp b/src/coreclr/vm/tieredcompilation.cpp index 65596604cb06c7..9a512caea1dafc 100644 --- a/src/coreclr/vm/tieredcompilation.cpp +++ b/src/coreclr/vm/tieredcompilation.cpp @@ -257,8 +257,8 @@ bool TieredCompilationManager::TrySetCodeEntryPointAndRecordMethodForCallCountin { // Non-final tier: set precode target to the code entry point. Vtable slots remain pointing to the // temporary entry point (precode), so calls flow through precode -> code without going through the - // prestub. No vtable slot backpatching is needed during non-final tiers. - pMethodDesc->SetMethodEntryPoint(codeEntryPoint); + // prestub. Do NOT set GetMethodEntryPoint() — keeping it at the temporary entry point prevents + // DoBackpatch() from recording vtable slots during non-final tiers. Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); } From 68dbdbad34243e94d0d57d2afdf1cf467e5cc22e Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 19:38:04 -0700 Subject: [PATCH 6/8] Add LOG calls for vtable slot backpatch paths during tiered compilation The existing LOG calls in MethodDesc::SetCodeEntryPoint only cover non-backpatchable methods. Backpatchable methods bypass SetCodeEntryPoint and go through dedicated paths in CallCountingManager::SetCodeEntryPoint, CompleteCallCounting, PublishVersionableCodeIfNecessary, and TrySetCodeEntryPointAndRecordMethodForCallCounting. Without logging in these paths, the vtable slot lifecycle for backpatchable methods is invisible in checked build logs. Add LOG calls at all 10 vtable slot backpatch paths with descriptive labels: InitialPublish-NonFinal, CallCountingStub, ThresholdReached, NonFinal, FinalTier, FinalTier-NonDefault, SameVersion-NonFinal, DiffVersion-FinalTier, DiffVersion-NonFinal, and DelayActivation-NonFinal. All use LF_TIEREDCOMPILATION at LL_INFO10000, consistent with the existing LOG calls in method.cpp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/callcounting.cpp | 48 ++++++++++++++++++++++++++++ src/coreclr/vm/codeversion.cpp | 6 ++++ src/coreclr/vm/tieredcompilation.cpp | 5 +++ 3 files changed, 59 insertions(+) diff --git a/src/coreclr/vm/callcounting.cpp b/src/coreclr/vm/callcounting.cpp index 67627fc1aa41d9..cf2b0895dbcc9b 100644 --- a/src/coreclr/vm/callcounting.cpp +++ b/src/coreclr/vm/callcounting.cpp @@ -579,6 +579,12 @@ bool CallCountingManager::SetCodeEntryPoint( // precode to prestub. Vtable slots continue to point to the precode (temporary entry point). Calls // through those slots will go through prestub -> DoBackpatch(), which lazily discovers, records, and // patches each slot to point directly to the final tier code. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(FinalTier-NonDefault) - setting owning vtable slot to final code," + " resetting precode to prestub for lazy DoBackpatch\n", + methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, + DBG_ADDR(codeEntryPoint))); methodDesc->SetMethodEntryPoint(codeEntryPoint); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); } @@ -611,6 +617,12 @@ bool CallCountingManager::SetCodeEntryPoint( { // Final tier: set the method entry point to the final code and reset precode to // prestub for lazy vtable slot backpatching via DoBackpatch(). + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(FinalTier) - setting owning vtable slot to final code," + " resetting precode to prestub\n", + methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, + DBG_ADDR(codeEntryPoint))); methodDesc->SetMethodEntryPoint(codeEntryPoint); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->ResetTargetInterlocked(); @@ -620,6 +632,12 @@ bool CallCountingManager::SetCodeEntryPoint( // Non-final tier: set precode target to the code entry point. Vtable slots remain // pointing to the temporary entry point (precode). Do NOT set GetMethodEntryPoint() // to prevent DoBackpatch() from recording vtable slots during non-final tiers. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(NonFinal) - precode target redirected," + " vtable slot stays at temporary entry point\n", + methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, + DBG_ADDR(codeEntryPoint))); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); } @@ -670,6 +688,12 @@ bool CallCountingManager::SetCodeEntryPoint( // Non-final tier (call counting threshold just reached): set precode target to the code // entry point. Vtable slots stay pointing to the temporary entry point (precode). Do NOT // set GetMethodEntryPoint() to prevent DoBackpatch() from recording vtable slots. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(ThresholdReached) - precode target redirected," + " vtable slot stays at temporary entry point\n", + methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, + DBG_ADDR(codeEntryPoint))); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); } @@ -751,6 +775,12 @@ bool CallCountingManager::SetCodeEntryPoint( // Vtable slots already point to the temporary entry point (precode) since we never backpatch vtable slots // during non-final tiers. GetMethodEntryPoint() is kept at the temporary entry point to prevent // DoBackpatch() from recording vtable slots during non-final tiers. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) stub=" FMT_ADDR + " path=VtableSlotBackpatch(CallCountingStub) - precode target set to call counting stub," + " vtable slot stays at temporary entry point\n", + methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, + DBG_ADDR(callCountingCodeEntryPoint))); Precode *precode = Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()); precode->SetTargetInterlocked(callCountingCodeEntryPoint, FALSE); } @@ -934,6 +964,12 @@ void CallCountingManager::CompleteCallCounting() // precode target to the code entry point. Vtable slots remain pointing to the // temporary entry point (precode). Do NOT set GetMethodEntryPoint() to prevent // DoBackpatch() from recording vtable slots during non-final tiers. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CompleteCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(SameVersion-NonFinal) - precode target redirected\n", + methodDesc, methodDesc->m_pszDebugClassName, + methodDesc->m_pszDebugMethodName, + DBG_ADDR(activeCodeVersion.GetNativeCode()))); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(activeCodeVersion.GetNativeCode(), FALSE /* fOnlyRedirectFromPrestub */); @@ -962,6 +998,12 @@ void CallCountingManager::CompleteCallCounting() { // Final tier: set the method entry point to the final code and reset // precode to prestub for lazy vtable slot backpatching via DoBackpatch(). + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CompleteCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(DiffVersion-FinalTier) - setting owning" + " vtable slot to final code, resetting precode to prestub\n", + methodDesc, methodDesc->m_pszDebugClassName, + methodDesc->m_pszDebugMethodName, DBG_ADDR(activeNativeCode))); methodDesc->SetMethodEntryPoint(activeNativeCode); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->ResetTargetInterlocked(); @@ -970,6 +1012,12 @@ void CallCountingManager::CompleteCallCounting() { // Non-final tier: set precode target to the code entry point. Do NOT set // GetMethodEntryPoint() to prevent DoBackpatch() from recording slots. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "CompleteCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(DiffVersion-NonFinal) - precode target" + " redirected\n", + methodDesc, methodDesc->m_pszDebugClassName, + methodDesc->m_pszDebugMethodName, DBG_ADDR(activeNativeCode))); Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(activeNativeCode, FALSE /* fOnlyRedirectFromPrestub */); } diff --git a/src/coreclr/vm/codeversion.cpp b/src/coreclr/vm/codeversion.cpp index fbcb53c5c523ec..2d281bccdc4bd0 100644 --- a/src/coreclr/vm/codeversion.cpp +++ b/src/coreclr/vm/codeversion.cpp @@ -1852,6 +1852,12 @@ PCODE CodeVersionManager::PublishVersionableCodeIfNecessary( // precode (temporary entry point). Keeping GetMethodEntryPoint() == GetTemporaryEntryPoint() // causes DoBackpatch() to return early, preventing slot recording during non-final tiers. // Slots will only be recorded and backpatched when the final tier is activated. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "PublishVersionableCode pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(InitialPublish-NonFinal) - precode target redirected," + " vtable slot stays at temporary entry point\n", + pMethodDesc, pMethodDesc->m_pszDebugClassName, pMethodDesc->m_pszDebugMethodName, + DBG_ADDR(pCode))); Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(pCode, TRUE /* fOnlyRedirectFromPrestub */); } diff --git a/src/coreclr/vm/tieredcompilation.cpp b/src/coreclr/vm/tieredcompilation.cpp index 9a512caea1dafc..0f736738b76506 100644 --- a/src/coreclr/vm/tieredcompilation.cpp +++ b/src/coreclr/vm/tieredcompilation.cpp @@ -259,6 +259,11 @@ bool TieredCompilationManager::TrySetCodeEntryPointAndRecordMethodForCallCountin // temporary entry point (precode), so calls flow through precode -> code without going through the // prestub. Do NOT set GetMethodEntryPoint() — keeping it at the temporary entry point prevents // DoBackpatch() from recording vtable slots during non-final tiers. + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "TrySetCodeEntryPointAndRecordMethodForCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " path=VtableSlotBackpatch(DelayActivation-NonFinal) - precode target redirected\n", + pMethodDesc, pMethodDesc->m_pszDebugClassName, pMethodDesc->m_pszDebugMethodName, + DBG_ADDR(codeEntryPoint))); Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); } From 3df9c21df0935224269d6b3d74c0c3eb796da9ef Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 9 Mar 2026 20:02:03 -0700 Subject: [PATCH 7/8] Extract SetBackpatchableEntryPoint helper for vtable slot backpatch paths Consolidate 10 duplicated vtable slot backpatch code patterns across callcounting.cpp, codeversion.cpp, and tieredcompilation.cpp into a single MethodDesc::SetBackpatchableEntryPoint helper method. The helper encapsulates the two core patterns: - Final tier: SetMethodEntryPoint(code) + ResetTargetInterlocked() - Non-final tier: SetTargetInterlocked(target, fOnlyRedirectFromPrestub) Includes centralized LOG calls and an assert to prevent invalid parameter combinations (isFinalTier + fOnlyRedirectFromPrestub). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/callcounting.cpp | 109 ++------------------------- src/coreclr/vm/codeversion.cpp | 14 +--- src/coreclr/vm/method.cpp | 36 +++++++++ src/coreclr/vm/method.hpp | 1 + src/coreclr/vm/tieredcompilation.cpp | 12 +-- 5 files changed, 45 insertions(+), 127 deletions(-) diff --git a/src/coreclr/vm/callcounting.cpp b/src/coreclr/vm/callcounting.cpp index cf2b0895dbcc9b..3f1e1bb3450407 100644 --- a/src/coreclr/vm/callcounting.cpp +++ b/src/coreclr/vm/callcounting.cpp @@ -575,18 +575,7 @@ bool CallCountingManager::SetCodeEntryPoint( { if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Final tier for backpatchable methods: set the method entry point to the final code and reset the - // precode to prestub. Vtable slots continue to point to the precode (temporary entry point). Calls - // through those slots will go through prestub -> DoBackpatch(), which lazily discovers, records, and - // patches each slot to point directly to the final tier code. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(FinalTier-NonDefault) - setting owning vtable slot to final code," - " resetting precode to prestub for lazy DoBackpatch\n", - methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, - DBG_ADDR(codeEntryPoint))); - methodDesc->SetMethodEntryPoint(codeEntryPoint); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + methodDesc->SetBackpatchableEntryPoint(codeEntryPoint, true /* isFinalTier */); } else { @@ -613,34 +602,7 @@ bool CallCountingManager::SetCodeEntryPoint( // relatively rare, let it be handled elsewhere. if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - if (activeCodeVersion.IsFinalTier()) - { - // Final tier: set the method entry point to the final code and reset precode to - // prestub for lazy vtable slot backpatching via DoBackpatch(). - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(FinalTier) - setting owning vtable slot to final code," - " resetting precode to prestub\n", - methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, - DBG_ADDR(codeEntryPoint))); - methodDesc->SetMethodEntryPoint(codeEntryPoint); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->ResetTargetInterlocked(); - } - else - { - // Non-final tier: set precode target to the code entry point. Vtable slots remain - // pointing to the temporary entry point (precode). Do NOT set GetMethodEntryPoint() - // to prevent DoBackpatch() from recording vtable slots during non-final tiers. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(NonFinal) - precode target redirected," - " vtable slot stays at temporary entry point\n", - methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, - DBG_ADDR(codeEntryPoint))); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); - } + methodDesc->SetBackpatchableEntryPoint(codeEntryPoint, activeCodeVersion.IsFinalTier()); } else { @@ -685,17 +647,7 @@ bool CallCountingManager::SetCodeEntryPoint( } if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Non-final tier (call counting threshold just reached): set precode target to the code - // entry point. Vtable slots stay pointing to the temporary entry point (precode). Do NOT - // set GetMethodEntryPoint() to prevent DoBackpatch() from recording vtable slots. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(ThresholdReached) - precode target redirected," - " vtable slot stays at temporary entry point\n", - methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, - DBG_ADDR(codeEntryPoint))); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); + methodDesc->SetBackpatchableEntryPoint(codeEntryPoint, false /* isFinalTier */); } else { @@ -771,18 +723,7 @@ bool CallCountingManager::SetCodeEntryPoint( // - Stubs should be deletable without leaving dangling pointers in vtable slots // - On some architectures (e.g. arm64), jitted code may load the entry point into a register at a GC-safe // point, and the stub could be deleted before the register is used for the call - // - // Vtable slots already point to the temporary entry point (precode) since we never backpatch vtable slots - // during non-final tiers. GetMethodEntryPoint() is kept at the temporary entry point to prevent - // DoBackpatch() from recording vtable slots during non-final tiers. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CallCountingManager::SetCodeEntryPoint pMD=%p (%s::%s) stub=" FMT_ADDR - " path=VtableSlotBackpatch(CallCountingStub) - precode target set to call counting stub," - " vtable slot stays at temporary entry point\n", - methodDesc, methodDesc->m_pszDebugClassName, methodDesc->m_pszDebugMethodName, - DBG_ADDR(callCountingCodeEntryPoint))); - Precode *precode = Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()); - precode->SetTargetInterlocked(callCountingCodeEntryPoint, FALSE); + methodDesc->SetBackpatchableEntryPoint(callCountingCodeEntryPoint, false /* isFinalTier */); } else { @@ -960,19 +901,7 @@ void CallCountingManager::CompleteCallCounting() { if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Non-final tier (call counting just completed, tier 1 promotion queued): set - // precode target to the code entry point. Vtable slots remain pointing to the - // temporary entry point (precode). Do NOT set GetMethodEntryPoint() to prevent - // DoBackpatch() from recording vtable slots during non-final tiers. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CompleteCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(SameVersion-NonFinal) - precode target redirected\n", - methodDesc, methodDesc->m_pszDebugClassName, - methodDesc->m_pszDebugMethodName, - DBG_ADDR(activeCodeVersion.GetNativeCode()))); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(activeCodeVersion.GetNativeCode(), - FALSE /* fOnlyRedirectFromPrestub */); + methodDesc->SetBackpatchableEntryPoint(activeCodeVersion.GetNativeCode(), false /* isFinalTier */); } else { @@ -994,33 +923,7 @@ void CallCountingManager::CompleteCallCounting() { if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { - if (activeCodeVersion.IsFinalTier()) - { - // Final tier: set the method entry point to the final code and reset - // precode to prestub for lazy vtable slot backpatching via DoBackpatch(). - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CompleteCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(DiffVersion-FinalTier) - setting owning" - " vtable slot to final code, resetting precode to prestub\n", - methodDesc, methodDesc->m_pszDebugClassName, - methodDesc->m_pszDebugMethodName, DBG_ADDR(activeNativeCode))); - methodDesc->SetMethodEntryPoint(activeNativeCode); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->ResetTargetInterlocked(); - } - else - { - // Non-final tier: set precode target to the code entry point. Do NOT set - // GetMethodEntryPoint() to prevent DoBackpatch() from recording slots. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "CompleteCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(DiffVersion-NonFinal) - precode target" - " redirected\n", - methodDesc, methodDesc->m_pszDebugClassName, - methodDesc->m_pszDebugMethodName, DBG_ADDR(activeNativeCode))); - Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(activeNativeCode, FALSE /* fOnlyRedirectFromPrestub */); - } + methodDesc->SetBackpatchableEntryPoint(activeNativeCode, activeCodeVersion.IsFinalTier()); } else { diff --git a/src/coreclr/vm/codeversion.cpp b/src/coreclr/vm/codeversion.cpp index 2d281bccdc4bd0..a6f591e3ba9518 100644 --- a/src/coreclr/vm/codeversion.cpp +++ b/src/coreclr/vm/codeversion.cpp @@ -1847,19 +1847,7 @@ PCODE CodeVersionManager::PublishVersionableCodeIfNecessary( bool mayHaveEntryPointSlotsToBackpatch2 = pMethodDesc->MayHaveEntryPointSlotsToBackpatch(); if (handleCallCountingForFirstCall && mayHaveEntryPointSlotsToBackpatch2) { - // Non-final tier for backpatchable methods: redirect the precode to the code, but do NOT - // set GetMethodEntryPoint() or backpatch vtable slots. Vtable slots stay pointing to the - // precode (temporary entry point). Keeping GetMethodEntryPoint() == GetTemporaryEntryPoint() - // causes DoBackpatch() to return early, preventing slot recording during non-final tiers. - // Slots will only be recorded and backpatched when the final tier is activated. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "PublishVersionableCode pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(InitialPublish-NonFinal) - precode target redirected," - " vtable slot stays at temporary entry point\n", - pMethodDesc, pMethodDesc->m_pszDebugClassName, pMethodDesc->m_pszDebugMethodName, - DBG_ADDR(pCode))); - Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(pCode, TRUE /* fOnlyRedirectFromPrestub */); + pMethodDesc->SetBackpatchableEntryPoint(pCode, false /* isFinalTier */, TRUE /* fOnlyRedirectFromPrestub */); } else { diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 4714de07b10f63..5b8bdc6ff4f70a 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -3334,6 +3334,42 @@ void MethodDesc::ResetCodeEntryPoint() GetPrecode()->ResetTargetInterlocked(); } } + +// Sets the entry point for a backpatchable method during tiered compilation. +// For final tier: sets the owning vtable slot to codeEntryPoint and resets the precode to prestub, +// enabling lazy vtable slot discovery via DoBackpatch(). +// For non-final tier: redirects the precode target to codeEntryPoint without modifying the vtable slot. +// The vtable slot stays at the temporary entry point (precode), preventing DoBackpatch() from +// recording vtable slots during non-final tiers. +void MethodDesc::SetBackpatchableEntryPoint(PCODE codeEntryPoint, bool isFinalTier, BOOL fOnlyRedirectFromPrestub) +{ + WRAPPER_NO_CONTRACT; + _ASSERTE(MayHaveEntryPointSlotsToBackpatch()); + _ASSERTE(codeEntryPoint != (PCODE)NULL); + _ASSERTE(!isFinalTier || !fOnlyRedirectFromPrestub); + + Precode *precode = Precode::GetPrecodeFromEntryPoint(GetTemporaryEntryPoint()); + if (isFinalTier) + { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "MethodDesc::SetBackpatchableEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " isFinalTier=true - setting owning vtable slot to final code," + " resetting precode to prestub for lazy DoBackpatch\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(codeEntryPoint))); + SetMethodEntryPoint(codeEntryPoint); + precode->ResetTargetInterlocked(); + } + else + { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "MethodDesc::SetBackpatchableEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR + " isFinalTier=false fOnlyRedirectFromPrestub=%d - precode target redirected," + " vtable slot stays at temporary entry point\n", + this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(codeEntryPoint), + fOnlyRedirectFromPrestub)); + precode->SetTargetInterlocked(codeEntryPoint, fOnlyRedirectFromPrestub); + } +} #endif // FEATURE_TIERED_COMPILATION void MethodDesc::ResetCodeEntryPointForEnC() diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index a42552c5ac60e6..12fd22e56eb296 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1452,6 +1452,7 @@ class MethodDesc void SetCodeEntryPoint(PCODE entryPoint); #ifdef FEATURE_TIERED_COMPILATION void ResetCodeEntryPoint(); + void SetBackpatchableEntryPoint(PCODE entryPoint, bool isFinalTier, BOOL fOnlyRedirectFromPrestub = FALSE); #endif // FEATURE_TIERED_COMPILATION void ResetCodeEntryPointForEnC(); diff --git a/src/coreclr/vm/tieredcompilation.cpp b/src/coreclr/vm/tieredcompilation.cpp index 0f736738b76506..3d7f4972311eec 100644 --- a/src/coreclr/vm/tieredcompilation.cpp +++ b/src/coreclr/vm/tieredcompilation.cpp @@ -255,17 +255,7 @@ bool TieredCompilationManager::TrySetCodeEntryPointAndRecordMethodForCallCountin // would not be counted anymore. if (pMethodDesc->MayHaveEntryPointSlotsToBackpatch()) { - // Non-final tier: set precode target to the code entry point. Vtable slots remain pointing to the - // temporary entry point (precode), so calls flow through precode -> code without going through the - // prestub. Do NOT set GetMethodEntryPoint() — keeping it at the temporary entry point prevents - // DoBackpatch() from recording vtable slots during non-final tiers. - LOG((LF_TIEREDCOMPILATION, LL_INFO10000, - "TrySetCodeEntryPointAndRecordMethodForCallCounting pMD=%p (%s::%s) entryPoint=" FMT_ADDR - " path=VtableSlotBackpatch(DelayActivation-NonFinal) - precode target redirected\n", - pMethodDesc, pMethodDesc->m_pszDebugClassName, pMethodDesc->m_pszDebugMethodName, - DBG_ADDR(codeEntryPoint))); - Precode::GetPrecodeFromEntryPoint(pMethodDesc->GetTemporaryEntryPoint()) - ->SetTargetInterlocked(codeEntryPoint, FALSE /* fOnlyRedirectFromPrestub */); + pMethodDesc->SetBackpatchableEntryPoint(codeEntryPoint, false /* isFinalTier */); } else { From 0c178ffa62e30ba76f2af90fa0f2a919aa552184 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 10 Mar 2026 13:02:28 -0700 Subject: [PATCH 8/8] Register funcptr precodes in backpatching table for proper tiered compilation lifecycle Funcptr precodes for backpatchable methods are now registered in the entry point slot backpatching table at creation time, replacing the ad-hoc funcptr precode lookup and patching that was previously done in TryBackpatchEntryPointSlots. This ensures: - During non-final tiers, funcptr precode targets point to the method's precode (temporary entry point), so calls flow through the same path as vtable calls - At final tier, SetBackpatchableEntryPoint calls Backpatch_Locked to update all registered slots (including funcptr precodes) to the final code - During rejit, BackpatchToResetEntryPointSlots resets funcptr targets via the backpatching table, ensuring proper re-discovery through the prestub The change adds Precode::GetTargetSlot() and StubPrecode::GetTargetSlot() to expose the writable target field address for registration as a SlotType_Normal entry point slot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/fptrstubs.cpp | 37 ++++++++++++++++++++++++--------- src/coreclr/vm/method.cpp | 40 +++++++++++++++--------------------- src/coreclr/vm/precode.cpp | 22 ++++++++++++++++++++ src/coreclr/vm/precode.h | 6 ++++++ src/coreclr/vm/prestub.cpp | 10 ++++++--- 5 files changed, 78 insertions(+), 37 deletions(-) diff --git a/src/coreclr/vm/fptrstubs.cpp b/src/coreclr/vm/fptrstubs.cpp index cf3f4db379f92b..52a090dd34e95c 100644 --- a/src/coreclr/vm/fptrstubs.cpp +++ b/src/coreclr/vm/fptrstubs.cpp @@ -76,6 +76,9 @@ PCODE FuncPtrStubs::GetFuncPtrStub(MethodDesc * pMD, PrecodeType type) if (pPrecode != NULL) { + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "FuncPtrStubs::GetFuncPtrStub pMD=%p type=%d - found existing stub, entryPoint=" FMT_ADDR "\n", + pMD, type, DBG_ADDR(pPrecode->GetEntryPoint()))); return pPrecode->GetEntryPoint(); } @@ -141,6 +144,10 @@ PCODE FuncPtrStubs::GetFuncPtrStub(MethodDesc * pMD, PrecodeType type) pPrecode = pNewPrecode; m_hashTable.Add(pPrecode); amt.SuppressRelease(); + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "FuncPtrStubs::GetFuncPtrStub pMD=%p type=%d - created new stub," + " target=" FMT_ADDR " setTargetAfter=%d\n", + pMD, type, DBG_ADDR(target), setTargetAfterAddingToHashTable)); } else { @@ -149,25 +156,35 @@ PCODE FuncPtrStubs::GetFuncPtrStub(MethodDesc * pMD, PrecodeType type) } } +#ifndef FEATURE_PORTABLE_ENTRYPOINTS if (setTargetAfterAddingToHashTable) { GCX_PREEMP(); _ASSERTE(pMD->IsVersionableWithVtableSlotBackpatch()); - PCODE temporaryEntryPoint = pMD->GetTemporaryEntryPoint(); + LoaderAllocator *mdLoaderAllocator = pMD->GetLoaderAllocator(); MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder; - // Set the funcptr stub's entry point to the current entry point inside the lock and after the funcptr stub is exposed, - // to synchronize with backpatching in MethodDesc::BackpatchEntryPointSlots() - PCODE entryPoint = pMD->GetMethodEntryPoint(); - if (entryPoint != temporaryEntryPoint) - { - // Need only patch the precode from the prestub, since if someone else managed to patch the precode already then its - // target would already be up-to-date - pPrecode->SetTargetInterlocked(entryPoint, TRUE /* fOnlyRedirectFromPrestub */); - } + // Register the funcptr precode's target slot in the backpatching table. This records the slot and + // immediately backpatches it to the current entry point. During non-final tiers, GetMethodEntryPoint() + // returns the temporary entry point (method's precode), so calls through the funcptr stub will flow + // through the method's precode to the current code. At final tier, the slot is updated to point + // directly to the final code along with all other registered entry point slots. + PCODE currentEntryPoint = pMD->GetMethodEntryPoint(); + LOG((LF_TIEREDCOMPILATION, LL_INFO10000, + "FuncPtrStubs::GetFuncPtrStub pMD=%p (%s::%s) - registering funcptr precode target in backpatch table," + " currentEntryPoint=" FMT_ADDR "\n", + pMD, pMD->m_pszDebugClassName, pMD->m_pszDebugMethodName, DBG_ADDR(currentEntryPoint))); + MethodDescBackpatchInfoTracker *backpatchTracker = mdLoaderAllocator->GetMethodDescBackpatchInfoTracker(); + backpatchTracker->AddSlotAndPatch_Locked( + pMD, + mdLoaderAllocator, + (TADDR)pPrecode->GetTargetSlot(), + EntryPointSlots::SlotType_Normal, + currentEntryPoint); } +#endif // !FEATURE_PORTABLE_ENTRYPOINTS return pPrecode->GetEntryPoint(); } diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 182b5e7c83b90e..c2d49d20632f30 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -3226,27 +3226,9 @@ bool MethodDesc::TryBackpatchEntryPointSlots( this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(entryPoint), DBG_ADDR(previousEntryPoint), IsVersionableWithVtableSlotBackpatch())); - if (IsVersionableWithVtableSlotBackpatch()) - { - // Backpatch the func ptr stub if it was created - FuncPtrStubs *funcPtrStubs = mdLoaderAllocator->GetFuncPtrStubsNoCreate(); - if (funcPtrStubs != nullptr) - { - Precode *funcPtrPrecode = funcPtrStubs->Lookup(this); - if (funcPtrPrecode != nullptr) - { - if (isPrestubEntryPoint) - { - funcPtrPrecode->ResetTargetInterlocked(); - } - else - { - funcPtrPrecode->SetTargetInterlocked(entryPoint, FALSE /* fOnlyRedirectFromPrestub */); - } - } - } - } - + // Backpatch all registered entry point slots. For methods versionable with vtable slot backpatch, + // this also handles funcptr precodes which are registered in the backpatch table at creation time + // (see FuncPtrStubs::GetFuncPtrStub). backpatchInfoTracker->Backpatch_Locked(this, entryPoint); // Set the entry point to backpatch inside the lock to synchronize with backpatching in MethodDesc::DoBackpatch(), and set @@ -3359,8 +3341,9 @@ void MethodDesc::ResetCodeEntryPoint() } // Sets the entry point for a backpatchable method during tiered compilation. -// For final tier: sets the owning vtable slot to codeEntryPoint and resets the precode to prestub, -// enabling lazy vtable slot discovery via DoBackpatch(). +// For final tier: sets the owning vtable slot to codeEntryPoint, backpatches all registered entry point +// slots (e.g. funcptr precodes) to codeEntryPoint, and resets the method's precode to prestub to enable +// lazy vtable slot discovery via DoBackpatch(). // For non-final tier: redirects the precode target to codeEntryPoint without modifying the vtable slot. // The vtable slot stays at the temporary entry point (precode), preventing DoBackpatch() from // recording vtable slots during non-final tiers. @@ -3370,6 +3353,7 @@ void MethodDesc::SetBackpatchableEntryPoint(PCODE codeEntryPoint, bool isFinalTi _ASSERTE(MayHaveEntryPointSlotsToBackpatch()); _ASSERTE(codeEntryPoint != (PCODE)NULL); _ASSERTE(!isFinalTier || !fOnlyRedirectFromPrestub); + _ASSERTE(!isFinalTier || MethodDescBackpatchInfoTracker::IsLockOwnedByCurrentThread()); Precode *precode = Precode::GetPrecodeFromEntryPoint(GetTemporaryEntryPoint()); if (isFinalTier) @@ -3377,9 +3361,17 @@ void MethodDesc::SetBackpatchableEntryPoint(PCODE codeEntryPoint, bool isFinalTi LOG((LF_TIEREDCOMPILATION, LL_INFO10000, "MethodDesc::SetBackpatchableEntryPoint pMD=%p (%s::%s) entryPoint=" FMT_ADDR " isFinalTier=true - setting owning vtable slot to final code," - " resetting precode to prestub for lazy DoBackpatch\n", + " backpatching registered slots, resetting precode to prestub for lazy DoBackpatch\n", this, m_pszDebugClassName, m_pszDebugMethodName, DBG_ADDR(codeEntryPoint))); SetMethodEntryPoint(codeEntryPoint); + + // Backpatch all registered entry point slots (e.g. funcptr precodes) to the final code. + // Vtable slots are not yet registered at this point — they are lazily discovered and recorded + // by DoBackpatch() when the next call goes through the precode → prestub path. + LoaderAllocator *mdLoaderAllocator = GetLoaderAllocator(); + MethodDescBackpatchInfoTracker *backpatchTracker = mdLoaderAllocator->GetMethodDescBackpatchInfoTracker(); + backpatchTracker->Backpatch_Locked(this, codeEntryPoint); + precode->ResetTargetInterlocked(); } else diff --git a/src/coreclr/vm/precode.cpp b/src/coreclr/vm/precode.cpp index 5b1422aa1c8273..6ff6542108f741 100644 --- a/src/coreclr/vm/precode.cpp +++ b/src/coreclr/vm/precode.cpp @@ -126,6 +126,28 @@ PCODE Precode::GetTarget() return target; } +#ifndef DACCESS_COMPILE +PTR_PCODE Precode::GetTargetSlot() +{ + LIMITED_METHOD_CONTRACT; + + PrecodeType precodeType = GetType(); + switch (precodeType) + { + case PRECODE_STUB: + return AsStubPrecode()->GetTargetSlot(); +#ifdef HAS_FIXUP_PRECODE + case PRECODE_FIXUP: + return AsFixupPrecode()->GetTargetSlot(); +#endif // HAS_FIXUP_PRECODE + + default: + UnexpectedPrecodeType("Precode::GetTargetSlot", precodeType); + return NULL; + } +} +#endif // !DACCESS_COMPILE + MethodDesc* Precode::GetMethodDesc(BOOL fSpeculative /*= FALSE*/) { CONTRACTL { diff --git a/src/coreclr/vm/precode.h b/src/coreclr/vm/precode.h index 3f8958ecea9b83..74f904f1f09173 100644 --- a/src/coreclr/vm/precode.h +++ b/src/coreclr/vm/precode.h @@ -151,6 +151,12 @@ struct StubPrecode return GetData()->Target; } + PCODE *GetTargetSlot() + { + LIMITED_METHOD_CONTRACT; + return &GetData()->Target; + } + BYTE GetType(); static BOOL IsStubPrecodeByASM(PCODE addr); diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index e0e8f077a53e96..3f21cd5e3e6ebc 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -101,9 +101,13 @@ PCODE MethodDesc::DoBackpatch(MethodTable * pMT, MethodTable *pDispatchingMT, bo _ASSERTE(!(pMT->IsInterface() && !IsStatic())); // Backpatching the funcptr stub: - // For methods versionable with vtable slot backpatch, a funcptr stub is guaranteed to point to the at-the-time - // current entry point shortly after creation, and backpatching it further is taken care of by - // MethodDesc::BackpatchEntryPointSlots() + // For methods versionable with vtable slot backpatch, funcptr precodes are registered in the + // backpatching table (see FuncPtrStubs::GetFuncPtrStub). During non-final tiers, the funcptr + // precode target is set to the temporary entry point (the method's precode), so calls through + // the funcptr stub flow through the method's precode to the current code. At final tier, the + // funcptr precode target is updated to the final code by Backpatch_Locked in + // SetBackpatchableEntryPoint(). Because the funcptr precode is in the backpatching table, it is + // also updated during rejit scenarios via BackpatchToResetEntryPointSlots(). // Backpatching the temporary entry point: // The temporary entry point is not directly backpatched for methods versionable with vtable slot backpatch.