You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Debugger Hot Reload with a profiler loaded: CanSetEnCBits refuses it, DOTNET_MODIFIABLE_ASSEMBLIES=debug allows it. Is that the supported route? #135152
Under a debugger, Hot Reload / Edit and Continue is refused for every module whenever a CLR profiler is loaded. The IDE reports ENC2011 … Changes are not allowed on the current module, which does not mention the profiler. Setting DOTNET_MODIFIABLE_ASSEMBLIES=debug in the debuggee's environment makes the same edit apply, with the same profiler loaded and no other change.
I would like to know whether that is a supported configuration:
If it is, could it be documented as the way to use debugger Hot Reload while a profiler is loaded, together with what the profiler then has to handle? And could the refusal point at it?
If it is not, could the docs say so and explain why? The profiler check that refuses the debugger's request is still evaluated, but the variable makes it irrelevant.
What I observed
Setup: VS Code with C# Dev Kit 3.40.210 and the C# extension 2.160.4, .NET 10.0.12 (linux-x64), SDK 10.0.301. The app was a net10.0 console app built Debug (Optimize=false), started with a coreclr launch configuration. It prints, in a loop, a value computed from a const int in a NoInlining method. I edited the constant and saved. The profiler was minprof.c, a small test profiler from the repro attached to #134714. That issue is a separate, unrelated debugger-transport crash; the profiler is reused here only because it is public and minimal. It was run with MINPROF_MODE=none. In this app it calls SetEventMask(COR_PRF_MONITOR_JIT_COMPILATION) in Initialize, matches no method and modifies nothing. Its environment variables (CORECLR_ENABLE_PROFILING, CORECLR_PROFILER, CORECLR_PROFILER_PATH) were set in the launch configuration's env, and so was DOTNET_MODIFIABLE_ASSEMBLIES in the third row. One run each:
Debuggee environment
Hot Reload result
no profiler
applied: the printed value changed on the next iteration
profiler loaded
refused: error ENC2011: Changes made in project '…' require restarting the application: Changes are not allowed on the current module. The app kept running the old code
applied: the printed value changed on the next iteration
Why it behaves this way, as far as I can read the source
The following is from reading main at 35bc6dafeee0; v10.0.12 is the same in substance. I have not traced which of these calls VS Code's debugger makes, beyond the results above.
The debugger's request is refused when a profiler is present. A debugger enables EnC from the module-load callback with ICorDebugModule2::SetJITCompilerFlags(CORDEBUG_JIT_ENABLE_ENC). That reaches DacDbiInterfaceImpl::SetCompilerFlags, which adds DACF_ENC_ENABLED only if CanSetEnCBits passes. CanSetEnCBits requires !CORProfilerPresent(); otherwise SetCompilerFlags returns CORDBG_S_NOT_ALL_BITS_SET, a success code. The cDAC has the same rule (DacDbiImpl.cs).
The variable enables EnC at module load, and that path does not consult the profiler.Module::SetDebuggerInfoBits calls EnableEditAndContinue() on an EnC-capable module when the variable is not none and either the debugger asked for DACF_ENC_ENABLED or the variable is debug and JIT optimizations are disabled. It runs when the module is attached to its assembly (assembly.cpp), with bits taken from DebuggableAttribute. For a Debug build those bits disable optimizations. So with DOTNET_MODIFIABLE_ASSEMBLIES=debug the module is EnC-enabled before any debugger is involved. SetCompilerFlags calls SetDebuggerInfoBits again with the debugger's bits, and the same debug clause applies there too. Nothing clears IS_EDIT_AND_CONTINUE once it is set.
So the profiler check is still evaluated and still refuses the request, but it no longer decides anything.GetJITCompilerFlags reports CORDEBUG_JIT_ENABLE_ENC from the module's actual state (GetCompilerFlags). Applying an edit checks only that state (Debugger::ApplyChangesAndSendResult). The cDAC's SetDebuggerInfoBits has the same debug clause (Loader_1.cs).
In other words, CanSetEnCBits gates the debugger's request, not whether the module is editable. The variable's description in clrconfigvalues.h says it "Enables hot reload on debug built assemblies with the 'debug' keyword". In a review comment on #123744, about replacing ForceEnc (comment):
ForceEnc has been undocumented testing hack that does not work correctly. We have introduced ModifiableAssemblies env var as something that works correctly and that is documented.
I could not find where it is documented. learn.microsoft.com and the profiling docs do not seem to mention it.
Questions
Is debugger EnC with a profiler loaded supported when the debuggee sets DOTNET_MODIFIABLE_ASSEMBLIES=debug? If yes, could that be documented: for the variable itself, and in the profiling docs as what a profiler author should tell users?
What does the CORProfilerPresent() term in CanSetEnCBits still protect? The comment in debugger.cpp says "you can't profile with EnC". If that still holds, the variable defeats it, and perhaps the load-time path should honor it too. If it no longer holds, the debugger's own request could be allowed, so that users do not need the variable.
Diagnostics. Today the only signal is a success code, CORDBG_S_NOT_ALL_BITS_SET, which does not say why, so the IDE's message cannot name the profiler. Deprecate ForceEnc and migrate scenarios to DOTNET_MODIFIABLE_ASSEMBLIES #124017 records the same kind of ask for the attach case: a refusal that suggests DOTNET_MODIFIABLE_ASSEMBLIES=debug. Could the runtime let a debugger find out that the reason is a loaded profiler, so the message can say so and point at the variable?
What should a profiler expect for an edited method? From the source, and not all of it measured, these are what a profiler would need to handle. It would help to have them confirmed and written down:
ReJIT is unavailable in those modules.RequestReJIT on a method in an EnC-enabled module fails with CORPROF_E_MODULE_IS_ENC (rejit.cpp; RequestReJITWithInliners silently fails for debug modules if COMPLUS_ForceEnc is enabled #91963: "EnC and ReJIT use different methods to version code, it is expected that they won't work together."). With the variable set, this applies to every Debug-built module from load, whether or not a debugger attaches.
On .NET 10, edits go through the module's dynamic-IL table.ApplyEditAndContinue stores the new body with SetDynamicIL (encee.cpp at v10.0.12). ICorProfilerInfo::SetILFunctionBody writes to the same table. The edited method is compiled again as the default version, so a profiler gets JITCompilationStarted. A profiler that rewrites IL must take the body from GetILFunctionBody at that callback. If it reinstalls IL it built for the earlier version, it overwrites the edit with IL that no longer matches the updated metadata.
On main (since Use CodeVersioning for EnC #130159, in 11.0 RC1), each edit is an IL code version with a non-zero id (encee.cpp). The prestub raises ReJITCompilationStarted/Finished for any non-zero id (prestub.cpp), and only to profilers that set COR_PRF_ENABLE_REJIT (profilepriv.inl). If I read this correctly, a profiler on 11.0 gets either a ReJIT notification with an id it never requested, or no compilation-started notification at all, where 10.0 sends JITCompilationStarted. The ETW events deliberately keep reporting 0 for EnC versions (eventtrace.cpp), and the profiler callbacks do not seem to. Separately, GetILFunctionBody still reads only the dynamic-IL table and then the metadata RVA (proftoeeinterfaceimpl.cpp), so I am not sure which body it returns for an edited method on 11.0. I have not run this on 11.0. Is that change intended for profilers?
Workarounds
Set DOTNET_MODIFIABLE_ASSEMBLIES=debug in the debuggee's environment (in the launch configuration), accepting the ReJIT restriction above.
Use dotnet watch, which sets the variable itself.
Restart after an edit.
Note
Parts of this issue were drafted with AI assistance (Anthropic Claude) and reviewed by the submitter.
Summary
Under a debugger, Hot Reload / Edit and Continue is refused for every module whenever a CLR profiler is loaded. The IDE reports
ENC2011 … Changes are not allowed on the current module, which does not mention the profiler. SettingDOTNET_MODIFIABLE_ASSEMBLIES=debugin the debuggee's environment makes the same edit apply, with the same profiler loaded and no other change.I would like to know whether that is a supported configuration:
What I observed
Setup: VS Code with C# Dev Kit 3.40.210 and the C# extension 2.160.4, .NET 10.0.12 (linux-x64), SDK 10.0.301. The app was a
net10.0console app built Debug (Optimize=false), started with acoreclrlaunch configuration. It prints, in a loop, a value computed from aconst intin aNoInliningmethod. I edited the constant and saved. The profiler wasminprof.c, a small test profiler from the repro attached to #134714. That issue is a separate, unrelated debugger-transport crash; the profiler is reused here only because it is public and minimal. It was run withMINPROF_MODE=none. In this app it callsSetEventMask(COR_PRF_MONITOR_JIT_COMPILATION)inInitialize, matches no method and modifies nothing. Its environment variables (CORECLR_ENABLE_PROFILING,CORECLR_PROFILER,CORECLR_PROFILER_PATH) were set in the launch configuration'senv, and so wasDOTNET_MODIFIABLE_ASSEMBLIESin the third row. One run each:error ENC2011: Changes made in project '…' require restarting the application: Changes are not allowed on the current module.The app kept running the old codeDOTNET_MODIFIABLE_ASSEMBLIES=debugWhy it behaves this way, as far as I can read the source
The following is from reading
mainat35bc6dafeee0;v10.0.12is the same in substance. I have not traced which of these calls VS Code's debugger makes, beyond the results above.ICorDebugModule2::SetJITCompilerFlags(CORDEBUG_JIT_ENABLE_ENC). That reachesDacDbiInterfaceImpl::SetCompilerFlags, which addsDACF_ENC_ENABLEDonly ifCanSetEnCBitspasses.CanSetEnCBitsrequires!CORProfilerPresent(); otherwiseSetCompilerFlagsreturnsCORDBG_S_NOT_ALL_BITS_SET, a success code. The cDAC has the same rule (DacDbiImpl.cs).Module::SetDebuggerInfoBitscallsEnableEditAndContinue()on an EnC-capable module when the variable is notnoneand either the debugger asked forDACF_ENC_ENABLEDor the variable isdebugand JIT optimizations are disabled. It runs when the module is attached to its assembly (assembly.cpp), with bits taken fromDebuggableAttribute. For a Debug build those bits disable optimizations. So withDOTNET_MODIFIABLE_ASSEMBLIES=debugthe module is EnC-enabled before any debugger is involved.SetCompilerFlagscallsSetDebuggerInfoBitsagain with the debugger's bits, and the samedebugclause applies there too. Nothing clearsIS_EDIT_AND_CONTINUEonce it is set.GetJITCompilerFlagsreportsCORDEBUG_JIT_ENABLE_ENCfrom the module's actual state (GetCompilerFlags). Applying an edit checks only that state (Debugger::ApplyChangesAndSendResult). The cDAC'sSetDebuggerInfoBitshas the samedebugclause (Loader_1.cs).In other words,
CanSetEnCBitsgates the debugger's request, not whether the module is editable. The variable's description inclrconfigvalues.hsays it "Enables hot reload on debug built assemblies with the 'debug' keyword". In a review comment on #123744, about replacingForceEnc(comment):I could not find where it is documented. learn.microsoft.com and the profiling docs do not seem to mention it.
Questions
DOTNET_MODIFIABLE_ASSEMBLIES=debug? If yes, could that be documented: for the variable itself, and in the profiling docs as what a profiler author should tell users?CORProfilerPresent()term inCanSetEnCBitsstill protect? The comment indebugger.cppsays "you can't profile with EnC". If that still holds, the variable defeats it, and perhaps the load-time path should honor it too. If it no longer holds, the debugger's own request could be allowed, so that users do not need the variable.CORDBG_S_NOT_ALL_BITS_SET, which does not say why, so the IDE's message cannot name the profiler. Deprecate ForceEnc and migrate scenarios to DOTNET_MODIFIABLE_ASSEMBLIES #124017 records the same kind of ask for the attach case: a refusal that suggestsDOTNET_MODIFIABLE_ASSEMBLIES=debug. Could the runtime let a debugger find out that the reason is a loaded profiler, so the message can say so and point at the variable?RequestReJITon a method in an EnC-enabled module fails withCORPROF_E_MODULE_IS_ENC(rejit.cpp;RequestReJITWithInlinerssilently fails for debug modules ifCOMPLUS_ForceEncis enabled #91963: "EnC and ReJIT use different methods to version code, it is expected that they won't work together."). With the variable set, this applies to every Debug-built module from load, whether or not a debugger attaches.ApplyEditAndContinuestores the new body withSetDynamicIL(encee.cppatv10.0.12).ICorProfilerInfo::SetILFunctionBodywrites to the same table. The edited method is compiled again as the default version, so a profiler getsJITCompilationStarted. A profiler that rewrites IL must take the body fromGetILFunctionBodyat that callback. If it reinstalls IL it built for the earlier version, it overwrites the edit with IL that no longer matches the updated metadata.main(since Use CodeVersioning for EnC #130159, in 11.0 RC1), each edit is an IL code version with a non-zero id (encee.cpp). The prestub raisesReJITCompilationStarted/Finishedfor any non-zero id (prestub.cpp), and only to profilers that setCOR_PRF_ENABLE_REJIT(profilepriv.inl). If I read this correctly, a profiler on 11.0 gets either a ReJIT notification with an id it never requested, or no compilation-started notification at all, where 10.0 sendsJITCompilationStarted. The ETW events deliberately keep reporting 0 for EnC versions (eventtrace.cpp), and the profiler callbacks do not seem to. Separately,GetILFunctionBodystill reads only the dynamic-IL table and then the metadata RVA (proftoeeinterfaceimpl.cpp), so I am not sure which body it returns for an edited method on 11.0. I have not run this on 11.0. Is that change intended for profilers?Workarounds
DOTNET_MODIFIABLE_ASSEMBLIES=debugin the debuggee's environment (in the launch configuration), accepting the ReJIT restriction above.dotnet watch, which sets the variable itself.Note
Parts of this issue were drafted with AI assistance (Anthropic Claude) and reviewed by the submitter.