File: Compiler\DeferredWasmCallProcessor.cs
Web Access
Project: ILCompiler.ReadyToRun.csproj (ILCompiler.ReadyToRun)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;

using ILCompiler.DependencyAnalysis;
using ILCompiler.DependencyAnalysis.ReadyToRun;
using ILCompiler.DependencyAnalysisFramework;

using Internal.IL;
using Internal.JitInterface;
using Internal.TypeSystem;

namespace ILCompiler;

internal readonly struct DeferredWasmMethodFinalizationInfo(int level, bool isEager = false)
{
    public int Level { get; } = level;
    public bool IsEager { get; } = isEager;
}

// #DeferredWasmCallFinalization
//
// Own post-codegen processing of deferred calls in Wasm compilations containing
// CoreLib. The JIT generates each method before dependency analysis quiesces;
// the finalization plan orders rewriting and activation-fixup minimization,
// not another compilation pass.
//
// Finalize the closure of native-facing startup entrypoints and already-rooted
// managed JIT helpers first, then relevel and finalize the remaining methods.
// Publish direct-call eligibility only after an entire level is finalized:
// callees at lower levels have complete activation guarantees, while calls
// within a recursive component remain indirect.
//
// Eager preparation follows both direct calls and MethodEntry_ReadyToRun
// fixups. Promote its actual import cells to eager sections, preserving string
// classification, and remove those same cells from every method's precode.
// Merely compiling a method does not make it eager. Validation and data fixups
// must run before eager method activation can permanently publish R2R code.
//
// _pendingMethods retains raw debug data and import-time activation tokens
// until rewriting. _finalizedMethods records the level and eager status used
// by call selection. _targets interns deferred call nodes across JIT workers,
// which also populate distinct MethodData entries. Registration and
// finalization are serialized around those workers. After calls and eager
// promotion are finalized, commit deferred import placement and
// revisit method dependencies to publish the chosen calls and imports.
// See #DeferredWasmCallRewriting and #PrecodeFixupMinimization.
internal sealed class DeferredWasmCallProcessor
{
    private const int DefaultMaxDirectCallActivationDepth = 4;

    private readonly ReadyToRunCodegenCompilation _compilation;
    private readonly DependencyAnalyzerBase<NodeFactory> _dependencyGraph;
    private readonly DeferredWasmCallRewriter _rewriter;
    private readonly Dictionary<MethodDesc, DeferredWasmMethodFinalizationInfo> _finalizedMethods = new();
    private readonly HashSet<MethodDesc> _startupEagerMethods;
    private readonly ConcurrentDictionary<(MethodWithToken Method, WasmTypeNode Signature), DeferredWasmCallNode> _targets = new();
    private readonly Dictionary<MethodWithGCInfo, MethodData> _pendingMethods = new();
    private readonly HashSet<Import> _fixupScratch = new();
    private bool _finalizingEagerMethods;
    private bool _policyFrozen;

    private sealed class MethodData
    {
        internal OffsetMapping[] Locations;
        internal NativeVarInfo[] Variables;
        internal Dictionary<MethodWithGCInfo, DelayLoadMethodImport> ActivationImports;
    }

    private NodeFactory NodeFactory => _compilation.NodeFactory;
    private Logger Logger => _compilation.Logger;

    internal DeferredWasmCallProcessor(
        ReadyToRunCodegenCompilation compilation, DependencyAnalyzerBase<NodeFactory> dependencyGraph)
    {
        _compilation = compilation;
        _dependencyGraph = dependencyGraph;
        _rewriter = new DeferredWasmCallRewriter(compilation);
        _startupEagerMethods = GetStartupEagerMethods();
        _dependencyGraph.DependencyAnalysisQuiesced += FinalizeCalls;
        foreach (MethodDesc method in _startupEagerMethods)
        {
            _dependencyGraph.AddRoot(NodeFactory.CompiledMethodNode(method), "Native-facing startup method");
        }
    }

    // Register methods before their JIT workers publish the data needed for
    // deferred finalization. New methods after policy selection indicate a
    // missed dependency, but can still be rewritten using the frozen policy.
    internal void QueueMethods(List<DependencyNodeCore<NodeFactory>> dependencies)
    {
        foreach (DependencyNodeCore<NodeFactory> dependency in dependencies)
        {
            if (dependency is MethodWithGCInfo method)
            {
                Debug.Assert(!_policyFrozen,
                    $"Method {method.Method} requires compilation after the deferred Wasm call policy was frozen.");
                _pendingMethods.TryAdd(method, new MethodData());
            }
        }
    }

    // Retain unencoded debug offsets and the original activation tokens until
    // call rewriting has established the final layout and activation paths.
    internal void InitializeMethodData(
        MethodWithGCInfo method, OffsetMapping[] locations, NativeVarInfo[] variables,
        Dictionary<MethodWithGCInfo, DelayLoadMethodImport> activationImports)
    {
        MethodData data = _pendingMethods[method];
        data.Locations = locations;
        data.Variables = variables;
        data.ActivationImports = activationImports;
    }

    // Preserve the ordinary import when it cannot identify the same compiled
    // target; otherwise share a deferred node for this token and call signature.
    internal ISymbolNode GetCall(MethodWithToken method, ISymbolNode fallback, WasmTypeNode signature = null)
    {
        if (fallback is not DelayLoadMethodImport { MethodCodeNode: not null } import ||
            import.MethodCodeNode.Method != method.Method.GetCanonMethodTarget(CanonicalFormKind.Specific))
        {
            return fallback;
        }

        // Optional native libraries must remain lazily bound, so their stubs
        // cannot enter the direct-call graph or its eager closure.
        if (method.Method.IsPInvoke && !_compilation.CompilationModuleGroup.IsDirectPInvoke(method.Method))
        {
            return fallback;
        }

        return _targets.GetOrAdd(
            (method, signature), static (key, fallback) => new DeferredWasmCallNode(fallback, key.Signature), import);
    }

    // Require finalized activation guarantees and bound non-eager activation
    // depth. Eager methods are prepared at module startup instead.
    internal bool CanUseDirectCall(MethodDesc method, out DeferredWasmMethodFinalizationInfo finalizationInfo)
    {
        finalizationInfo = default;
        return NodeFactory.Target.Architecture == TargetArchitecture.Wasm32 &&
            _finalizedMethods.TryGetValue(method, out finalizationInfo) &&
            (finalizationInfo.IsEager || _finalizingEagerMethods ||
                (!method.IsNoInlining && !method.IsNoOptimization &&
                    // Include the caller and the level-zero leaf in the activation depth.
                    (finalizationInfo.Level < DefaultMaxDirectCallActivationDepth - 1 ||
                        IsDirectCallDepthExempt(method.OwningType))));
    }

    // Build the post-codegen schedule once. Later quiescence notifications only
    // finalize newly discovered methods and publish their chosen dependencies.
    private void FinalizeCalls()
    {
        if (!_policyFrozen)
        {
            var methods = new List<MethodWithGCInfo>();
            foreach (MethodWithGCInfo method in _pendingMethods.Keys)
            {
                if (!method.IsEmpty)
                {
                    methods.Add(method);
                }
            }

            DeferredWasmCallFinalizationPlan plan = DeferredWasmCallFinalizationPlan.Create(methods, NodeFactory);
            FinalizePlan(plan);
            _policyFrozen = true;
            NodeFactory.CompleteDeferredImportPlacement();
        }
        foreach (MethodWithGCInfo method in _pendingMethods.Keys)
        {
            if (_pendingMethods[method] is not null)
            {
                FinalizeMethod(method);
            }
            _dependencyGraph.RevisitStaticDependencies(method);
        }
        _pendingMethods.Clear();
    }

    // Establish eager activation first, then finalize the remaining methods
    // relative to that available frontier. Neither stage regenerates JIT code.
    private void FinalizePlan(DeferredWasmCallFinalizationPlan plan)
    {
        var methodNodes = new List<MethodWithGCInfo>();
        var managedHelpers = new List<MethodWithGCInfo>();
        var finalizedMethods = new HashSet<MethodDesc>();
        var eagerRoots = new HashSet<MethodDesc>(_startupEagerMethods);

        for (int level = 0; level < plan.LevelCount; level++)
        {
            IReadOnlyList<MethodDesc> methodsAtLevel = plan.GetMethodsAtLevel(level);
            foreach (MethodDesc method in methodsAtLevel)
            {
                MethodWithGCInfo methodNode = NodeFactory.CompiledMethodNode(method);
                methodNodes.Add(methodNode);
                if (methodNode.IsJitHelper)
                {
                    managedHelpers.Add(methodNode);
                    eagerRoots.Add(method);
                }
            }
        }

        _compilation.EnsureDirectManagedHelpersInitialized(managedHelpers);

        DeferredWasmCallFinalizationPlan eagerPlan = plan.CreateEagerPlan(eagerRoots);
        _finalizingEagerMethods = true;
        FinalizeMethods(eagerPlan, isEager: true);
        _finalizingEagerMethods = false;

        // Only successfully finalized eager methods precede non-eager level zero.
        var finalizedEagerMethods = new HashSet<MethodDesc>(finalizedMethods);
        DeferredWasmCallFinalizationPlan nonEagerPlan = plan.CreateNonEagerPlan(finalizedEagerMethods);
        FinalizeMethods(nonEagerPlan, isEager: false);

        _compilation.ValidateManagedHelpers(managedHelpers);
        PromoteModuleEagerFixups(methodNodes, finalizedMethods, finalizedEagerMethods);

        // Publish a level only after every member has complete code and fixups,
        // preventing direct activation edges within a recursive component.
        void FinalizeMethods(DeferredWasmCallFinalizationPlan stagePlan, bool isEager)
        {
            for (int level = 0; level < stagePlan.LevelCount; level++)
            {
                IReadOnlyList<MethodDesc> methodsAtLevel = stagePlan.GetMethodsAtLevel(level);
                if (methodsAtLevel.Count == 0)
                {
                    continue;
                }

                var methodsToFinalize = new List<MethodWithGCInfo>(methodsAtLevel.Count);
                foreach (MethodDesc method in methodsAtLevel)
                {
                    MethodWithGCInfo methodNode = NodeFactory.CompiledMethodNode(method);
                    if (_pendingMethods[methodNode] is not null)
                    {
                        methodsToFinalize.Add(methodNode);
                    }
                }

                string stage = isEager ? "eager" : "non-eager";
                if (Logger.IsVerbose)
                {
                    Logger.Writer.WriteLine($"Finalizing deferred Wasm calls {stage} level {level} with {methodsAtLevel.Count} methods");
                }

                foreach (MethodWithGCInfo method in methodsToFinalize)
                {
                    FinalizeMethod(method);
                }

                int finalizedCount = 0;
                foreach (MethodWithGCInfo method in methodsToFinalize)
                {
                    if (!method.IsEmpty)
                    {
                        finalizedMethods.Add(method.Method);
                        _finalizedMethods.Add(method.Method, new DeferredWasmMethodFinalizationInfo(level, isEager));
                        finalizedCount++;
                    }
                }

                if (Logger.IsVerbose)
                {
                    Logger.Writer.WriteLine($"Finalized deferred Wasm calls {stage} level {level}: {finalizedCount} finalized methods");
                }
            }
        }
    }

    // Rewrite before minimizing fixups, then release raw per-method data while
    // retaining the pending key for dependency publication.
    private void FinalizeMethod(MethodWithGCInfo method)
    {
        if (!method.IsEmpty)
        {
            MethodData data = _pendingMethods[method];
            _rewriter.Rewrite(method, data.Locations, data.Variables, data.ActivationImports);
            method.FinalizePrecodeFixups(scratch: _fixupScratch);
        }
        _pendingMethods[method] = null;
    }

    // These native-facing CoreLib entrypoints must be ready before ordinary
    // managed calls can establish activation through their precodes.
    private HashSet<MethodDesc> GetStartupEagerMethods()
    {
        var methods = new HashSet<MethodDesc>();
        AddMethod("System"u8, "AppContext"u8, "Setup");
        AddMethod("System.Diagnostics.Tracing"u8, "EventSource"u8, "InitializeDefaultEventSources");
        AddMethod("System"u8, "Environment"u8, "InitializeCommandLineArgs");
        AddMethod("System"u8, "Environment"u8, "CallEntryPoint");
        AddMethod("System"u8, "StartupHookProvider"u8, "ManagedStartup");
        return methods;

        void AddMethod(ReadOnlySpan<byte> namespaceName, ReadOnlySpan<byte> typeName, string methodName)
        {
            MetadataType type = _compilation.TypeSystemContext.SystemModule.GetKnownType(namespaceName, typeName);
            MethodDesc foundMethod = null;
            foreach (MethodDesc method in type.GetMethods())
            {
                if (method.GetName() == methodName && method.IsUnmanagedCallersOnly)
                {
                    Debug.Assert(foundMethod is null);
                    foundMethod = method;
                }
            }

            Debug.Assert(foundMethod is not null);
            if (_compilation.CompilationModuleGroup.ContainsMethodBody(foundMethod, unboxingStub: false))
            {
                methods.Add(foundMethod);
            }
        }
    }

    // Move the eager closure's activation requirements to module startup, then
    // remove those cells from every method's precode. See #PrecodeFixupMinimization.
    private void PromoteModuleEagerFixups(
        List<MethodWithGCInfo> methods,
        HashSet<MethodDesc> finalizedMethods,
        HashSet<MethodDesc> finalizedEagerMethods)
    {
        var eagerMethodClosure = new HashSet<MethodWithGCInfo>();
        var methodsToVisit = new Stack<MethodWithGCInfo>();
        var eagerFixups = new HashSet<Import>();

        foreach (MethodDesc method in finalizedEagerMethods)
        {
            MethodWithGCInfo methodNode = NodeFactory.CompiledMethodNode(method);
            Debug.Assert(!methodNode.IsEmpty);

            MethodWithToken methodWithToken = new MethodWithToken(
                method,
                NodeFactory.Resolver.GetModuleTokenForMethod(method, allowDynamicallyCreatedReference: true, throwIfNotFound: true),
                constrainedType: null,
                unboxing: false,
                genericContextObject: null);
            _dependencyGraph.AddRoot(
                _compilation.SymbolNodeFactory.EagerReadyToRunMethodEntry(methodWithToken),
                "Eager-stage method is already activated");

            methodsToVisit.Push(methodNode);
        }

        while (methodsToVisit.TryPop(out MethodWithGCInfo method))
        {
            if (!eagerMethodClosure.Add(method))
            {
                continue;
            }

            foreach (Import fixup in method.GetGuaranteedPrecodeFixups(_fixupScratch))
            {
                eagerFixups.Add(fixup);
                // Eager helper entries also prepare their bodies, even when the
                // generated call does not contribute a direct-call edge.
                if (fixup.Signature is MethodFixupSignature { IsReadyToRunEntry: true } signature)
                {
                    MethodDesc target = signature.Method.GetCanonMethodTarget(CanonicalFormKind.Specific);
                    if (finalizedMethods.Contains(target))
                    {
                        methodsToVisit.Push(NodeFactory.CompiledMethodNode(target));
                    }
                }
            }

            for (int i = 0; i < method.CodeRelocations.Length; i++)
            {
                if (method.GetDirectCallTarget(i) is MethodWithGCInfo target)
                {
                    methodsToVisit.Push(target);
                }
            }
        }

        foreach (Import fixup in eagerFixups)
        {
            if (!fixup.Table.IsEager)
            {
                // Eagerness changes initialization timing, not the cell's value
                // kind: keep strings classified as StringHandle rather than
                // merging them into the untyped eager data section.
                ImportSectionNode table = fixup.Signature switch
                {
                    MethodFixupSignature { RequiresEagerActivation: true } or
                        ResumptionStubEntryPointSignature or StoreMultiCallableAddrOfCodeSignature => NodeFactory.EagerMethodImports,
                    _ when fixup.Table == NodeFactory.StringImports => NodeFactory.EagerStringImports,
                    _ => NodeFactory.EagerImports,
                };
                fixup.PromoteToEager(table);
            }

            _dependencyGraph.AddRoot(fixup, "Eager method precode fixup is module-eager");
        }

        foreach (MethodWithGCInfo method in methods)
        {
            if (!method.IsEmpty)
            {
                method.ApplyModuleEagerFixups(eagerFixups);
                Debug.Assert(!eagerMethodClosure.Contains(method) || method.Fixups.Count == 0);
            }
        }
    }

    // Keep common primitive and collection operations eligible even when their
    // position in the call graph exceeds the general activation-depth bound.
    internal static bool IsDirectCallDepthExempt(TypeDesc type)
    {
        if (type.IsPrimitive || type.IsObject || type.IsString)
        {
            return true;
        }

        // Generic collection and enumeration interfaces inherit these non-generic interfaces.
        foreach (DefType interfaceType in type.GetTypeDefinition().RuntimeInterfaces)
        {
            if (interfaceType is MetadataType metadataType &&
                metadataType.Module == type.Context.SystemModule &&
                metadataType.Namespace == "System.Collections"u8 &&
                (metadataType.Name == "IEnumerable"u8 || metadataType.Name == "IEnumerator"u8))
            {
                return true;
            }
        }

        return false;
    }
}