Skip to content

Debugger Hot Reload with a profiler loaded: CanSetEnCBits refuses it, DOTNET_MODIFIABLE_ASSEMBLIES=debug allows it. Is that the supported route? #135152

Description

@pcshrosbree

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. 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
profiler loaded + DOTNET_MODIFIABLE_ASSEMBLIES=debug 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.

  1. 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).
  2. 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.
  3. 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

  1. 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?
  2. 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.
  3. 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?
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions