Add cDAC JIT notification contract with lazy table allocation#127114
Add cDAC JIT notification contract with lazy table allocation#127114max-charlamb wants to merge 3 commits intomainfrom
Conversation
c052fb3 to
d951242
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs:124
- There’s a stray XML doc line
/// </summary>after the newTryCreateoverload. This produces malformed XML documentation (and can fail the build if warnings are treated as errors). Remove it or restore the missing opening documentation element for the following member.
}
/// </summary>
/// <param name="contractDescriptor">The contract descriptor to use for this target</param>
/// <param name="globalPointerValues">The values for any global pointers specified in the contract descriptor.</param>
/// <param name="readFromTarget">A callback to read memory blocks at a given address from the target</param>
/// <param name="writeToTarget">A callback to write memory blocks at a given address to the target</param>
/// <param name="getThreadContext">A callback to fetch a thread's context</param>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Two DAC-parity fixes surfaced by self-review of PR #127114: 1. Batch IXCLRDataProcess::SetCodeNotifications now rejects upfront with E_OUTOFMEMORY when numTokens exceeds the table capacity. Matches the legacy DAC (daccess.cpp: 'numTokens > jn.GetTableSize()') and prevents partial writes when a batch overflows mid-loop. 2. ICodeNotifications.GetCodeNotification now throws InvalidOperationException (mapped to E_OUTOFMEMORY by wrappers) when the table has not been allocated, matching legacy DAC behavior (JITNotifications::IsActive()). Previously returned CLRDATA_METHNOTIFY_NONE, which was an unintentional behavioral divergence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two DAC-parity fixes surfaced by self-review of PR #127114: 1. Batch IXCLRDataProcess::SetCodeNotifications now rejects upfront with E_OUTOFMEMORY when numTokens exceeds the table capacity. Matches the legacy DAC (daccess.cpp: 'numTokens > jn.GetTableSize()') and prevents partial writes when a batch overflows mid-loop. 2. ICodeNotifications.GetCodeNotification now throws InvalidOperationException (mapped to E_OUTOFMEMORY by wrappers) when the table has not been allocated, matching legacy DAC behavior (JITNotifications::IsActive()). Previously returned CLRDATA_METHNOTIFY_NONE, which was an unintentional behavioral divergence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
c8403cb to
bfa14cc
Compare
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
bfa14cc to
851dc52
Compare
Move the no-upfront-capacity-check note off the ICodeNotifications contract interface XML doc and onto the IXCLRDataProcess.SetCodeNotifications implementation, where the divergence actually lives: the legacy DAC checks numTokens against table size up front and returns E_OUTOFMEMORY atomically, while the cDAC writes entries one at a time and may leave partial writes before returning E_FAIL on overflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…ert lazy-alloc size - CodeNotifications_1.SetCodeNotification clear path: replace single Length-- with a trailing-trim while-loop. The single decrement was insufficient when an interior hole already existed adjacent to the cleared tail entry, leaving Length inflated past the highest active index. - CodeNotificationsTests.SetCodeNotification_NullTable_LazyAllocates_ThenWorks: capture the requested allocation size in the AllocateMemory delegate and assert it equals EntrySize * (TableCapacity + 1) to guard the byte-size computation against regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| ClrDataAddress addr; | ||
| int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr); | ||
| allocatedAddress = (ulong)addr; |
There was a problem hiding this comment.
In CLRDataCreateInstanceImpl, the allocVirtual delegate casts size to uint and uses an uninitialized ClrDataAddress addr. If size ever exceeds uint.MaxValue this will truncate, and if AllocVirtual fails without writing addr the returned allocatedAddress may contain garbage. Please mirror the safer pattern used earlier in Init: validate the size fits in uint, initialize addr to 0, and only set allocatedAddress on success (or explicitly zero it on failure).
| ClrDataAddress addr; | |
| int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr); | |
| allocatedAddress = (ulong)addr; | |
| allocatedAddress = 0; | |
| if (size > uint.MaxValue) | |
| { | |
| return HResults.E_INVALIDARG; | |
| } | |
| ClrDataAddress addr = 0; | |
| int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr); | |
| if (result >= 0) | |
| { | |
| allocatedAddress = (ulong)addr; | |
| } |
|
|
||
| void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags) | ||
| { |
There was a problem hiding this comment.
SetCodeNotification accepts any CodeNotificationKind value and later casts it to ushort when writing the table. That means undefined bits can be silently truncated or persisted into the target table if a non-validated value is passed (e.g., by non-COM consumers). Consider validating that flags contains only the supported bits (Generated/Discarded) and throwing ArgumentException for invalid values to keep contract behavior well-defined and aligned with the documented COM-layer validation.
| void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags) | |
| { | |
| private static void ValidateFlags(CodeNotificationKind flags) | |
| { | |
| CodeNotificationKind validFlags = CodeNotificationKind.Generated | CodeNotificationKind.Discarded; | |
| if ((flags & ~validFlags) != 0) | |
| { | |
| throw new ArgumentException("Only Generated and Discarded notification flags are supported.", nameof(flags)); | |
| } | |
| } | |
| void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags) | |
| { | |
| ValidateFlags(flags); |
| void ICodeNotifications.SetAllCodeNotifications(TargetPointer module, CodeNotificationKind flags) | ||
| { | ||
| // When the table has not been allocated there are no entries to update, so this is a | ||
| // no-op. Matches native JITNotifications::SetAllNotifications (util.cpp:1112). | ||
| TableView? maybeView = PrepareTable(allocateIfMissing: false); | ||
| if (maybeView is null) | ||
| return; |
There was a problem hiding this comment.
SetAllCodeNotifications writes (ushort)flags into each entry without validating that flags contains only the supported bits. This can allow invalid values to be stored in the target table if the contract is used directly. Recommend validating flags (same mask used by SetCodeNotification) and throwing ArgumentException for unsupported bits.
🤖 Copilot Code Review — PR #127114Note This review was AI-generated by GitHub Copilot using multi-model review (Claude Opus 4.6 primary + Claude Sonnet 4.5 and GPT-5.3-Codex sub-agents). Holistic AssessmentMotivation: This PR adds a new Approach: The approach is sound: a new contract ( Summary: Detailed Findings
|
Note
This PR was created with assistance from GitHub Copilot.
Summary
Implement the cDAC JIT code notification contract, removing the legacy DAC allowlist fallback for
SetCodeNotificationand the fourIXCLRDataMethodDefinitionfallbacks. Adds a new dedicatedICodeNotificationscontract (c1) coveringGetCodeNotification,SetCodeNotification, andSetAllCodeNotifications.Key Design Decisions
ICodeNotificationscontract: JIT code notification table management is split out of the existing read-onlyINotificationscontract (which now only decodes debugger events).INotifications= read-only event decoding;ICodeNotifications= mutable in-target table management.ICLRDataTarget2::AllocVirtual. This avoids a ~24 KB per-process memory cost for apps that never use JIT notifications.JITNotificationTableSizeglobal descriptor. Slot 0 of the table stores only thelength(in itsmethodTokenfield);clrModuleis unused. This avoids coupling the runtime's table-allocation path to the cDAC's direct-read model — no runtime changes are needed to seed bookkeeping fields.g_pNotificationTablestarts as NULL until the first non-trivialSetCodeNotificationcall. The contract treats a NULL table as equivalent to "no entries":GetCodeNotification→None(same observable state as "method not registered"). Deliberate simplification from the legacy DAC, which returnedE_OUTOFMEMORYviaJITNotifications::IsActive(); the information to the caller is identical in both cases.SetAllCodeNotifications→ no-op.SetCodeNotificationwithNone→ no-op (nothing to clear).SetCodeNotificationwith flags → lazy-allocates the table viaICLRDataTarget2::AllocVirtual, then sets.SetCodeNotificationthrowsCOMExceptionwithHResult == E_FAILwhen no free slot is available, matching the legacy DAC'sSetNotificationfailure path.IXCLRDataProcess::SetCodeNotificationsdoes not reject oversize batches upfront. If a batch would overflow the in-target table, the contract writes as many entries as fit and then throwsE_FAILfrom the firstSetCodeNotificationthat cannot allocate a slot. This deliberately diverges from the legacy DAC (which rejected withE_OUTOFMEMORYbefore writing anything); rather than expose a capacity guarantee that could drift from the runtime's actual behavior, the contract surfaces the same failure through the normal per-entry path. Invalid flag combinations still throwArgumentException(→E_INVALIDARG).SetAllCodeNotificationslength trim: Deliberately diverges fromJITNotifications::SetAllNotificationsinsrc/coreclr/vm/util.cpp. The native algorithm decrementsLengthfor every freed slot in[0, OldLength), which can trimLengthbelow the index of still-active entries from other modules, making them unreachable viaTryFindEntry. The cDAC trims only trailing free entries. Documented with an explanatory comment in both code and docs.Changes
Target Abstraction
AllocateMemory(uint size)virtual method toTarget(defaults toNotImplementedException).WriteNUInt(ulong, TargetNUInt)abstract method toTarget;WriteNUIntField/WritePointerFieldextensions now delegate toTarget.WriteNUInt/WritePointerinstead of inlining pointer-size branching.AllocVirtualDelegate(non-nullable) toContractDescriptorTarget.TryCreate/Create; callers that cannot allocate supply anE_NOTIMPL-returning or throwing delegate.ICLRDataTarget2::AllocVirtualthroughCLRDataCreateInstanceand thecdac_reader_initentrypoint.cdac_reader.h/cdac.cpp/cdacstress.cppto pass an optionalalloc_virtualcallback.Runtime
JITNotification+ globalsJITNotificationTable,JITNotificationTableSize.CDAC_GLOBAL_CONTRACT(CodeNotifications, c1).InitializeJITNotificationTable— default-construction zeroes slot 0'smethodToken(length) naturally, and capacity lives in the global.Contract Implementation
ICodeNotificationsabstraction andCodeNotifications_1implementation.Data.JITNotificationIData class with typed read/write helpers and caching.TableViewholds aData.JITNotificationIData for the sentinel slot (index 0) and routes length reads/writes through it; capacity is read from theJITNotificationTableSizeglobal.E_FAIL.Legacy COM Wrappers
IXCLRDataMethodDefinition::GetCodeNotification/SetCodeNotificationonClrDataMethodDefinition.IXCLRDataProcess::SetCodeNotifications/GetCodeNotifications/SetAllCodeNotificationsonSOSDacImpl.CLRDATA_METHNOTIFY_GENERATED/DISCARDED); added aCodeNotificationFlagsConverterbetween the COM flag bits and theCodeNotificationKindenum.SetCodeNotificationand the threeIXCLRDataMethodDefinitionentries (StartEnumInstances,GetName,HasClassOrMethodInstantiation) from theLegacyFallbackHelperallowlist.Tests
NotificationsTests(event decoding) andCodeNotificationsTests(table ops, null-table, lazy-allocate, module filtering, batch, table-full).TestPlaceholderTargetextended with anAllocateMemoryDelegatehook and aWriteNUIntoverride.ContractDescriptorBuilder,DumpTestBase, andStressLogAnalyzerupdated for the new non-nullableAllocVirtualDelegatecreation API.Documentation
docs/design/datacontracts/CodeNotifications.mdwith APIs, data descriptors, pseudocode, and the divergence note onSetAllCodeNotifications.docs/design/datacontracts/Notifications.mdto event decoding and cross-linked toCodeNotifications.md.Fixes #126760