From: Simon Nattress Date: Fri, 22 Jan 2021 04:57:23 +0000 (-0800) Subject: Parse method call chain model file and resolve methods (#47302) X-Git-Tag: submit/tizen/20210909.063632~3667 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=311de302de8ac21837e98eb359595e733af38c83;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Parse method call chain model file and resolve methods (#47302) * 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+d__6[System.__Canon]!MoveNext ``` --- diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs index fea5310..1b901f5 100644 --- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs +++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs @@ -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 index 0000000..eaf66f9 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/CallChainProfile.cs @@ -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 _referenceableModules; + private readonly Dictionary> _resolvedProfileData; + + // Diagnostics +#if DEBUG + private int _methodResolvesAttempted = 0; + private int _methodsSuccessfullyResolved = 0; + private Dictionary _resolveFails = new Dictionary(); +#endif + + public CallChainProfile(string callChainProfileFile, + CompilerTypeSystemContext context, + IEnumerable referenceableModules) + { + _referenceableModules = referenceableModules; + var analysisData = ReadCallChainAnalysisData(callChainProfileFile); + _resolvedProfileData = ResolveMethods(analysisData, context); + } + + /// + /// Try to resolve each name from the profile data to a MethodDesc + /// + private Dictionary> ResolveMethods(Dictionary> profileData, CompilerTypeSystemContext context) + { + var resolvedProfileData = new Dictionary>(); + Dictionary nameToMethodDescMap = new Dictionary(); + + 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()); + } + + if (!resolvedProfileData[resolvedKeyMethod].ContainsKey(resolvedCalledMethod)) + { + resolvedProfileData[resolvedKeyMethod].Add(resolvedCalledMethod, 0); + } + resolvedProfileData[resolvedKeyMethod][resolvedCalledMethod] += methodAndHitCount.Value; + } + } + + return resolvedProfileData; + } + + private MethodDesc CachedResolveMethodName(Dictionary 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+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+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; + } + + /// + /// 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. + /// + /// MethodDesc if found, null otherwise + 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> ReadCallChainAnalysisData(string jsonProfileFile) + { + Dictionary> profileData = new Dictionary>(); + + 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 followingMethodList = new List(); + 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()); + } + 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 + /// + /// Dump diagnostic information to the console + /// + 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 + } + +} + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index e6f136e..fd51184 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -97,6 +97,7 @@ + diff --git a/src/coreclr/tools/aot/crossgen2/CommandLineOptions.cs b/src/coreclr/tools/aot/crossgen2/CommandLineOptions.cs index 910743f..1c15eaa 100644 --- a/src/coreclr/tools/aot/crossgen2/CommandLineOptions.cs +++ b/src/coreclr/tools/aot/crossgen2/CommandLineOptions.cs @@ -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); diff --git a/src/coreclr/tools/aot/crossgen2/Program.cs b/src/coreclr/tools/aot/crossgen2/Program.cs index 35dbcec..d6c7dbf 100644 --- a/src/coreclr/tools/aot/crossgen2/Program.cs +++ b/src/coreclr/tools/aot/crossgen2/Program.cs @@ -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, diff --git a/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx b/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx index 6b3e6be..a0b7c2e 100644 --- a/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx +++ b/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx @@ -309,6 +309,9 @@ Warning: -Od overrides other optimization options + + Json file(s) for predictive profile guided optimization + Generate PDB symbol information file (supported on Windows only)