Skip to content

Add runtime API to load Hot Reload deltas #45689

@lambdageek

Description

@lambdageek

Background and Motivation

As part of #44806 and #45629 contributing to the overall inner loop performance theme for .NET6, the runtime needs to expose an API to accept and apply hot reload deltas at runtime from workloads and frameworks that want to enable this capability.

Proposed API

namespace System.Reflection.Metadata
{
    public static partial class AssemblyExtensions
    {
        /// <summary>
        /// Hot reload update API
        /// 
        /// Applies an update to the given assembly using the metadata, IL and PDB deltas. Currently executing
        /// methods will continue to use the existing IL. New executions of modified methods will use the new
        /// IL. The supported changes are runtime specific - different .NET runtimes may have different
        /// limitations. The runtime makes no guarantees if the delta includes unsupported changes.
        /// </summary>
        /// <param name="assembly">The assembly to update</param>
        /// <param name="metadataDelta">The metadata changes</param>
        /// <param name="ilDelta">The IL changes</param>
        /// <param name="pdbDelta">The PDB changes. Current not supported on .NET Core</param>
        /// <exception cref="ArgumentNullException">if assembly parameter is null</exception>
        /// <exception cref="...">others TBD</exception>
        public static void ApplyUpdate(Assembly assembly, ReadOnlySpan<byte> metadataDelta, ReadOnlySpan<byte> ilDelta, ReadOnlySpan<byte> pdbDelta = default);
    }
}

Usage Examples

A framework implementing a code reload injector would use the API to deliver deltas to the runtime:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;

public abstract class AbstractHotReloadFramework
{
     // Asynchronously receive sets of related changes from some external source
    public abstract IAsyncEnumerable<IEnumerable<AssemblyLoadContext.HotReloadDelta> ReceiveChangesAsync(CancellationToken ct = default);

    public abstract Task QueueRefresh();
	
    public async Task Loop(CancellationToken ct = default)
    {
        await foreach (var change in ReceiveChangesAsync(ct)) {
	    if (ct.IsCancellationRequested)
		break;
            AssemblyExtensions.ApplyUpdate(change.Assembly, change.MetadataDelta, change.ILDelta, change.PDBDelta);
	    var _t = Task.Run (() => QueueRefresh());
	}
    }
}

Alternative Designs

An atomic update of multiple assemblies deltas. This is for multi-assembly edits (i.e. adding a new method in Assembly A and invoking in Assembly B) and allows atomic updates of the same code across all AssemblyLoadContexts. For the later, it assumes that the caller can find all the Assembly instances of the updated code across all the current AssemblyLoadContexts.

namespace System.Runtime.Loader {
public partial class AssemblyLoadContext {
/// <summary>
/// Contains a hot reload assembly delta
/// </summary>
public struct HotReloadDelta
{
    /// <summary>
    /// The assembly to update
    /// </summary>
    public Assembly Assembly { get; }

    /// <summary>
    /// The metadata changes
    /// </summary>
    public ReadOnlyMemory<byte> MetadataDelta { get; }

    /// <summary>
    /// The IL changes
    /// </summary>
    public ReadOnlyMemory<byte> ILDelta { get; }

    /// <summary>
    /// The PDB changes
    /// </summary>
    public ReadOnlyMemory<byte> PDBDelta { get; }

    public HotReloadDelta(Assembly assembly, ReadOnlyMemory<byte> metadataDelta, Memory<byte> ilDelta, ReadOnlyMemory<byte> pdbDelta)
    {
        Assembly = assembly;
        MetadataDelta = metadataDelta;
        ILDelta = ilDelta;
        PDBDelta = pdbDelta;
    }
}

/// <summary>
/// Hot reload update API
/// 
/// Applies an update to the given assembly using the metadata, IL and PDB. Currently executing
/// methods will continue to use the existing IL. New executions of modified methods will use 
/// the new IL. The supported changes are runtime specific - different .NET runtimes may have 
/// different limitations. The runtime makes no guarantees if the delta has unsupported changes.
/// </summary>
/// <param name="deltas">list of deltas</param>
/// <returns>true for success and false for failure of any update. Some of the updates could have been completed.</returns>
public static bool ApplyHotReloadUpdate(IEnumerable<HotReloadDelta> deltas);
}
}
  • If we want to support a transactional model where deltas that affect multiple assemblies are applied simultaneously, a more complex design may be needed.

        using System;
        using System.Collections.Generic;
        using System.Reflection;
        using System.Runtime.Loader;
    
    namespace System.Runtime.CompilerServices
    {
        public partial class RuntimeHelpers
        {
    	[Flags]
    	public enum HotReloadSupportedFlags : byte
    	{
    	    None = 0,
    	    /// Method body replacement supported
    	    CodeReload = 1,
    	    /// Metadata additions supported
    	    AddedTypes = 2,
    	    /// Metadata changes supported
    	    ModifiedTypes = 4,
    
    	    /// Running methods are updated in place
    	    OnStackReplacement = 128,
    	};
    		
    	///
    	/// Initializes hot reload in the runtime
    	/// Returns a HotReloadInjector object to the first caller.
    	///
    	/// Returns null to every subsequent caller
    	///
    	/// Throws NotSupportedException if this runtime does not support hot reload
    	public static HotReloadInjector EnableHotReload()
    	    => throw new NotImplementedException();
    
    
    	/// Returns a value indicating whether the runtime supports hot reload
    	public static HotReloadSupportedFlags IsHotReloadSupported
    	{
    	    get => throw new NotImplementedException();
    	}
    
        }
    
        public sealed partial class HotReloadInjector
        {
    	/// Begin a hot reload transaction
    	///
    	/// Throws InvalidOperationException if there is already a transaction in progress.
    	///
    	/// If preserveDiagnostics is true, then a call to
    	/// GetCommitFailureDiagnostics following a TryCommitTransaction call
    	/// will return some diagnostic messages about the failure.
    	public HotReloadTransaction BeginHotReload (AssemblyLoadContext alc = default, bool preserveDiagnostics = false)
    	    => throw new NotImplementedException();
    
        }
    
        public sealed partial class HotReloadTransaction : IDisposable
        {
    	/// Add a metadata delta for the given assembly to the transaction.
    	///
    	/// Throws ArgumentException if the assembly is from the wrong ALC.
    	public void AddDelta(Assembly assm, byte[] dmeta, byte[] dil, byte[] dpdb = null)
    	    => throw new NotImplementedException();
    
    	/// Cancel the transaction, discard all the accumulated deltas
    	///
    	/// Throws InvalidOperationException if the transaction is already
    	/// canceled or committed.
    	public void CancelTransaction()
    	    => throw new NotImplementedException();
    
    	/// Apply the accumulated deltas.
    	///
    	/// Returns true on success, or false if the transaction couldn't be
    	/// applied but the runtime is left in a consistent state.
    	///
    	/// If a transaction cannot be applied, and the runtime cannot be left
    	/// in a consistent state, an ExecutionEngineException is thrown.  It
    	/// is recommended that the application shutdown as soon as possible.
    	public bool TryCommitTransaction()
    	    => throw new NotImplementedException();
    
    	/// If TryCommitTransaction() failed and preserveDiagnostics was true when the transaction was started, return a list of diagnostics about the failure.
    	/// Otherwise returns null.
    	public IReadOnlyList<string> GetCommitFailureDiagnostics()
    	    => throw new NotImplementedException();
    
    	~HotReloadTransaction() => Dispose(false);
    
    	/// If the transaction hasn't been committed, it is canceled.
    	public void Dispose()
    	{
    	    Dispose(true);
    	    GC.SuppressFinalize(this);
    	}
    
    	// true if transaction is not committed, canceled, or disposed
    	private bool Active {get; set;}
    
    	private void Dispose(bool disposing)
    	{
    	    if (Active)
    		    CancelTransaction();
    	}
        }
    }
    
  • Instead of a managed API, we could tightly couple the API to an existing side-channel such as the diagnostic server, or the debugger. That would mean that workloads and frameworks wishing to provide hot reload capabilities would need to use a communication channel provided by those services to inject changes.

  • Instead of a managed API, we could expose a native hosting API function to inject changes. That would mean that workloads and frameworks wishing to provide hot reload capabilities would need to implement at least part of their functionality using native code.

Related API changes, not part of this proposal

There will be separate API proposals for additional aspects of hot reload.

  • IL Linker System.Runtime.CompilerServices.RuntimeFeature properties: Give framework authors a way to include hot reload support, but be able to link it out if the framework is running on a runtime without the capability. Potentially also a property to determine if on-stack replacement is available, since it changes the semantics of delta application.
  • Managed events for delta application. Give framework authors a way to be notified about the changes in a delta (for example a list of stateless change-aware types).
  • A hosting API addition to notify the runtime that a hot reload session will be started and that certain assemblies may receive deltas.

Risks

Unknown at this time.

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.RuntimeenhancementProduct code improvement that does NOT require public API changes/additions

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions