diff --git a/eng/Subsets.props b/eng/Subsets.props
index e16c5fb861462b..874f666820ee5b 100644
--- a/eng/Subsets.props
+++ b/eng/Subsets.props
@@ -525,6 +525,8 @@
Test="true" Category="clr" Condition="'$(DotNetBuildSourceOnly)' != 'true'"/>
+
diff --git a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs
index 931a95bb4b177f..128a30753aab39 100644
--- a/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs
+++ b/src/coreclr/tools/Common/TypeSystem/Common/Utilities/CustomAttributeTypeNameParser.cs
@@ -36,6 +36,16 @@ public static TypeDesc GetTypeByCustomAttributeTypeName(this ModuleDesc module,
}.Resolve(parsed);
}
+ ///
+ /// Determines whether is a valid assembly-qualified type name,
+ /// including assembly qualification for constructed generic arguments.
+ ///
+ public static bool IsAssemblyQualifiedTypeName(string name)
+ {
+ return TypeName.TryParse(name.AsSpan(), out TypeName parsed, s_typeNameParseOptions)
+ && IsFullyQualified(parsed);
+ }
+
public static TypeDesc GetTypeByCustomAttributeTypeNameForDataFlow(string name, ModuleDesc callingModule,
TypeSystemContext context, List referencedModules, bool needsAssemblyName, bool fallbackToCoreLib, out bool failedBecauseNotFullyQualified)
{
@@ -65,32 +75,32 @@ public static TypeDesc GetTypeByCustomAttributeTypeNameForDataFlow(string name,
TypeDesc type = resolver.Resolve(parsed);
return type;
+ }
- static bool IsFullyQualified(TypeName typeName)
+ private static bool IsFullyQualified(TypeName typeName)
+ {
+ if (typeName.AssemblyName is null)
{
- if (typeName.AssemblyName is null)
- {
- return false;
- }
+ return false;
+ }
- if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef)
- {
- return IsFullyQualified(typeName.GetElementType());
- }
+ if (typeName.IsArray || typeName.IsPointer || typeName.IsByRef)
+ {
+ return IsFullyQualified(typeName.GetElementType());
+ }
- if (typeName.IsConstructedGenericType)
+ if (typeName.IsConstructedGenericType)
+ {
+ foreach (TypeName typeArgument in typeName.GetGenericArguments())
{
- foreach (var typeArgument in typeName.GetGenericArguments())
+ if (!IsFullyQualified(typeArgument))
{
- if (!IsFullyQualified(typeArgument))
- {
- return false;
- }
+ return false;
}
}
-
- return true;
}
+
+ return true;
}
private struct TypeNameResolver
diff --git a/src/coreclr/tools/dotnet-pgo/MethodListMibcWriter.cs b/src/coreclr/tools/dotnet-pgo/MethodListMibcWriter.cs
new file mode 100644
index 00000000000000..1892f84a2334d7
--- /dev/null
+++ b/src/coreclr/tools/dotnet-pgo/MethodListMibcWriter.cs
@@ -0,0 +1,335 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using ILCompiler;
+using ILCompiler.IBC;
+
+using Internal.TypeSystem;
+
+namespace Microsoft.Diagnostics.Tools.Pgo
+{
+ internal static class MethodListMibcWriter
+ {
+ private sealed class MethodListDocument
+ {
+ public string Runtime { get; set; }
+
+ public string Os { get; set; }
+
+ public string Architecture { get; set; }
+
+ public MethodListEntry[] Methods { get; set; }
+ }
+
+ private sealed class MethodListEntry
+ {
+ public string Type { get; set; }
+
+ public string Name { get; set; }
+
+ public string[] ParameterTypes { get; set; }
+
+ public string[] GenericArguments { get; set; }
+ }
+
+ public static int Run(string methodListPath, string outputPath, IReadOnlyList references, bool compressed, bool validate)
+ {
+ try
+ {
+ ValidateInputPaths(methodListPath, outputPath, references);
+ MethodListDocument document = ReadDocument(methodListPath);
+
+ using var context = new MethodListTypeSystemContext(references);
+ List methods = ResolveMethods(context, document);
+ methods.Sort(static (left, right) => StringComparer.Ordinal.Compare(GetSortKey(left), GetSortKey(right)));
+
+ var config = new MibcConfig
+ {
+ Runtime = document.Runtime,
+ Os = document.Os,
+ Arch = document.Architecture,
+ };
+
+ var profileData = new MethodProfileData[methods.Count];
+ for (int i = 0; i < methods.Count; i++)
+ {
+ profileData[i] = new MethodProfileData(
+ methods[i],
+ MethodProfilingDataFlags.ReadMethodCode,
+ exclusiveWeight: 0,
+ callWeights: null,
+ scenarioMask: 0xFFFFFFFF,
+ schemaData: null);
+ }
+
+ return MibcEmitter.GenerateMibcFile(
+ config,
+ context,
+ new FileInfo(outputPath),
+ profileData,
+ validate,
+ uncompressed: !compressed);
+ }
+ catch (Exception ex) when (ex is IOException or JsonException or InvalidDataException or TypeSystemException or BadImageFormatException or FormatException)
+ {
+ Program.PrintError(ex.Message);
+ return -6;
+ }
+ }
+
+ private static string GetSortKey(MethodDesc method)
+ {
+ return $"{((MetadataType)method.OwningType.GetTypeDefinition()).Module.Assembly.GetName().Name}:{method}";
+ }
+
+ private static MethodListDocument ReadDocument(string methodListPath)
+ {
+ MethodListDocument document = JsonSerializer.Deserialize(
+ File.ReadAllText(methodListPath),
+ new JsonSerializerOptions
+ {
+ AllowTrailingCommas = true,
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
+ });
+
+ if (document is null)
+ {
+ throw new InvalidDataException($"Method list '{methodListPath}' is empty.");
+ }
+
+ if (string.IsNullOrWhiteSpace(document.Runtime) ||
+ string.IsNullOrWhiteSpace(document.Os) ||
+ string.IsNullOrWhiteSpace(document.Architecture))
+ {
+ throw new InvalidDataException("The method list must specify non-empty runtime, os, and architecture values.");
+ }
+
+ if (document.Methods is null || document.Methods.Length == 0)
+ {
+ throw new InvalidDataException($"Method list '{methodListPath}' contains no methods.");
+ }
+
+ return document;
+ }
+
+ private static List ResolveMethods(MethodListTypeSystemContext context, MethodListDocument document)
+ {
+ var methods = new List(document.Methods.Length);
+ var seenMethods = new HashSet();
+
+ foreach (MethodListEntry entry in document.Methods)
+ {
+ MethodDesc method = ResolveMethod(context, entry);
+ if (!seenMethods.Add(method))
+ {
+ throw new InvalidDataException($"Method list contains duplicate entry '{GetSortKey(method)}'.");
+ }
+
+ methods.Add(method);
+ }
+
+ return methods;
+ }
+
+ private static MethodDesc ResolveMethod(MethodListTypeSystemContext context, MethodListEntry entry)
+ {
+ if (entry is null || string.IsNullOrWhiteSpace(entry.Type) || string.IsNullOrWhiteSpace(entry.Name))
+ {
+ throw new InvalidDataException("Each method entry must specify non-empty type and name values.");
+ }
+
+ TypeDesc owningType = ResolveClosedType(context, entry.Type, $"declaring type for method '{entry.Name}'");
+ TypeDesc[] genericArguments = ResolveTypes(context, entry.GenericArguments, $"generic arguments for '{entry.Type}.{entry.Name}'");
+ TypeDesc[] parameterTypes = ResolveTypes(context, entry.ParameterTypes, $"parameter types for '{entry.Type}.{entry.Name}'");
+
+ MethodDesc match = null;
+ bool foundName = false;
+ bool foundArity = false;
+ foreach (MethodDesc candidate in owningType.GetMethods())
+ {
+ if (!candidate.Name.StringEquals(entry.Name))
+ {
+ continue;
+ }
+
+ foundName = true;
+ if (candidate.Instantiation.Length != genericArguments.Length)
+ {
+ continue;
+ }
+
+ foundArity = true;
+ MethodDesc instantiatedCandidate = genericArguments.Length == 0
+ ? candidate
+ : candidate.MakeInstantiatedMethod(genericArguments);
+
+ if (entry.ParameterTypes is not null && !ParametersMatch(instantiatedCandidate.Signature, parameterTypes))
+ {
+ continue;
+ }
+
+ if (match is not null)
+ {
+ throw new InvalidDataException(
+ $"Method '{entry.Type}.{entry.Name}' is ambiguous; specify parameterTypes to select an overload.");
+ }
+
+ match = instantiatedCandidate;
+ }
+
+ if (match is not null)
+ {
+ return match;
+ }
+
+ if (foundName && !foundArity)
+ {
+ throw new InvalidDataException(
+ $"Method '{entry.Type}.{entry.Name}' has no overload with generic arity {genericArguments.Length}.");
+ }
+
+ throw new InvalidDataException($"Unable to resolve method '{entry.Type}.{entry.Name}'.");
+ }
+
+ private static TypeDesc ResolveClosedType(MethodListTypeSystemContext context, string typeName, string description)
+ {
+ if (!CustomAttributeTypeNameParser.IsAssemblyQualifiedTypeName(typeName))
+ {
+ throw new InvalidDataException($"Type '{typeName}' in {description} must be assembly-qualified.");
+ }
+
+ int assemblySeparator = GetAssemblySeparator(typeName);
+ Debug.Assert(assemblySeparator >= 0);
+ AssemblyNameInfo assemblyName = AssemblyNameInfo.Parse(typeName.AsSpan(assemblySeparator + 1).Trim());
+ if (!context.HasAssembly(assemblyName))
+ {
+ throw new FileNotFoundException(
+ $"No reference was supplied for assembly '{assemblyName.Name}' required by type '{typeName}'.");
+ }
+
+ TypeDesc type = context.SystemModule.GetTypeByCustomAttributeTypeName(typeName, throwIfNotFound: false);
+ if (type is null)
+ {
+ throw new InvalidDataException($"Unable to resolve type '{typeName}' in {description}.");
+ }
+
+ if (type.ContainsSignatureVariables(treatGenericParameterLikeSignatureVariable: true))
+ {
+ throw new InvalidDataException($"Open generic type '{typeName}' in {description} is not supported.");
+ }
+
+ return type;
+ }
+
+ private static TypeDesc[] ResolveTypes(MethodListTypeSystemContext context, string[] typeNames, string description)
+ {
+ if (typeNames is null)
+ {
+ return Array.Empty();
+ }
+
+ var types = new TypeDesc[typeNames.Length];
+ for (int i = 0; i < typeNames.Length; i++)
+ {
+ if (string.IsNullOrWhiteSpace(typeNames[i]))
+ {
+ throw new InvalidDataException($"Type names in {description} must be non-empty.");
+ }
+
+ types[i] = ResolveClosedType(context, typeNames[i], description);
+ }
+
+ return types;
+ }
+
+ private static int GetAssemblySeparator(string typeName)
+ {
+ int bracketDepth = 0;
+ bool escaped = false;
+ for (int i = 0; i < typeName.Length; i++)
+ {
+ char character = typeName[i];
+ if (escaped)
+ {
+ escaped = false;
+ continue;
+ }
+
+ if (character == '\\')
+ {
+ escaped = true;
+ }
+ else if (character == '[')
+ {
+ bracketDepth++;
+ }
+ else if (character == ']')
+ {
+ bracketDepth--;
+ }
+ else if (character == ',' && bracketDepth == 0)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static bool ParametersMatch(MethodSignature signature, TypeDesc[] parameterTypes)
+ {
+ if (signature.Length != parameterTypes.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < parameterTypes.Length; i++)
+ {
+ if (signature[i] != parameterTypes[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static void ValidateInputPaths(string methodListPath, string outputPath, IReadOnlyList references)
+ {
+ if (string.IsNullOrWhiteSpace(methodListPath))
+ {
+ throw new InvalidDataException("A method list filename must be specified.");
+ }
+
+ if (!File.Exists(methodListPath))
+ {
+ throw new FileNotFoundException($"Unable to find method list '{methodListPath}'.", methodListPath);
+ }
+
+ if (string.IsNullOrWhiteSpace(outputPath))
+ {
+ throw new InvalidDataException("An output filename must be specified.");
+ }
+
+ if (!outputPath.EndsWith(".mibc", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidDataException("The Mibc output filename must end with '.mibc'.");
+ }
+
+ if (references is null || references.Count == 0)
+ {
+ throw new InvalidDataException("At least one reference assembly must be specified.");
+ }
+ }
+ }
+}
diff --git a/src/coreclr/tools/dotnet-pgo/MethodListTypeSystemContext.cs b/src/coreclr/tools/dotnet-pgo/MethodListTypeSystemContext.cs
new file mode 100644
index 00000000000000..bfdc9a61a79528
--- /dev/null
+++ b/src/coreclr/tools/dotnet-pgo/MethodListTypeSystemContext.cs
@@ -0,0 +1,108 @@
+// 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.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+using Internal.TypeSystem;
+using Internal.TypeSystem.Ecma;
+
+namespace Microsoft.Diagnostics.Tools.Pgo
+{
+ internal sealed class MethodListTypeSystemContext : MetadataTypeSystemContext, IMetadataStringDecoderProvider, IDisposable
+ {
+ private readonly Dictionary _modules = new(StringComparer.OrdinalIgnoreCase);
+ private readonly List _streams = new();
+ private readonly List _readers = new();
+ private MetadataStringDecoder _metadataStringDecoder;
+
+ public MethodListTypeSystemContext(IReadOnlyList references)
+ {
+ try
+ {
+ foreach (string reference in references)
+ {
+ if (!File.Exists(reference))
+ {
+ throw new FileNotFoundException($"Unable to find reference '{reference}'.", reference);
+ }
+
+ FileStream stream = File.OpenRead(reference);
+ _streams.Add(stream);
+
+ var reader = new PEReader(stream);
+ _readers.Add(reader);
+
+ if (!reader.HasMetadata)
+ {
+ throw new BadImageFormatException($"Reference '{reference}' does not contain managed metadata.", reference);
+ }
+
+ EcmaModule module = EcmaModule.Create(this, reader, containingAssembly: null);
+ string simpleName = module.Assembly.GetName().Name;
+ if (!_modules.TryAdd(simpleName, module))
+ {
+ throw new InvalidDataException($"Multiple references define assembly '{simpleName}'.");
+ }
+ }
+
+ if (!_modules.TryGetValue("System.Private.CoreLib", out ModuleDesc systemModule))
+ {
+ throw new InvalidDataException("A reference to System.Private.CoreLib is required.");
+ }
+
+ SetSystemModule(systemModule);
+ }
+ catch
+ {
+ Dispose();
+ throw;
+ }
+ }
+
+ public override bool SupportsCanon => true;
+
+ public override bool SupportsUniversalCanon => false;
+
+ public bool HasAssembly(AssemblyNameInfo name)
+ {
+ return _modules.ContainsKey(name.Name);
+ }
+
+ public void Dispose()
+ {
+ foreach (PEReader reader in _readers)
+ {
+ reader.Dispose();
+ }
+
+ foreach (FileStream stream in _streams)
+ {
+ stream.Dispose();
+ }
+ }
+
+ public MetadataStringDecoder GetMetadataStringDecoder()
+ {
+ return _metadataStringDecoder ??= new CachingMetadataStringDecoder(0x10000);
+ }
+
+ public override ModuleDesc ResolveAssembly(AssemblyNameInfo name, bool throwIfNotFound)
+ {
+ if (_modules.TryGetValue(name.Name, out ModuleDesc module))
+ {
+ return module;
+ }
+
+ if (throwIfNotFound)
+ {
+ throw new FileNotFoundException($"Unable to resolve assembly '{name.Name}'.");
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/coreclr/tools/dotnet-pgo/MibcEmitter.cs b/src/coreclr/tools/dotnet-pgo/MibcEmitter.cs
index 2379a45e31f31f..6980a4c649c0b0 100644
--- a/src/coreclr/tools/dotnet-pgo/MibcEmitter.cs
+++ b/src/coreclr/tools/dotnet-pgo/MibcEmitter.cs
@@ -32,6 +32,8 @@ namespace Microsoft.Diagnostics.Tools.Pgo
{
static class MibcEmitter
{
+ private static readonly DateTimeOffset s_deterministicZipTimestamp = new(1980, 1, 1, 0, 0, 0, TimeSpan.Zero);
+
class MIbcGroup : IPgoEncodedValueEmitter
{
private static int s_emitCount = 0;
@@ -323,6 +325,7 @@ public static int GenerateMibcFile(MibcConfig config, TypeSystemContext tsc, Fil
using (ZipArchive file = ZipFile.Open(outputFileName.FullName, ZipArchiveMode.Create))
{
var entry = file.CreateEntry(outputFileName.Name + ".dll", CompressionLevel.Optimal);
+ entry.LastWriteTime = s_deterministicZipTimestamp;
using (Stream archiveStream = entry.Open())
{
peFile.CopyTo(archiveStream);
diff --git a/src/coreclr/tools/dotnet-pgo/PgoRootCommand.cs b/src/coreclr/tools/dotnet-pgo/PgoRootCommand.cs
index 179aa957b336dd..0f492fe45088c7 100644
--- a/src/coreclr/tools/dotnet-pgo/PgoRootCommand.cs
+++ b/src/coreclr/tools/dotnet-pgo/PgoRootCommand.cs
@@ -23,6 +23,8 @@ internal sealed class PgoRootCommand : RootCommand
new("--input", "-i") { Description = "Name of the input mibc file to dump", Required = true, Arity = ArgumentArity.ExactlyOne };
public Option TraceFilePath { get; } =
new("--trace", "-t") { Description = "Specify the trace file to be parsed" };
+ public Option MethodListFilePath { get; } =
+ new("--method-list") { Description = "Specify a JSON file containing methods to place in a Mibc file", Required = true };
public Option OutputFilePath { get; } =
new("--output", "-o") { Description = "Specify the output filename to be created" };
public Option PreciseDebugInfoFile { get; } =
@@ -82,6 +84,7 @@ internal sealed class PgoRootCommand : RootCommand
public JitTraceOptions JitTraceOptions;
public bool Warnings;
public bool BasicProgressMessages;
+ public bool CreateMibcFromMethodList;
public bool DetailedProgressMessages;
public bool DumpMibc;
public ParseResult Result;
@@ -137,6 +140,25 @@ public PgoRootCommand(string[] args) : base(".NET PGO Tool")
Subcommands.Add(createMbicCommand);
+ Command createMibcFromMethodListCommand = new("create-mibc-from-method-list", "Create a Mibc profile data file from a JSON method list")
+ {
+ MethodListFilePath,
+ OutputFilePath,
+ Reference,
+ _verbosity,
+ Compressed,
+ };
+
+ createMibcFromMethodListCommand.SetAction(result =>
+ {
+ CreateMibcFromMethodList = true;
+ ValidateOutputFile = true;
+
+ return ExecuteWithContext(result, true);
+ });
+
+ Subcommands.Add(createMibcFromMethodListCommand);
+
JitTraceOptions = JitTraceOptions.none;
#if DEBUG
Command createJitTraceCommand = new("create-jittrace","Transform a trace file into a jittrace runtime file")
diff --git a/src/coreclr/tools/dotnet-pgo/Program.cs b/src/coreclr/tools/dotnet-pgo/Program.cs
index 5a0e2d1fe14111..96d9d7fdbcc1f4 100644
--- a/src/coreclr/tools/dotnet-pgo/Program.cs
+++ b/src/coreclr/tools/dotnet-pgo/Program.cs
@@ -261,6 +261,15 @@ public int Run()
{
return InnerDumpMain();
}
+ if (_command.CreateMibcFromMethodList)
+ {
+ return MethodListMibcWriter.Run(
+ Get(_command.MethodListFilePath),
+ Get(_command.OutputFilePath),
+ Get(_command.Reference),
+ Get(_command.Compressed),
+ _command.ValidateOutputFile);
+ }
if (_inputFilesToMerge.Count > 0)
{
return InnerMergeMain();
diff --git a/src/coreclr/tools/dotnet-pgo/README.md b/src/coreclr/tools/dotnet-pgo/README.md
index a377bf1e4e23e8..0058421bfa4bbe 100644
--- a/src/coreclr/tools/dotnet-pgo/README.md
+++ b/src/coreclr/tools/dotnet-pgo/README.md
@@ -25,6 +25,62 @@ static void FunctionCalledAtStartup()
## Consuming a .mibc file
Invoke the `crossgen2` tool with the --mibc option, passing the .mibc file created by dotnet-pgo.
+## Creating a .mibc file from a method list
+
+The `create-mibc-from-method-list` command creates a deterministic profile root set without collecting
+a trace. This is an exploratory, general-purpose way to construct profiles; it is not required for
+automatic hardware-intrinsic rooting in partial no-JIT compilations.
+
+The input is a JSON object with the following properties:
+
+- `runtime`, `os`, and `architecture`: non-empty strings written to the MIBC configuration.
+- `methods`: a non-empty array of method entries.
+- `methods[].type`: the assembly-qualified declaring type name. Constructed generic types must also
+ use assembly-qualified generic arguments.
+- `methods[].name`: the metadata method name.
+- `methods[].parameterTypes`: optional assembly-qualified parameter type names used to select an
+ overload. Omit only when the name and generic arity identify exactly one method.
+- `methods[].genericArguments`: optional assembly-qualified type names for a closed generic method
+ instantiation.
+
+All referenced assemblies, including `System.Private.CoreLib`, must be passed with `--reference`.
+Open generic types and methods are not supported. Duplicate resolved methods are rejected.
+
+```json
+{
+ "runtime": "CoreCLR",
+ "os": "linux",
+ "architecture": "x64",
+ "methods": [
+ {
+ "type": "Example.ProfileRoots, Example",
+ "name": "Parse",
+ "parameterTypes": [
+ "System.String, System.Private.CoreLib"
+ ]
+ },
+ {
+ "type": "Example.ProfileRoots, Example",
+ "name": "Create",
+ "genericArguments": [
+ "System.Int32, System.Private.CoreLib"
+ ],
+ "parameterTypes": [
+ "System.Int32, System.Private.CoreLib"
+ ]
+ }
+ ]
+}
+```
+
+```console
+dotnet-pgo create-mibc-from-method-list \
+ --method-list methods.json \
+ --reference System.Private.CoreLib.dll \
+ --reference Example.dll \
+ --output methods.mibc
+```
+
## Example tracing commands used to generate the input to this tool:
Note, this tool requires MethodDetails events which are produced by the .NET 5.0 runtime, or by modifying the .NET Core 3 runtime to produce the event.
diff --git a/src/coreclr/tools/dotnet-pgo/dotnet-pgo.csproj b/src/coreclr/tools/dotnet-pgo/dotnet-pgo.csproj
index 40a919e7b9ba90..577452100b9a81 100644
--- a/src/coreclr/tools/dotnet-pgo/dotnet-pgo.csproj
+++ b/src/coreclr/tools/dotnet-pgo/dotnet-pgo.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/coreclr/tools/dotnet-pgo/tests/MethodListMibcTests.cs b/src/coreclr/tools/dotnet-pgo/tests/MethodListMibcTests.cs
new file mode 100644
index 00000000000000..44153869a244dc
--- /dev/null
+++ b/src/coreclr/tools/dotnet-pgo/tests/MethodListMibcTests.cs
@@ -0,0 +1,330 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+
+using Microsoft.Diagnostics.Tools.Pgo;
+
+using Xunit;
+
+namespace DotNetPgo.Tests
+{
+ public sealed class MethodListMibcTests
+ {
+ private static readonly string s_dotnetPgoPath = typeof(PgoFileType).Assembly.Location;
+ private static readonly string[] s_platformReferences =
+ ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator);
+ private static readonly string s_testAssemblyPath = typeof(MethodListMibcTests).Assembly.Location;
+ private static readonly string s_testAssemblyName = typeof(MethodListMibcTests).Assembly.GetName().Name;
+
+ [Fact]
+ public void CreatesDeterministicMibcAndRoundTripsThroughDump()
+ {
+ using var directory = new TemporaryDirectory();
+ string firstMethodList = directory.WriteJson(
+ "first.json",
+ CreateDocument(
+ Method(nameof(MethodListTargets.Generic), [QualifiedName(typeof(int))], [QualifiedName(typeof(int))]),
+ Method("Hidden"),
+ Method(nameof(MethodListTargets.Overloaded), parameterTypes: [QualifiedName(typeof(string))])));
+ string secondMethodList = directory.WriteJson(
+ "second.json",
+ CreateDocument(
+ Method(nameof(MethodListTargets.Overloaded), parameterTypes: [QualifiedName(typeof(string))]),
+ Method("Hidden"),
+ Method(nameof(MethodListTargets.Generic), [QualifiedName(typeof(int))], [QualifiedName(typeof(int))])));
+
+ string mibcPath = Path.Combine(directory.Path, "methods.mibc");
+
+ CommandResult firstCreate = RunCreate(firstMethodList, mibcPath);
+ AssertSuccess(firstCreate);
+ Assert.Contains($"Validated {mibcPath}", firstCreate.StandardOutput, StringComparison.Ordinal);
+ byte[] firstMibc = File.ReadAllBytes(mibcPath);
+ AssertSuccess(RunCreate(secondMethodList, mibcPath));
+ Assert.Equal(firstMibc, File.ReadAllBytes(mibcPath));
+
+ string dumpPath = Path.Combine(directory.Path, "dump.json");
+ AssertSuccess(Run("dump", "--input", mibcPath, "--output", dumpPath));
+
+ using JsonDocument dump = JsonDocument.Parse(File.ReadAllText(dumpPath));
+ JsonElement methods = dump.RootElement.GetProperty("Methods");
+ Assert.Equal(3, methods.GetArrayLength());
+ Assert.All(methods.EnumerateArray(), method =>
+ {
+ Assert.False(method.TryGetProperty("CallWeights", out _));
+ Assert.False(method.TryGetProperty("ExclusiveWeight", out _));
+ Assert.False(method.TryGetProperty("InstrumentationData", out _));
+ });
+ }
+
+ [Fact]
+ public void RejectsAmbiguousMethod()
+ {
+ using var directory = new TemporaryDirectory();
+ string methodList = directory.WriteJson(
+ "ambiguous.json",
+ CreateDocument(Method(nameof(MethodListTargets.Overloaded))));
+
+ CommandResult result = RunCreate(methodList, Path.Combine(directory.Path, "ambiguous.mibc"));
+
+ AssertFailure(result, "is ambiguous");
+ }
+
+ [Fact]
+ public void RejectsDuplicateMethod()
+ {
+ using var directory = new TemporaryDirectory();
+ Dictionary method = Method("Hidden");
+ string methodList = directory.WriteJson("duplicate.json", CreateDocument(method, method));
+
+ CommandResult result = RunCreate(methodList, Path.Combine(directory.Path, "duplicate.mibc"));
+
+ AssertFailure(result, "duplicate entry");
+ }
+
+ [Fact]
+ public void RejectsInvalidMethodAndGenericArity()
+ {
+ using var directory = new TemporaryDirectory();
+ string missingMethodList = directory.WriteJson(
+ "missing-method.json",
+ CreateDocument(Method("Missing")));
+ string wrongArityMethodList = directory.WriteJson(
+ "wrong-arity.json",
+ CreateDocument(Method(nameof(MethodListTargets.Generic))));
+
+ AssertFailure(
+ RunCreate(missingMethodList, Path.Combine(directory.Path, "missing-method.mibc")),
+ "Unable to resolve method");
+ AssertFailure(
+ RunCreate(wrongArityMethodList, Path.Combine(directory.Path, "wrong-arity.mibc")),
+ "generic arity 0");
+ }
+
+ [Fact]
+ public void RejectsInvalidJsonMissingTypeAndReference()
+ {
+ using var directory = new TemporaryDirectory();
+ string invalidJson = Path.Combine(directory.Path, "invalid.json");
+ File.WriteAllText(invalidJson, "{");
+ string missingType = directory.WriteJson(
+ "missing-type.json",
+ CreateDocument(
+ new Dictionary
+ {
+ ["type"] = $"Missing.Type, {s_testAssemblyName}",
+ ["name"] = "Method",
+ }));
+
+ AssertFailure(RunCreate(invalidJson, Path.Combine(directory.Path, "invalid.mibc")), "JSON");
+ AssertFailure(RunCreate(missingType, Path.Combine(directory.Path, "missing-type.mibc")), "Unable to resolve type");
+
+ string validMethod = directory.WriteJson(
+ "missing-reference.json",
+ CreateDocument(Method("Hidden")));
+ var arguments = new List
+ {
+ "create-mibc-from-method-list",
+ "--method-list", validMethod,
+ };
+ AddReferences(arguments, includeTestAssembly: false);
+ arguments.AddRange(
+ [
+ "--output", Path.Combine(directory.Path, "missing-reference.mibc"),
+ "--compressed", "false",
+ ]);
+ AssertFailure(Run(arguments.ToArray()), $"No reference was supplied for assembly '{s_testAssemblyName}'");
+ }
+
+ [Fact]
+ public void RejectsNonMibcOutput()
+ {
+ using var directory = new TemporaryDirectory();
+ string methodList = directory.WriteJson(
+ "method.json",
+ CreateDocument(Method("Hidden")));
+
+ AssertFailure(RunCreate(methodList, Path.Combine(directory.Path, "output.bin")), "must end with '.mibc'");
+ }
+
+ [Fact]
+ public void RejectsUnqualifiedConstructedGenericArgument()
+ {
+ using var directory = new TemporaryDirectory();
+ string methodList = directory.WriteJson(
+ "unqualified-generic-argument.json",
+ CreateDocument(
+ new Dictionary
+ {
+ ["type"] = "System.Collections.Generic.List`1[[System.String]], System.Private.CoreLib",
+ ["name"] = "Add",
+ ["parameterTypes"] = new[] { QualifiedName(typeof(string)) },
+ }));
+
+ AssertFailure(
+ RunCreate(methodList, Path.Combine(directory.Path, "unqualified-generic-argument.mibc")),
+ "must be assembly-qualified");
+ }
+
+ private static void AssertFailure(CommandResult result, string expectedError)
+ {
+ Assert.NotEqual(0, result.ExitCode);
+ Assert.Contains(expectedError, result.StandardError, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static void AssertSuccess(CommandResult result)
+ {
+ Assert.True(
+ result.ExitCode == 0,
+ $"Command failed with exit code {result.ExitCode}.{Environment.NewLine}stdout:{Environment.NewLine}{result.StandardOutput}{Environment.NewLine}stderr:{Environment.NewLine}{result.StandardError}");
+ }
+
+ private static Dictionary CreateDocument(params Dictionary[] methods)
+ {
+ return new Dictionary
+ {
+ ["runtime"] = "CoreCLR",
+ ["os"] = "test",
+ ["architecture"] = "test",
+ ["methods"] = methods,
+ };
+ }
+
+ private static Dictionary Method(
+ string name,
+ string[] genericArguments = null,
+ string[] parameterTypes = null)
+ {
+ var method = new Dictionary
+ {
+ ["type"] = QualifiedName(typeof(MethodListTargets)),
+ ["name"] = name,
+ };
+
+ if (genericArguments is not null)
+ {
+ method["genericArguments"] = genericArguments;
+ }
+
+ if (parameterTypes is not null)
+ {
+ method["parameterTypes"] = parameterTypes;
+ }
+
+ return method;
+ }
+
+ private static string QualifiedName(Type type)
+ {
+ return type.AssemblyQualifiedName;
+ }
+
+ private static CommandResult RunCreate(string methodList, string output)
+ {
+ var arguments = new List
+ {
+ "create-mibc-from-method-list",
+ "--method-list", methodList,
+ };
+ AddReferences(arguments, includeTestAssembly: true);
+ arguments.AddRange(
+ [
+ "--output", output,
+ ]);
+
+ return Run(arguments.ToArray());
+ }
+
+ private static void AddReferences(List arguments, bool includeTestAssembly)
+ {
+ var assemblyNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (string reference in s_platformReferences)
+ {
+ string assemblyName = AssemblyName.GetAssemblyName(reference).Name;
+ if ((!includeTestAssembly && assemblyName == s_testAssemblyName) || !assemblyNames.Add(assemblyName))
+ {
+ continue;
+ }
+
+ arguments.Add("--reference");
+ arguments.Add(reference);
+ }
+
+ if (includeTestAssembly && assemblyNames.Add(s_testAssemblyName))
+ {
+ arguments.Add("--reference");
+ arguments.Add(s_testAssemblyPath);
+ }
+ }
+
+ private static CommandResult Run(params string[] arguments)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = Environment.ProcessPath,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ };
+ startInfo.ArgumentList.Add(s_dotnetPgoPath);
+ foreach (string argument in arguments)
+ {
+ startInfo.ArgumentList.Add(argument);
+ }
+
+ using Process process = Process.Start(startInfo);
+ string standardOutput = process.StandardOutput.ReadToEnd();
+ string standardError = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ return new CommandResult(process.ExitCode, standardOutput, standardError);
+ }
+
+ private sealed class TemporaryDirectory : IDisposable
+ {
+ public TemporaryDirectory()
+ {
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
+ Directory.CreateDirectory(Path);
+ }
+
+ public string Path { get; }
+
+ public void Dispose()
+ {
+ Directory.Delete(Path, recursive: true);
+ }
+
+ public string WriteJson(string fileName, object value)
+ {
+ string path = System.IO.Path.Combine(Path, fileName);
+ File.WriteAllText(path, JsonSerializer.Serialize(value));
+ return path;
+ }
+ }
+
+ private sealed record CommandResult(int ExitCode, string StandardOutput, string StandardError);
+ }
+
+ internal static class MethodListTargets
+ {
+ internal static T Generic(T value) => value;
+
+ private static void Hidden()
+ {
+ }
+
+ internal static void Overloaded(int value)
+ {
+ }
+
+ internal static void Overloaded(string value)
+ {
+ }
+ }
+}
diff --git a/src/coreclr/tools/dotnet-pgo/tests/dotnet-pgo.Tests.csproj b/src/coreclr/tools/dotnet-pgo/tests/dotnet-pgo.Tests.csproj
new file mode 100644
index 00000000000000..3cdf044eeec2cc
--- /dev/null
+++ b/src/coreclr/tools/dotnet-pgo/tests/dotnet-pgo.Tests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ $(NetCoreAppToolCurrent)
+ AnyCPU
+
+
+
+
+
+
+
+
+
+
+