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. diff --git a/src/coreclr/vm/callcounting.cpp b/src/coreclr/vm/callcounting.cpp index 2fe7f4aa3d2731..4395d5be919125 100644 --- a/src/coreclr/vm/callcounting.cpp +++ b/src/coreclr/vm/callcounting.cpp @@ -373,29 +373,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 @@ -514,7 +491,14 @@ bool CallCountingManager::SetCodeEntryPoint( activeCodeVersion.IsFinalTier() || !g_pConfig->TieredCompilation_CallCounting()) { - methodDesc->SetCodeEntryPoint(codeEntryPoint); + if (activeCodeVersion.IsFinalTier() && methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + methodDesc->SetBackpatchableEntryPoint(codeEntryPoint, true /* isFinalTier */); + } + else + { + methodDesc->SetCodeEntryPoint(codeEntryPoint); + } return true; } @@ -534,7 +518,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()) + { + methodDesc->SetBackpatchableEntryPoint(codeEntryPoint, activeCodeVersion.IsFinalTier()); + } + else + { + methodDesc->SetCodeEntryPoint(codeEntryPoint); + } return true; } @@ -572,7 +563,14 @@ bool CallCountingManager::SetCodeEntryPoint( ->GetTieredCompilationManager() ->AsyncPromoteToTier1(activeCodeVersion, createTieringBackgroundWorkerRef); } - methodDesc->SetCodeEntryPoint(codeEntryPoint); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + methodDesc->SetBackpatchableEntryPoint(codeEntryPoint, false /* isFinalTier */); + } + else + { + methodDesc->SetCodeEntryPoint(codeEntryPoint); + } callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); return true; } while (false); @@ -635,51 +633,22 @@ 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 + methodDesc->SetBackpatchableEntryPoint(callCountingCodeEntryPoint, false /* isFinalTier */); } else { _ASSERTE(methodDesc->IsVersionableWithPrecode()); + methodDesc->SetCodeEntryPoint(callCountingCodeEntryPoint); } - methodDesc->SetCodeEntryPoint(callCountingCodeEntryPoint); callCountingInfo->SetStage(CallCountingInfo::Stage::StubMayBeActive); return true; } @@ -848,7 +817,14 @@ void CallCountingManager::CompleteCallCounting() { if (activeCodeVersion == codeVersion) { - methodDesc->SetCodeEntryPoint(activeCodeVersion.GetNativeCode()); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + methodDesc->SetBackpatchableEntryPoint(activeCodeVersion.GetNativeCode(), false /* isFinalTier */); + } + else + { + methodDesc->SetCodeEntryPoint(activeCodeVersion.GetNativeCode()); + } break; } @@ -863,12 +839,23 @@ void CallCountingManager::CompleteCallCounting() PCODE activeNativeCode = activeCodeVersion.GetNativeCode(); if (activeNativeCode != 0) { - methodDesc->SetCodeEntryPoint(activeNativeCode); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + methodDesc->SetBackpatchableEntryPoint(activeNativeCode, activeCodeVersion.IsFinalTier()); + } + else + { + methodDesc->SetCodeEntryPoint(activeNativeCode); + } break; } } methodDesc->ResetCodeEntryPoint(); + if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) + { + Precode::GetPrecodeFromEntryPoint(methodDesc->GetTemporaryEntryPoint())->ResetTargetInterlocked(); + } } while (false); callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); @@ -1006,7 +993,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); } @@ -1026,14 +1021,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(); - } } } @@ -1061,7 +1048,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(); @@ -1081,14 +1067,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); @@ -1139,24 +1125,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 254f22d5eb2dce..8c87758de7f939 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 ------------- @@ -280,27 +285,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 @@ -335,7 +319,6 @@ class CallCountingManager private: CallCountingInfoByCodeVersionHash m_callCountingInfoByCodeVersionHash; CallCountingStubAllocator m_callCountingStubAllocator; - MethodDescForwarderStubHash m_methodDescForwarderStubHash; SArray m_callCountingInfosPendingCompletion; public: diff --git a/src/coreclr/vm/codeversion.cpp b/src/coreclr/vm/codeversion.cpp index 50bae39dc7046d..f71f07b7c25843 100644 --- a/src/coreclr/vm/codeversion.cpp +++ b/src/coreclr/vm/codeversion.cpp @@ -1851,9 +1851,16 @@ PCODE CodeVersionManager::PublishVersionableCodeIfNecessary( if (doPublish) { bool mayHaveEntryPointSlotsToBackpatch2 = pMethodDesc->MayHaveEntryPointSlotsToBackpatch(); - MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder2( - mayHaveEntryPointSlotsToBackpatch2); - pMethodDesc->TrySetInitialCodeEntryPointForVersionableMethod(pCode, mayHaveEntryPointSlotsToBackpatch2); + if (handleCallCountingForFirstCall && mayHaveEntryPointSlotsToBackpatch2) + { + pMethodDesc->SetBackpatchableEntryPoint(pCode, false /* isFinalTier */, TRUE /* fOnlyRedirectFromPrestub */); + } + else + { + MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder2( + mayHaveEntryPointSlotsToBackpatch2); + pMethodDesc->TrySetInitialCodeEntryPointForVersionableMethod(pCode, mayHaveEntryPointSlotsToBackpatch2); + } } else { 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 32d9a717280647..c2d49d20632f30 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -3188,7 +3188,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) @@ -3207,35 +3207,28 @@ 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; } - 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 */); - } - } - } - } + 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())); + // 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 @@ -3273,11 +3266,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; } @@ -3285,6 +3282,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 @@ -3298,12 +3297,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; } @@ -3336,6 +3339,52 @@ 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, 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. +void MethodDesc::SetBackpatchableEntryPoint(PCODE codeEntryPoint, bool isFinalTier, BOOL fOnlyRedirectFromPrestub) +{ + WRAPPER_NO_CONTRACT; + _ASSERTE(MayHaveEntryPointSlotsToBackpatch()); + _ASSERTE(codeEntryPoint != (PCODE)NULL); + _ASSERTE(!isFinalTier || !fOnlyRedirectFromPrestub); + _ASSERTE(!isFinalTier || MethodDescBackpatchInfoTracker::IsLockOwnedByCurrentThread()); + + 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," + " 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 + { + 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 5bcea131c50eb0..68804cfd448aad 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1453,6 +1453,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/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) 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 b46b1bfefc7b97..3f21cd5e3e6ebc 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -101,15 +101,32 @@ 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 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 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. + // + // 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 71231e36426b27..ac67a1d0fcc791 100644 --- a/src/coreclr/vm/tieredcompilation.cpp +++ b/src/coreclr/vm/tieredcompilation.cpp @@ -228,7 +228,14 @@ 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()) + { + pMethodDesc->SetBackpatchableEntryPoint(codeEntryPoint, false /* isFinalTier */); + } + else + { + pMethodDesc->SetCodeEntryPoint(codeEntryPoint); + } _ASSERTE(m_methodsPendingCountingForTier1 != nullptr); m_methodsPendingCountingForTier1->Append(pMethodDesc); return true;