Parse method call chain model file and resolve methods (#47302)
authorSimon Nattress <nattress@gmail.com>
Fri, 22 Jan 2021 04:57:23 +0000 (20:57 -0800)
committerGitHub <noreply@github.com>
Fri, 22 Jan 2021 04:57:23 +0000 (20:57 -0800)
* Parse method call chain model file and resolve methods
* Resolve methods from Azure profile trace building a list of caller->callees with counts to use in compilation heuristics
* Resolves the two main forms of methods we see in the traces:

```
System.Core.ni.dll!System.Linq.Enumerable+WhereSelectEnumerableIterator`2[System.__Canon,System.__Canon].MoveNext
Microsoft.Azure.Monitoring.WarmPath.FrontEnd.Middleware.SecurityMiddlewareBase`1+<Invoke>d__6[System.__Canon]!MoveNext
```

src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CallChainProfile.cs [new file with mode: 0644]
src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj
src/coreclr/tools/aot/crossgen2/CommandLineOptions.cs
src/coreclr/tools/aot/crossgen2/Program.cs
src/coreclr/tools/aot/crossgen2/Properties/Resources.resx

index fea5310..1b901f5 100644 (file)
@@ -69,7 +69,9 @@ namespace Internal.TypeSystem
             AssemblyName homeAssembly = FindAssemblyIfNamePresent(name);
             if (homeAssembly != null)
             {
-                homeModule = module.Context.ResolveAssembly(homeAssembly);
+                homeModule = module.Context.ResolveAssembly(homeAssembly, throwIfNotFound);
+                if (homeModule == null)
+                    return null;
             }
             MetadataType typeDef = resolver != null ? resolver(genericTypeDefName.ToString(), homeModule, throwIfNotFound) :
                 ResolveCustomAttributeTypeDefinitionName(genericTypeDefName.ToString(), homeModule, throwIfNotFound);
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CallChainProfile.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CallChainProfile.cs
new file mode 100644 (file)
index 0000000..eaf66f9
--- /dev/null
@@ -0,0 +1,329 @@
+// 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.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+
+using ILCompiler.IBC;
+
+using Internal.TypeSystem;
+using Internal.TypeSystem.Ecma;
+
+namespace ILCompiler
+{
+    // This is a sample of the Json format the call chain data is stored in:
+    //
+    //{
+    //    "Microsoft.CodeAnalysis.CSharp.BinderFactory!GetBinder": [
+    //        [
+    //            "Microsoft.CodeAnalysis.CSharp.BinderFactory!GetBinder",
+    //            "Microsoft.CodeAnalysis.CSharp.BinderFactory+BinderFactoryVisitor!VisitCompilationUnit"
+    //        ],
+    //        [
+    //            1,
+    //            2
+    //        ]
+    //    ],
+    //    "Microsoft.CodeAnalysis.CSharp.BinderFactory+BinderFactoryVisitor!VisitCompilationUnit": [
+    //        [
+    //            "System.Lazy`1[System.__Canon]!CreateValue"
+    //        ],
+    //        [
+    //            1
+    //        ]
+    //    ],
+    //}
+    public class CallChainProfile
+    {
+        private readonly IEnumerable<ModuleDesc> _referenceableModules;
+        private readonly Dictionary<MethodDesc, Dictionary<MethodDesc, int>> _resolvedProfileData;
+
+        // Diagnostics
+#if DEBUG
+        private int _methodResolvesAttempted = 0;
+        private int _methodsSuccessfullyResolved = 0;
+        private Dictionary<string, int> _resolveFails = new Dictionary<string, int>();
+#endif
+
+        public CallChainProfile(string callChainProfileFile,
+                                CompilerTypeSystemContext context,
+                                IEnumerable<ModuleDesc> referenceableModules)
+        {
+            _referenceableModules = referenceableModules;
+            var analysisData = ReadCallChainAnalysisData(callChainProfileFile);
+            _resolvedProfileData = ResolveMethods(analysisData, context);
+        }
+
+        /// <summary>
+        /// Try to resolve each name from the profile data to a MethodDesc
+        /// </summary>
+        private Dictionary<MethodDesc, Dictionary<MethodDesc, int>> ResolveMethods(Dictionary<string, Dictionary<string, int>> profileData, CompilerTypeSystemContext context)
+        {
+            var resolvedProfileData = new Dictionary<MethodDesc, Dictionary<MethodDesc, int>>();
+            Dictionary<string, MethodDesc> nameToMethodDescMap = new Dictionary<string, MethodDesc>();
+
+            foreach (var keyAndMethods in profileData)
+            {
+                // Resolve the calling method
+                var resolvedKeyMethod = CachedResolveMethodName(nameToMethodDescMap, keyAndMethods.Key, context);
+
+                if (resolvedKeyMethod == null)
+                    continue;
+
+                // Resolve each callee and counts
+                foreach (var methodAndHitCount in keyAndMethods.Value)
+                {
+                    var resolvedCalledMethod = CachedResolveMethodName(nameToMethodDescMap ,methodAndHitCount.Key, context);
+                    if (resolvedCalledMethod == null)
+                        continue;
+
+                    if (!resolvedProfileData.ContainsKey(resolvedKeyMethod))
+                    {
+                        resolvedProfileData.Add(resolvedKeyMethod, new Dictionary<MethodDesc, int>());
+                    }
+
+                    if (!resolvedProfileData[resolvedKeyMethod].ContainsKey(resolvedCalledMethod))
+                    {
+                        resolvedProfileData[resolvedKeyMethod].Add(resolvedCalledMethod, 0);
+                    }
+                    resolvedProfileData[resolvedKeyMethod][resolvedCalledMethod] += methodAndHitCount.Value;
+                }
+            }
+
+            return resolvedProfileData;
+        }
+
+        private MethodDesc CachedResolveMethodName(Dictionary<string, MethodDesc> nameToMethodDescMap, string methodName, CompilerTypeSystemContext context)
+        {
+            MethodDesc resolvedMethod = null;
+            if (nameToMethodDescMap.ContainsKey(methodName))
+            {
+                resolvedMethod = nameToMethodDescMap[methodName];
+            }
+            else
+            {
+                resolvedMethod = ResolveMethodName(context, methodName);
+                nameToMethodDescMap.Add(methodName, resolvedMethod);
+            }
+
+#if DEBUG
+            if (resolvedMethod == null)
+            {
+                if (!_resolveFails.ContainsKey(methodName))
+                {
+                    _resolveFails.Add(methodName, 0);
+                }
+                _resolveFails[methodName]++;
+            }
+#endif
+            return resolvedMethod;
+        }
+
+        private MethodDesc ResolveMethodName(CompilerTypeSystemContext context, string methodName)
+        {
+            // Example method name entries. Can we parse them as custom attribute formatted names?
+            // mscorlib.ni.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo..ctor
+            // System.Core.ni.dll!System.Linq.Enumerable+WhereSelectEnumerableIterator`2[System.__Canon,System.__Canon].MoveNext
+            // Microsoft.Azure.Monitoring.WarmPath.FrontEnd.Middleware.SecurityMiddlewareBase`1+<Invoke>d__6[System.__Canon]!MoveNext
+            // System.Runtime.CompilerServices.AsyncTaskMethodBuilder!Start
+#if DEBUG
+            _methodResolvesAttempted++;
+#endif
+
+            string[] splitMethodName = methodName.Split("!");
+            if (splitMethodName.Length != 2)
+            {
+                return null;
+            }
+
+            if (splitMethodName[0].EndsWith(".dll") ||
+                splitMethodName[0].EndsWith(".ni.dll") ||
+                splitMethodName[0].EndsWith(".exe") ||
+                splitMethodName[0].EndsWith(".ni.exe"))
+            {
+                // Native stack frame for the method name. This happens for managed methods in native images
+                // (Remember, this is .NET Framework data we're starting with)
+                string moduleSimpleName = Path.ChangeExtension(splitMethodName[0], null);
+                // Desktop has native images with ni.dll or ni.exe extensions very frequently
+                if (moduleSimpleName.EndsWith(".ni"))
+                    moduleSimpleName = moduleSimpleName.Substring(0, moduleSimpleName.Length - 3);
+                string unresolvedNamespaceTypeAndMethodName = splitMethodName[1];
+
+                // Try to resolve the module from the list of loaded assemblies
+                EcmaModule resolvedModule = context.GetModuleForSimpleName(moduleSimpleName, false);
+                if (resolvedModule == null)
+                    return null;
+
+                // Resolve a name like System.Linq.Enumerable+WhereSelectEnumerableIterator`2[System.__Canon,System.__Canon].MoveNext
+                // Take the string after the last period as the method name (special case for .ctor and .cctor)
+                string namespaceAndTypeName = null;
+                string methodNameWithoutType = null;
+
+                if (unresolvedNamespaceTypeAndMethodName.EndsWith("..ctor"))
+                {
+                    namespaceAndTypeName = unresolvedNamespaceTypeAndMethodName.Substring(0, unresolvedNamespaceTypeAndMethodName.Length - "..ctor".Length);
+                    methodNameWithoutType = ".ctor";
+                }
+                else if (unresolvedNamespaceTypeAndMethodName.EndsWith("..cctor"))
+                {
+                    namespaceAndTypeName = unresolvedNamespaceTypeAndMethodName.Substring(0, unresolvedNamespaceTypeAndMethodName.Length - "..cctor".Length);
+                    methodNameWithoutType = ".cctor";
+                }
+                else
+                {
+                    int lastDotIndex = unresolvedNamespaceTypeAndMethodName.LastIndexOf(".");
+                    if (lastDotIndex < 0)
+                        return null;
+
+                    namespaceAndTypeName = unresolvedNamespaceTypeAndMethodName.Substring(0, lastDotIndex);
+                    methodNameWithoutType = unresolvedNamespaceTypeAndMethodName.Length > lastDotIndex ? unresolvedNamespaceTypeAndMethodName.Substring(lastDotIndex + 1) : "";
+                }
+
+                var resolvedMethod = ResolveMethodName(context, resolvedModule, namespaceAndTypeName, methodNameWithoutType);
+                if (resolvedMethod != null)
+                {
+#if DEBUG
+                    _methodsSuccessfullyResolved++;
+#endif
+                    return resolvedMethod;
+                }
+                    
+            }
+            else
+            {
+                // We have Namespace.Type!Method format with no method signature information. Check all loaded modules for a matching
+                // type name, and the first method on that type with matching name.
+                // Microsoft.Azure.Monitoring.WarmPath.FrontEnd.Middleware.SecurityMiddlewareBase`1+<Invoke>d__6[System.__Canon]!MoveNext
+                // System.Runtime.CompilerServices.AsyncTaskMethodBuilder!Start
+                string namespaceAndTypeName = splitMethodName[0];
+                string methodNameWithoutType = splitMethodName[1];
+                
+                foreach (var module in _referenceableModules)
+                {
+                    var resolvedMethod = ResolveMethodName(context, module, namespaceAndTypeName, methodNameWithoutType);
+                    if (resolvedMethod != null)
+                    {
+#if DEBUG
+                        _methodsSuccessfullyResolved++;
+#endif
+                        return resolvedMethod;
+                    }
+                        
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Given a parsed out module, namespace + type, and method name, try to find a matching MethodDesc
+        /// TODO: We have no signature information for the method - what policy should we apply where multiple methods exist with the same name
+        /// but different signatures? For now we'll take the first matching and ignore others. Ideally we'll improve the profile data to include this.
+        /// </summary>
+        /// <returns>MethodDesc if found, null otherwise</returns>
+        private MethodDesc ResolveMethodName(CompilerTypeSystemContext context, ModuleDesc module, string namespaceAndTypeName, string methodName)
+        {
+            TypeDesc resolvedType = module.GetTypeByCustomAttributeTypeName(namespaceAndTypeName, false, (typeDefName, module, throwIfNotFound) =>
+            {
+                return (MetadataType)context.GetCanonType(typeDefName)
+                    ?? CustomAttributeTypeNameParser.ResolveCustomAttributeTypeDefinitionName(typeDefName, module, throwIfNotFound);
+            });
+
+            if (resolvedType != null)
+            {
+                var resolvedMethod = resolvedType.GetMethod(methodName, null);
+                if (resolvedMethod != null)
+                {
+                    return resolvedMethod;
+                }
+            }
+
+            return null;
+        }
+
+        private Dictionary<string, Dictionary<string, int>> ReadCallChainAnalysisData(string jsonProfileFile)
+        {
+            Dictionary<string, Dictionary<string, int>> profileData = new Dictionary<string, Dictionary<string, int>>();
+
+            using (StreamReader stream = File.OpenText(jsonProfileFile))
+            using (JsonDocument document = JsonDocument.Parse(stream.BaseStream))
+            {
+                JsonElement root = document.RootElement;
+                
+                foreach (JsonProperty methodAndCallees in root.EnumerateObject())
+                {
+                    string keyParts = methodAndCallees.Name;
+                    bool readingMethodNames = true;
+                    List<string> followingMethodList = new List<string>();
+                    foreach (JsonElement methodListArray in methodAndCallees.Value.EnumerateArray())
+                    {
+                        // This loop iterates twice: once for the callee method names, and again for a parallel list of call counts
+                        if (readingMethodNames)
+                        {
+                            foreach (JsonElement followingMethods in methodListArray.EnumerateArray())
+                            {
+                                followingMethodList.Add(followingMethods.GetString());
+                            }
+
+                            readingMethodNames = false;
+                        }
+                        else
+                        {
+                            // Read the array of call counts
+                            int index = 0;
+                            foreach (JsonElement methodCount in methodListArray.EnumerateArray())
+                            {
+                                if (string.IsNullOrEmpty(keyParts))
+                                    break;
+
+                                if (!profileData.ContainsKey(keyParts))
+                                {
+                                    profileData.Add(keyParts, new Dictionary<string, int>());
+                                }
+                                if (!profileData[keyParts].ContainsKey(followingMethodList[index]))
+                                {
+                                    profileData[keyParts].Add(followingMethodList[index], methodCount.GetInt32());
+                                }
+                                else
+                                {
+                                    profileData[keyParts][followingMethodList[index]] += methodCount.GetInt32();
+                                }
+                                index++;
+                            }
+                        }
+                    }
+                }
+            }
+            return profileData;
+        }
+
+#if DEBUG
+        /// <summary>
+        /// Dump diagnostic information to the console
+        /// </summary>
+        private void WriteProfileParseStats()
+        {
+            Console.WriteLine("Callchain profile entries:");
+
+            // Display all resolved methods in key -> { method -> count, method2 -> count} map
+            foreach (var key in _resolvedProfileData)
+            {
+                Console.WriteLine($"{key.Key.ToString()}");
+
+                foreach (var calledMethodAndCount in key.Value)
+                {
+                    Console.WriteLine($"\t{calledMethodAndCount.Key.ToString()} -> {calledMethodAndCount.Value} calls");
+                }
+            }
+
+            Console.WriteLine($"Method resolves attempted: {_methodResolvesAttempted}");
+            Console.WriteLine($"Successfully resolved {_methodsSuccessfullyResolved} methods ({(double)_methodsSuccessfullyResolved / (double)_methodResolvesAttempted:P})");
+        }
+#endif
+    }
+
+}
+
index e6f136e..fd51184 100644 (file)
@@ -97,6 +97,7 @@
     <Compile Include="..\..\Common\TypeSystem\Interop\InteropTypes.cs" Link="Interop\InteropTypes.cs" />
     <Compile Include="Compiler\AssemblyExtensions.cs" />
     <Compile Include="Compiler\DependencyAnalysis\ReadyToRun\DelayLoadMethodCallThunkNodeRange.cs" />
+    <Compile Include="Compiler\CallChainProfile.cs" />
     <Compile Include="ObjectWriter\MapFileBuilder.cs" />
     <Compile Include="CodeGen\ReadyToRunObjectWriter.cs" />
     <Compile Include="Compiler\CompilationModuleGroup.ReadyToRun.cs" />
index 910743f..1c15eaa 100644 (file)
@@ -55,6 +55,7 @@ namespace ILCompiler
         public string MethodLayout;
         public string FileLayout;
         public bool VerifyTypeAndFieldLayout;
+        public string CallChainProfileFile;
 
         public string SingleMethodTypeName;
         public string SingleMethodName;
@@ -129,6 +130,7 @@ namespace ILCompiler
                 syntax.DefineOption("method-layout", ref MethodLayout, SR.MethodLayoutOption);
                 syntax.DefineOption("file-layout", ref FileLayout, SR.FileLayoutOption);
                 syntax.DefineOption("verify-type-and-field-layout", ref VerifyTypeAndFieldLayout, SR.VerifyTypeAndFieldLayoutOption);
+                syntax.DefineOption("callchain-profile", ref CallChainProfileFile, SR.CallChainProfileFile);
 
                 syntax.DefineOption("h|help", ref Help, SR.HelpOption);
 
index 35dbcec..d6c7dbf 100644 (file)
@@ -523,6 +523,14 @@ namespace ILCompiler
                             _commandLineOptions.CompileBubbleGenerics);
                     }
 
+                    // Load any profiles generated by method call chain analyis
+                    CallChainProfile jsonProfile = null;
+
+                    if (!string.IsNullOrEmpty(_commandLineOptions.CallChainProfileFile))
+                    {
+                        jsonProfile = new CallChainProfile(_commandLineOptions.CallChainProfileFile, _typeSystemContext, referenceableModules);
+                    }
+
                     // Examine profile guided information as appropriate
                     ProfileDataManager profileDataManager =
                         new ProfileDataManager(logger,
index 6b3e6be..a0b7c2e 100644 (file)
   <data name="WarningOverridingOptimize" xml:space="preserve">
     <value>Warning: -Od overrides other optimization options</value>
   </data>
+  <data name="CallChainProfileFile" xml:space="preserve">
+    <value>Json file(s) for predictive profile guided optimization</value>
+  </data>
   <data name="PdbFileOption" xml:space="preserve">
     <value>Generate PDB symbol information file (supported on Windows only)</value>
   </data>