--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace ReadyToRun.SuperIlc
+{
+ public class Buckets
+ {
+ private Dictionary<string, List<ProcessInfo>> _bucketMap;
+
+ public Buckets()
+ {
+ _bucketMap = new Dictionary<string, List<ProcessInfo>>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public void AddCompilation(ProcessInfo process) => Add(AnalyzeCompilationFailure(process), process);
+ public void AddExecution(ProcessInfo process) => Add(AnalyzeExecutionFailure(process), process);
+
+ public void Add(string bucket, ProcessInfo process)
+ {
+ List<ProcessInfo> processes;
+ if (!_bucketMap.TryGetValue(bucket, out processes))
+ {
+ processes = new List<ProcessInfo>();
+ _bucketMap.Add(bucket, processes);
+ }
+ processes.Add(process);
+ }
+
+ public void WriteToFile(string outputFile, bool detailed)
+ {
+ using (StreamWriter outputStream = new StreamWriter(outputFile))
+ {
+ WriteToStream(outputStream, detailed);
+ }
+ }
+
+ public void WriteToStream(StreamWriter output, bool detailed)
+ {
+ output.WriteLine($@"#buckets: {_bucketMap.Count}, #failures: {_bucketMap.Sum(b => b.Value.Count)}");
+
+ if (_bucketMap.Count == 0)
+ {
+ // No bucketing info to display
+ return;
+ }
+
+ IEnumerable<KeyValuePair<string, List<ProcessInfo>>> orderedBuckets = _bucketMap.OrderByDescending(bucket => bucket.Value.Count);
+ foreach (KeyValuePair<string, List<ProcessInfo>> bucketKvp in orderedBuckets)
+ {
+ bucketKvp.Value.Sort((a, b) => a.Parameters.InputFileName.CompareTo(b.Parameters.InputFileName));
+ output.WriteLine($@" [{bucketKvp.Value.Count} failures] {bucketKvp.Key}");
+ }
+
+ output.WriteLine();
+ output.WriteLine("Detailed bucket info:");
+
+ foreach (KeyValuePair<string, List<ProcessInfo>> bucketKvp in orderedBuckets)
+ {
+ output.WriteLine("");
+ output.WriteLine($@"Bucket name: {bucketKvp.Key}");
+ output.WriteLine($@"Failing tests ({bucketKvp.Value.Count} total):");
+
+ foreach (ProcessInfo failure in bucketKvp.Value)
+ {
+ output.WriteLine($@" {failure.Parameters.InputFileName}");
+ }
+
+ if (detailed)
+ {
+ output.WriteLine();
+ output.WriteLine($@"Detailed test failures:");
+
+ foreach (ProcessInfo failure in bucketKvp.Value)
+ {
+ output.WriteLine($@"Test: {failure.Parameters.InputFileName}");
+ try
+ {
+ output.WriteLine(File.ReadAllText(failure.Parameters.LogPath));
+ }
+ catch (Exception ex)
+ {
+ output.WriteLine($"Error reading file {failure.Parameters.LogPath}: {ex.Message}");
+ }
+ output.WriteLine();
+ }
+ }
+ }
+ }
+
+ private static string AnalyzeCompilationFailure(ProcessInfo process)
+ {
+ try
+ {
+ if (process.TimedOut)
+ {
+ return "Timed out";
+ }
+
+ string[] lines = File.ReadAllLines(process.Parameters.LogPath);
+
+ for (int lineIndex = 2; lineIndex < lines.Length; lineIndex++)
+ {
+ string line = lines[lineIndex];
+ if (line.Length == 0 ||
+ line.StartsWith("EXEC : warning") ||
+ line.StartsWith("To repro,") ||
+ line.StartsWith("Emitting R2R PE file") ||
+ line.StartsWith("Warning: ") ||
+ line.StartsWith("Info: ") ||
+ line == "Assertion Failed")
+ {
+ continue;
+ }
+ return line;
+ }
+ return string.Join("; ", lines);
+ }
+ catch (Exception ex)
+ {
+ return ex.Message;
+ }
+ }
+
+ private static string AnalyzeExecutionFailure(ProcessInfo process)
+ {
+ try
+ {
+ if (process.TimedOut)
+ {
+ return "Timed out";
+ }
+
+ string[] lines = File.ReadAllLines(process.Parameters.LogPath);
+
+ for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
+ {
+ string line = lines[lineIndex];
+ if (line.StartsWith("Assert failure"))
+ {
+ int openParen = line.IndexOf('(');
+ int closeParen = line.IndexOf(')', openParen + 1);
+ if (openParen > 0 && closeParen > openParen)
+ {
+ line = line.Substring(0, openParen) + line.Substring(closeParen + 1);
+ }
+ return line;
+ }
+ else if (line.StartsWith("Unhandled exception", StringComparison.OrdinalIgnoreCase))
+ {
+ int leftBracket = line.IndexOf('[');
+ int rightBracket = line.IndexOf(']', leftBracket + 1);
+ if (leftBracket >= 0 && rightBracket > leftBracket)
+ {
+ line = line.Substring(0, leftBracket) + line.Substring(rightBracket + 1);
+ }
+ for (int detailLineIndex = lineIndex + 1; detailLineIndex < lines.Length; detailLineIndex++)
+ {
+ string detailLine = lines[detailLineIndex].TrimStart();
+ if (!detailLine.StartsWith("--->"))
+ {
+ break;
+ }
+ line += " " + detailLine;
+ }
+ return line;
+ }
+ }
+
+ return $"Exit code: {process.ExitCode} = 0x{process.ExitCode:X8}, expected {process.Parameters.ExpectedExitCode}";
+ }
+ catch (Exception ex)
+ {
+ return ex.Message;
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace ReadyToRun.SuperIlc
+{
+ public class BuildFolder
+ {
+ private List<string> _compilationInputFiles;
+
+ private List<string> _mainExecutables;
+
+ private List<string> _executionScripts;
+
+ private readonly List<ProcessInfo[]> _compilations;
+
+ private string _inputFolder;
+
+ private string _outputFolder;
+
+ private readonly List<ProcessInfo[]> _executions;
+
+ public string IssueID;
+
+ public BuildFolder(
+ List<string> compilationInputFiles,
+ List<string> mainExecutables,
+ List<string> executionScripts,
+ IEnumerable<CompilerRunner> compilerRunners,
+ string inputFolder,
+ string outputFolder,
+ BuildOptions options)
+ {
+ _compilationInputFiles = compilationInputFiles;
+ _mainExecutables = mainExecutables;
+ _executionScripts = executionScripts;
+ _inputFolder = inputFolder;
+ _outputFolder = outputFolder;
+
+ _compilations = new List<ProcessInfo[]>();
+ _executions = new List<ProcessInfo[]>();
+
+ foreach (string file in _compilationInputFiles)
+ {
+ ProcessInfo[] fileCompilations = new ProcessInfo[(int)CompilerIndex.Count];
+ foreach (CompilerRunner runner in compilerRunners)
+ {
+ ProcessInfo compilationProcess = new ProcessInfo(new CompilationProcessConstructor(runner, _outputFolder, file));
+ fileCompilations[(int)runner.Index] = compilationProcess;
+ }
+ _compilations.Add(fileCompilations);
+ }
+
+ if (!options.NoExe)
+ {
+ HashSet<string> scriptedExecutables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string script in _executionScripts ?? Enumerable.Empty<string>())
+ {
+ ProcessInfo[] scriptExecutions = new ProcessInfo[(int)CompilerIndex.Count];
+ _executions.Add(scriptExecutions);
+ scriptedExecutables.Add(Path.ChangeExtension(script, ".exe"));
+
+ foreach (CompilerRunner runner in compilerRunners)
+ {
+ HashSet<string> modules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ HashSet<string> folders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ modules.Add(runner.GetOutputFileName(_outputFolder, script));
+ modules.UnionWith(_compilationInputFiles);
+ modules.UnionWith(_compilationInputFiles.Select(file => runner.GetOutputFileName(_outputFolder, file)));
+ folders.Add(Path.GetDirectoryName(script));
+ folders.UnionWith(runner.ReferenceFolders);
+
+ scriptExecutions[(int)runner.Index] = new ProcessInfo(new ScriptExecutionProcessConstructor(runner, _outputFolder, script, modules, folders));
+ }
+ }
+
+ if (options.CoreRootDirectory != null)
+ {
+ foreach (string mainExe in _mainExecutables ?? Enumerable.Empty<string>())
+ {
+ if (scriptedExecutables.Contains(mainExe))
+ {
+ // Skip direct exe launch assuming it was run by the corresponding cmd script
+ continue;
+ }
+
+ ProcessInfo[] appExecutions = new ProcessInfo[(int)CompilerIndex.Count];
+ _executions.Add(appExecutions);
+ foreach (CompilerRunner runner in compilerRunners)
+ {
+ HashSet<string> modules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ HashSet<string> folders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ modules.Add(mainExe);
+ modules.Add(runner.GetOutputFileName(_outputFolder, mainExe));
+ modules.UnionWith(_compilationInputFiles);
+ modules.UnionWith(_compilationInputFiles.Select(file => runner.GetOutputFileName(_outputFolder, file)));
+ folders.Add(Path.GetDirectoryName(mainExe));
+ folders.UnionWith(runner.ReferenceFolders);
+
+ appExecutions[(int)runner.Index] = new ProcessInfo(new AppExecutionProcessConstructor(runner, _outputFolder, mainExe, modules, folders));
+ }
+ }
+ }
+ }
+ }
+
+ public static BuildFolder FromDirectory(string inputDirectory, IEnumerable<CompilerRunner> compilerRunners, string outputRoot, BuildOptions options)
+ {
+ List<string> compilationInputFiles = new List<string>();
+ List<string> passThroughFiles = new List<string>();
+ List<string> mainExecutables = new List<string>();
+ List<string> executionScripts = new List<string>();
+
+ string scriptExtension = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : ".sh");
+
+ // Copy unmanaged files (runtime, native dependencies, resources, etc)
+ foreach (string file in Directory.EnumerateFiles(inputDirectory))
+ {
+ bool isManagedAssembly = ComputeManagedAssemblies.IsManaged(file);
+ if (isManagedAssembly)
+ {
+ compilationInputFiles.Add(file);
+ }
+ else
+ {
+ passThroughFiles.Add(file);
+ }
+ string ext = Path.GetExtension(file);
+ if (ext.Equals(".exe", StringComparison.OrdinalIgnoreCase))
+ {
+ mainExecutables.Add(file);
+ }
+ else if (ext.Equals(scriptExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ executionScripts.Add(file);
+ }
+ }
+
+ if (compilationInputFiles.Count == 0)
+ {
+ return null;
+ }
+
+ foreach (CompilerRunner runner in compilerRunners)
+ {
+ string runnerOutputPath = runner.GetOutputPath(outputRoot);
+ runnerOutputPath.RecreateDirectory();
+ foreach (string file in passThroughFiles)
+ {
+ File.Copy(file, Path.Combine(runnerOutputPath, Path.GetFileName(file)));
+ }
+ }
+
+ return new BuildFolder(compilationInputFiles, mainExecutables, executionScripts, compilerRunners, inputDirectory, outputRoot, options);
+ }
+
+ public void AddModuleToJittedMethodsMapping(Dictionary<string, HashSet<string>> moduleToJittedMethods, int executionIndex, CompilerIndex compilerIndex)
+ {
+ ProcessInfo executionProcess = _executions[executionIndex][(int)compilerIndex];
+ if (executionProcess != null && executionProcess.JittedMethods != null)
+ {
+ foreach (KeyValuePair<string, HashSet<string>> moduleMethodKvp in executionProcess.JittedMethods)
+ {
+ HashSet<string> jittedMethodsPerModule;
+ if (!moduleToJittedMethods.TryGetValue(moduleMethodKvp.Key, out jittedMethodsPerModule))
+ {
+ jittedMethodsPerModule = new HashSet<string>();
+ moduleToJittedMethods.Add(moduleMethodKvp.Key, jittedMethodsPerModule);
+ }
+ jittedMethodsPerModule.UnionWith(moduleMethodKvp.Value);
+ }
+ }
+ }
+
+ public static void WriteJitStatistics(TextWriter writer, Dictionary<string, HashSet<string>>[] perCompilerStatistics, IEnumerable<CompilerRunner> compilerRunners)
+ {
+ Dictionary<string, int> moduleNameUnion = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
+ foreach (CompilerRunner compilerRunner in compilerRunners)
+ {
+ foreach (KeyValuePair<string, HashSet<string>> kvp in perCompilerStatistics[(int)compilerRunner.Index])
+ {
+ int methodCount;
+ moduleNameUnion.TryGetValue(kvp.Key, out methodCount);
+ moduleNameUnion[kvp.Key] = Math.Max(methodCount, kvp.Value.Count);
+ }
+ }
+
+ if (moduleNameUnion.Count == 0)
+ {
+ // No JIT statistics available
+ return;
+ }
+
+ writer.WriteLine();
+ writer.WriteLine("Jitted method statistics:");
+
+ foreach (CompilerRunner compilerRunner in compilerRunners)
+ {
+ writer.Write($"{compilerRunner.Index.ToString(),9} |");
+ }
+ writer.WriteLine(" Assembly Name");
+ writer.WriteLine(new string('-', 11 * compilerRunners.Count() + 14));
+ foreach (string moduleName in moduleNameUnion.OrderByDescending(kvp => kvp.Value).Select(kvp => kvp.Key))
+ {
+ foreach (CompilerRunner compilerRunner in compilerRunners)
+ {
+ HashSet<string> jittedMethodsPerModule;
+ perCompilerStatistics[(int)compilerRunner.Index].TryGetValue(moduleName, out jittedMethodsPerModule);
+ writer.Write(string.Format("{0,9} |", jittedMethodsPerModule != null ? jittedMethodsPerModule.Count.ToString() : ""));
+ }
+ writer.Write(' ');
+ writer.WriteLine(moduleName);
+ }
+ }
+
+ public void WriteJitStatistics(Dictionary<string, HashSet<string>>[] perCompilerStatistics, IEnumerable<CompilerRunner> compilerRunners)
+ {
+ for (int exeIndex = 0; exeIndex < _mainExecutables.Count; exeIndex++)
+ {
+ string jitStatisticsFile = Path.ChangeExtension(_mainExecutables[exeIndex], ".jit-statistics");
+ using (StreamWriter streamWriter = new StreamWriter(jitStatisticsFile))
+ {
+ WriteJitStatistics(streamWriter, perCompilerStatistics, compilerRunners);
+ }
+ }
+ }
+
+ public bool IsBlockedWithIssue => IssueID != null;
+
+ public string InputFolder => _inputFolder;
+
+ public string OutputFolder => _outputFolder;
+
+ public IList<string> MainExecutables => _mainExecutables;
+
+ public IList<String> ExecutionScripts => _executionScripts;
+
+ public IList<ProcessInfo[]> Compilations => _compilations;
+
+ public IList<ProcessInfo[]> Executions => _executions;
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using Microsoft.Diagnostics.Tracing.Parsers.Clr;
+
+namespace ReadyToRun.SuperIlc
+{
+ public class BuildFolderSet
+ {
+ private IEnumerable<BuildFolder> _buildFolders;
+
+ private IEnumerable<CompilerRunner> _compilerRunners;
+
+ private BuildOptions _options;
+
+ private Buckets _frameworkCompilationFailureBuckets;
+
+ private Buckets _compilationFailureBuckets;
+
+ private Buckets _executionFailureBuckets;
+
+ private long _frameworkCompilationMilliseconds;
+
+ private long _compilationMilliseconds;
+
+ private long _executionMilliseconds;
+
+ private long _buildMilliseconds;
+
+ private Dictionary<string, byte> _cpaotManagedSequentialResults;
+
+ private Dictionary<string, byte> _crossgenManagedSequentialResults;
+
+ private Dictionary<string, byte> _cpaotRequiresMarshalingResults;
+
+ private Dictionary<string, byte> _crossgenRequiresMarshalingResults;
+
+ public BuildFolderSet(
+ IEnumerable<BuildFolder> buildFolders,
+ IEnumerable<CompilerRunner> compilerRunners,
+ BuildOptions options)
+ {
+ _buildFolders = buildFolders;
+ _compilerRunners = compilerRunners;
+ _options = options;
+
+ _frameworkCompilationFailureBuckets = new Buckets();
+ _compilationFailureBuckets = new Buckets();
+ _executionFailureBuckets = new Buckets();
+
+ _cpaotManagedSequentialResults = new Dictionary<string, byte>();
+ _crossgenManagedSequentialResults = new Dictionary<string, byte>();
+
+ _cpaotRequiresMarshalingResults = new Dictionary<string, byte>();
+ _crossgenRequiresMarshalingResults = new Dictionary<string, byte>();
+
+ }
+
+ private void WriteJittedMethodSummary(StreamWriter logWriter)
+ {
+ Dictionary<string, HashSet<string>>[] allMethodsPerModulePerCompiler = new Dictionary<string, HashSet<string>>[(int)CompilerIndex.Count];
+
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ allMethodsPerModulePerCompiler[(int)runner.Index] = new Dictionary<string, HashSet<string>>();
+ }
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ for (int exeIndex = 0; exeIndex < folder.Executions.Count; exeIndex++)
+ {
+ Dictionary<string, HashSet<string>>[] appMethodsPerModulePerCompiler = new Dictionary<string, HashSet<string>>[(int)CompilerIndex.Count];
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ appMethodsPerModulePerCompiler[(int)runner.Index] = new Dictionary<string, HashSet<string>>();
+ folder.AddModuleToJittedMethodsMapping(allMethodsPerModulePerCompiler[(int)runner.Index], exeIndex, runner.Index);
+ folder.AddModuleToJittedMethodsMapping(appMethodsPerModulePerCompiler[(int)runner.Index], exeIndex, runner.Index);
+ }
+ folder.WriteJitStatistics(appMethodsPerModulePerCompiler, _compilerRunners);
+ }
+ }
+
+ BuildFolder.WriteJitStatistics(logWriter, allMethodsPerModulePerCompiler, _compilerRunners);
+ }
+
+ public bool Compile()
+ {
+ CompileFramework();
+
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ ResolveTestExclusions();
+
+ List<ProcessInfo> compilationsToRun = new List<ProcessInfo>();
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo compilationProcess = compilation[(int)runner.Index];
+ if (compilationProcess != null)
+ {
+ compilationsToRun.Add(compilationProcess);
+ }
+ }
+ }
+ }
+
+ ParallelRunner.Run(compilationsToRun, _options.DegreeOfParallelism);
+
+ bool success = true;
+ List<KeyValuePair<string, string>> failedCompilationsPerBuilder = new List<KeyValuePair<string, string>>();
+ int successfulCompileCount = 0;
+
+ List<ProcessInfo> r2rDumpExecutionsToRun = new List<ProcessInfo>();
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ string file = null;
+ string failedBuilders = null;
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo runnerProcess = compilation[(int)runner.Index];
+ if (runnerProcess == null)
+ {
+ // No runner process
+ }
+ else if (runnerProcess.Succeeded)
+ {
+ AnalyzeCompilationLog(runnerProcess, runner.Index);
+ if (_options.R2RDumpPath != null)
+ {
+ r2rDumpExecutionsToRun.Add(new ProcessInfo(new R2RDumpProcessConstructor(runner, runnerProcess.Parameters.OutputFileName, naked: false)));
+ r2rDumpExecutionsToRun.Add(new ProcessInfo(new R2RDumpProcessConstructor(runner, runnerProcess.Parameters.OutputFileName, naked: true)));
+ }
+ }
+ else // runner process failed
+ {
+ _compilationFailureBuckets.AddCompilation(runnerProcess);
+ try
+ {
+ File.Copy(runnerProcess.Parameters.InputFileName, runnerProcess.Parameters.OutputFileName);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("Error copying {0} to {1}: {2}", runnerProcess.Parameters.InputFileName, runnerProcess.Parameters.OutputFileName, ex.Message);
+ }
+ if (file == null)
+ {
+ file = runnerProcess.Parameters.InputFileName;
+ failedBuilders = runner.CompilerName;
+ }
+ else
+ {
+ failedBuilders += "; " + runner.CompilerName;
+ }
+ }
+ }
+ if (file != null)
+ {
+ failedCompilationsPerBuilder.Add(new KeyValuePair<string, string>(file, failedBuilders));
+ success = false;
+ }
+ else
+ {
+ successfulCompileCount++;
+ }
+ }
+ }
+
+ ParallelRunner.Run(r2rDumpExecutionsToRun, _options.DegreeOfParallelism);
+
+ foreach (ProcessInfo r2rDumpExecution in r2rDumpExecutionsToRun)
+ {
+ if (!r2rDumpExecution.Succeeded)
+ {
+ string causeOfFailure;
+ if (r2rDumpExecution.TimedOut)
+ {
+ causeOfFailure = "timed out";
+ }
+ else if (r2rDumpExecution.ExitCode != 0)
+ {
+ causeOfFailure = $"invalid exit code {r2rDumpExecution.ExitCode}";
+ }
+ else
+ {
+ causeOfFailure = "Unknown cause of failure";
+ }
+
+ Console.Error.WriteLine("Error running R2R dump on {0}: {1}", r2rDumpExecution.Parameters.InputFileName, causeOfFailure);
+ success = false;
+ }
+ }
+
+ _compilationMilliseconds = stopwatch.ElapsedMilliseconds;
+
+ return success;
+ }
+
+ public bool CompileFramework()
+ {
+ if (!_options.Framework)
+ {
+ return true;
+ }
+
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ string coreRoot = _options.CoreRootDirectory.FullName;
+ string[] frameworkFolderFiles = Directory.GetFiles(coreRoot);
+
+ IEnumerable<CompilerRunner> frameworkRunners = _options.CompilerRunners(isFramework: true);
+
+ // Pre-populate the output folders with the input files so that we have backdrops
+ // for failing compilations.
+ foreach (CompilerRunner runner in frameworkRunners)
+ {
+ string outputPath = runner.GetOutputPath(coreRoot);
+ outputPath.RecreateDirectory();
+ }
+
+ List<ProcessInfo> compilationsToRun = new List<ProcessInfo>();
+ List<KeyValuePair<string, ProcessInfo[]>> compilationsPerRunner = new List<KeyValuePair<string, ProcessInfo[]>>();
+ foreach (string frameworkDll in ComputeManagedAssemblies.GetManagedAssembliesInFolder(_options.CoreRootDirectory.FullName))
+ {
+ ProcessInfo[] processes = new ProcessInfo[(int)CompilerIndex.Count];
+ compilationsPerRunner.Add(new KeyValuePair<string, ProcessInfo[]>(frameworkDll, processes));
+ foreach (CompilerRunner runner in frameworkRunners)
+ {
+ ProcessInfo compilationProcess = new ProcessInfo(new CompilationProcessConstructor(runner, _options.CoreRootDirectory.FullName, frameworkDll));
+ compilationsToRun.Add(compilationProcess);
+ processes[(int)runner.Index] = compilationProcess;
+ }
+ }
+
+ ParallelRunner.Run(compilationsToRun, _options.DegreeOfParallelism);
+
+ HashSet<string> skipCopying = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ int[] failedCompilationsPerBuilder = new int[(int)CompilerIndex.Count];
+ int successfulCompileCount = 0;
+ int failedCompileCount = 0;
+ foreach (KeyValuePair<string, ProcessInfo[]> kvp in compilationsPerRunner)
+ {
+ bool anyCompilationsFailed = false;
+ foreach (CompilerRunner runner in frameworkRunners)
+ {
+ ProcessInfo compilationProcess = kvp.Value[(int)runner.Index];
+ if (compilationProcess.Succeeded)
+ {
+ skipCopying.Add(compilationProcess.Parameters.InputFileName);
+ AnalyzeCompilationLog(compilationProcess, runner.Index);
+ }
+ else
+ {
+ anyCompilationsFailed = true;
+ failedCompilationsPerBuilder[(int)runner.Index]++;
+ _frameworkCompilationFailureBuckets.AddCompilation(compilationProcess);
+ }
+ }
+ if (anyCompilationsFailed)
+ {
+ failedCompileCount++;
+ }
+ else
+ {
+ successfulCompileCount++;
+ }
+ }
+
+ foreach (CompilerRunner runner in frameworkRunners)
+ {
+ string outputPath = runner.GetOutputPath(coreRoot);
+ foreach (string file in frameworkFolderFiles)
+ {
+ if (!skipCopying.Contains(file))
+ {
+ string targetFile = Path.Combine(outputPath, Path.GetFileName(file));
+ File.Copy(file, targetFile, overwrite: true);
+ }
+ }
+ }
+
+ _frameworkCompilationMilliseconds = stopwatch.ElapsedMilliseconds;
+
+ return failedCompileCount == 0;
+ }
+
+ private void AnalyzeCompilationLog(ProcessInfo compilationProcess, CompilerIndex runnerIndex)
+ {
+ Dictionary<string, byte> managedSequentialTarget;
+ Dictionary<string, byte> requiresMarshalingTarget;
+
+ switch (runnerIndex)
+ {
+ case CompilerIndex.CPAOT:
+ managedSequentialTarget = _cpaotManagedSequentialResults;
+ requiresMarshalingTarget = _cpaotRequiresMarshalingResults;
+ break;
+
+ case CompilerIndex.Crossgen:
+ managedSequentialTarget = _crossgenManagedSequentialResults;
+ requiresMarshalingTarget = _crossgenRequiresMarshalingResults;
+ break;
+
+ default:
+ return;
+ }
+
+ try
+ {
+ const string ManagedSequentialStartMarker = "[[[IsManagedSequential{";
+ const string RequiresMarshalingStartMarker = "[[[MethodRequiresMarshaling{";
+
+ foreach (string line in File.ReadAllLines(compilationProcess.Parameters.LogPath))
+ {
+ AnalyzeMarker(line, ManagedSequentialStartMarker, managedSequentialTarget);
+ AnalyzeMarker(line, RequiresMarshalingStartMarker, requiresMarshalingTarget);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("Error reading log file {0}: {1}", compilationProcess.Parameters.LogPath, ex.Message);
+ }
+ }
+
+ private void AnalyzeMarker(string line, string marker, Dictionary<string, byte> target)
+ {
+ const string FalseEndMarker = "}=False]]]";
+ const string TrueEndMarker = "}=True]]]";
+ const string MultiEndMarker = "}=Multi]]]";
+
+ int startIndex = line.IndexOf(marker);
+ if (startIndex >= 0)
+ {
+ startIndex += marker.Length;
+ int falseEndIndex = line.IndexOf(FalseEndMarker, startIndex);
+ int trueEndIndex = falseEndIndex >= 0 ? falseEndIndex : line.IndexOf(TrueEndMarker, startIndex);
+ int multiEndIndex = trueEndIndex >= 0 ? trueEndIndex : line.IndexOf(MultiEndMarker, startIndex);
+ byte result;
+ if (falseEndIndex >= 0)
+ {
+ result = 0;
+ }
+ else if (trueEndIndex >= 0)
+ {
+ result = 1;
+ }
+ else if (multiEndIndex >= 0)
+ {
+ result = 2;
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ string typeName = line.Substring(startIndex, multiEndIndex - startIndex);
+
+ byte previousValue;
+ if (target.TryGetValue(typeName, out previousValue) && previousValue != result)
+ {
+ result = 2;
+ }
+ target[typeName] = result;
+ }
+ }
+
+ public bool Execute()
+ {
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+ List<ProcessInfo> executionsToRun = new List<ProcessInfo>();
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ AddBuildFolderExecutions(executionsToRun, folder, stopwatch);
+ }
+
+ ParallelRunner.Run(executionsToRun, degreeOfParallelism: _options.Sequential ? 1 : 0);
+
+ int successfulExecuteCount = 0;
+
+ bool success = true;
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ foreach (ProcessInfo[] execution in folder.Executions)
+ {
+ string file = null;
+ string failedBuilders = null;
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo runnerProcess = execution[(int)runner.Index];
+ if (runnerProcess != null && !runnerProcess.Succeeded)
+ {
+ _executionFailureBuckets.AddExecution(runnerProcess);
+
+ if (file == null)
+ {
+ file = runnerProcess.Parameters.InputFileName;
+ failedBuilders = runner.CompilerName;
+ }
+ else
+ {
+ failedBuilders += "; " + runner.CompilerName;
+ }
+ }
+ }
+ if (file != null)
+ {
+ success = false;
+ }
+ else
+ {
+ successfulExecuteCount++;
+ }
+ }
+ }
+
+ _executionMilliseconds = stopwatch.ElapsedMilliseconds;
+
+ return success;
+ }
+
+ public bool Build(IEnumerable<CompilerRunner> runners)
+ {
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ bool success = Compile();
+
+ if (!_options.NoExe)
+ {
+ success = Execute() && success;
+ }
+
+ _buildMilliseconds = stopwatch.ElapsedMilliseconds;
+
+ return success;
+ }
+
+ private void ResolveTestExclusions()
+ {
+ TestExclusionMap exclusions = TestExclusionMap.Create(_options);
+ foreach (BuildFolder folder in _buildFolders)
+ {
+ if (exclusions.TryGetIssue(folder.InputFolder, out string issueID))
+ {
+ folder.IssueID = issueID;
+ continue;
+ }
+ }
+ }
+
+ private void AddBuildFolderExecutions(List<ProcessInfo> executionsToRun, BuildFolder folder, Stopwatch stopwatch)
+ {
+ foreach (ProcessInfo[] execution in folder.Executions)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo executionProcess = execution[(int)runner.Index];
+ if (executionProcess != null)
+ {
+ bool compilationsSucceeded = folder.Compilations.All(comp => comp[(int)runner.Index]?.Succeeded ?? true);
+ if (compilationsSucceeded)
+ {
+ executionsToRun.Add(executionProcess);
+ }
+ else
+ {
+ // Forget the execution process when compilation failed
+ execution[(int)runner.Index] = null;
+ }
+ }
+ }
+ }
+ }
+
+ private void WriteTopRankingProcesses(StreamWriter logWriter, string metric, IEnumerable<ProcessInfo> processes)
+ {
+ const int TopAppCount = 10;
+
+ IEnumerable<ProcessInfo> selection = processes.OrderByDescending(process => process.DurationMilliseconds).Take(TopAppCount);
+ int count = selection.Count();
+ if (count == 0)
+ {
+ // No entries to log
+ return;
+ }
+
+ logWriter.WriteLine();
+
+ string headerLine = $"{count} top ranking {metric}";
+ logWriter.WriteLine(headerLine);
+ logWriter.WriteLine(new string('-', headerLine.Length));
+
+ foreach (ProcessInfo processInfo in selection)
+ {
+ logWriter.WriteLine($"{processInfo.DurationMilliseconds,10} | {processInfo.Parameters.InputFileName}");
+ }
+ }
+
+ enum CompilationOutcome
+ {
+ PASS = 0,
+ FAIL = 1,
+
+ Count
+ }
+
+ private enum ExecutionOutcome
+ {
+ PASS = 0,
+ EXIT_CODE = 1,
+ CRASHED = 2,
+ TIMED_OUT = 3,
+
+ Count
+ }
+
+ private CompilationOutcome GetCompilationOutcome(ProcessInfo compilation)
+ {
+ return compilation.Succeeded ? CompilationOutcome.PASS : CompilationOutcome.FAIL;
+ }
+
+ private ExecutionOutcome GetExecutionOutcome(ProcessInfo execution)
+ {
+ if (execution.TimedOut)
+ {
+ return ExecutionOutcome.TIMED_OUT;
+ }
+ if (execution.Crashed)
+ {
+ return ExecutionOutcome.CRASHED;
+ }
+ return (execution.Succeeded ? ExecutionOutcome.PASS : ExecutionOutcome.EXIT_CODE);
+ }
+
+ private void WriteBuildStatistics(StreamWriter logWriter)
+ {
+ // The Count'th element corresponds to totals over all compiler runners used in the run
+ int[,] compilationOutcomes = new int[(int)CompilationOutcome.Count, (int)CompilerIndex.Count + 1];
+ int[,] executionOutcomes = new int[(int)ExecutionOutcome.Count, (int)CompilerIndex.Count + 1];
+ int totalCompilations = 0;
+ int totalExecutions = 0;
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ bool[] compilationFailedPerRunner = new bool[(int)CompilerIndex.Count];
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ totalCompilations++;
+ bool anyCompilationFailed = false;
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ if (compilation[(int)runner.Index] != null)
+ {
+ CompilationOutcome outcome = GetCompilationOutcome(compilation[(int)runner.Index]);
+ compilationOutcomes[(int)outcome, (int)runner.Index]++;
+ if (outcome != CompilationOutcome.PASS)
+ {
+ anyCompilationFailed = true;
+ compilationFailedPerRunner[(int)runner.Index] = true;
+ }
+ }
+ }
+ if (anyCompilationFailed)
+ {
+ compilationOutcomes[(int)CompilationOutcome.FAIL, (int)CompilerIndex.Count]++;
+ }
+ else
+ {
+ compilationOutcomes[(int)CompilationOutcome.PASS, (int)CompilerIndex.Count]++;
+ }
+ }
+
+ foreach (ProcessInfo[] execution in folder.Executions)
+ {
+ totalExecutions++;
+ bool anyCompilationFailed = false;
+ int executionFailureOutcomeMask = 0;
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo execProcess = execution[(int)runner.Index];
+ bool compilationFailed = compilationFailedPerRunner[(int)runner.Index];
+ anyCompilationFailed |= compilationFailed;
+ bool executionFailed = !compilationFailed && (execProcess != null && !execProcess.Succeeded);
+ if (executionFailed)
+ {
+ ExecutionOutcome outcome = GetExecutionOutcome(execProcess);
+ executionOutcomes[(int)outcome, (int)runner.Index]++;
+ executionFailureOutcomeMask |= 1 << (int)outcome;
+ }
+ if (!compilationFailed && !executionFailed)
+ {
+ executionOutcomes[(int)ExecutionOutcome.PASS, (int)runner.Index]++;
+ }
+ }
+ if (executionFailureOutcomeMask != 0)
+ {
+ for (int outcomeIndex = 0; outcomeIndex < (int)ExecutionOutcome.Count; outcomeIndex++)
+ {
+ if ((executionFailureOutcomeMask & (1 << outcomeIndex)) != 0)
+ {
+ executionOutcomes[outcomeIndex, (int)CompilerIndex.Count]++;
+ }
+ }
+ }
+ else
+ {
+ executionOutcomes[(int)ExecutionOutcome.PASS, (int)CompilerIndex.Count]++;
+ }
+ }
+ }
+
+ logWriter.WriteLine();
+ logWriter.WriteLine($"Configuration: {(_options.Release ? "Release" : "Debug")}");
+ logWriter.WriteLine($"Framework: {(_options.Framework ? "build native" : _options.UseFramework ? "prebuilt native" : "MSIL")}");
+ logWriter.WriteLine($"Version bubble: {(_options.LargeBubble ? "input + all reference assemblies" : "single assembly")}");
+ logWriter.WriteLine($"Input folder: {_options.InputDirectory?.FullName}");
+ logWriter.WriteLine($"CORE_ROOT: {_options.CoreRootDirectory?.FullName}");
+ logWriter.WriteLine($"CPAOT: {_options.CpaotDirectory?.FullName}");
+ logWriter.WriteLine($"Total folders: {_buildFolders.Count()}");
+ logWriter.WriteLine($"Blocked w/issues: {_buildFolders.Count(folder => folder.IsBlockedWithIssue)}");
+ int foldersToBuild = FoldersToBuild.Count();
+ logWriter.WriteLine($"Folders to build: {foldersToBuild}");
+ logWriter.WriteLine($"# compilations: {totalCompilations}");
+ logWriter.WriteLine($"# executions: {totalExecutions}");
+ logWriter.WriteLine($"Total build time: {_buildMilliseconds} msecs");
+ logWriter.WriteLine($"Framework time: {_frameworkCompilationMilliseconds} msecs");
+ logWriter.WriteLine($"Compilation time: {_compilationMilliseconds} msecs");
+ logWriter.WriteLine($"Execution time: {_executionMilliseconds} msecs");
+
+ if (foldersToBuild != 0)
+ {
+ logWriter.WriteLine();
+ logWriter.Write($"{totalCompilations,7} ILC |");
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ logWriter.Write($"{runner.CompilerName,8} |");
+ }
+ logWriter.WriteLine(" Overall");
+ int lineSize = 10 * _compilerRunners.Count() + 13 + 8;
+ string separator = new string('-', lineSize);
+ logWriter.WriteLine(separator);
+ for (int outcomeIndex = 0; outcomeIndex < (int)CompilationOutcome.Count; outcomeIndex++)
+ {
+ logWriter.Write($"{((CompilationOutcome)outcomeIndex).ToString(),11} |");
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ logWriter.Write($"{compilationOutcomes[outcomeIndex, (int)runner.Index],8} |");
+ }
+ logWriter.WriteLine($"{compilationOutcomes[outcomeIndex, (int)CompilerIndex.Count],8}");
+ }
+
+ if (!_options.NoExe)
+ {
+ logWriter.WriteLine();
+ logWriter.Write($"{totalExecutions,7} EXE |");
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ logWriter.Write($"{runner.CompilerName,8} |");
+ }
+ logWriter.WriteLine(" Overall");
+ logWriter.WriteLine(separator);
+ for (int outcomeIndex = 0; outcomeIndex < (int)ExecutionOutcome.Count; outcomeIndex++)
+ {
+ logWriter.Write($"{((ExecutionOutcome)outcomeIndex).ToString(),11} |");
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ logWriter.Write($"{executionOutcomes[outcomeIndex, (int)runner.Index],8} |");
+ }
+ logWriter.WriteLine($"{executionOutcomes[outcomeIndex, (int)CompilerIndex.Count],8}");
+ }
+ }
+
+ WritePerFolderStatistics(logWriter);
+
+ WriteExecutableSizeStatistics(logWriter);
+
+ WriteJittedMethodSummary(logWriter);
+
+ WriteTopRankingProcesses(logWriter, "compilations by duration", EnumerateCompilations());
+ WriteTopRankingProcesses(logWriter, "executions by duration", EnumerateExecutions());
+ }
+
+ if (_options.Framework)
+ {
+ logWriter.WriteLine();
+ logWriter.WriteLine("Framework compilation failures:");
+ FrameworkCompilationFailureBuckets.WriteToStream(logWriter, detailed: false);
+ }
+
+ if (foldersToBuild != 0)
+ {
+ logWriter.WriteLine();
+ logWriter.WriteLine("Compilation failures:");
+ CompilationFailureBuckets.WriteToStream(logWriter, detailed: false);
+
+ if (!_options.NoExe)
+ {
+ logWriter.WriteLine();
+ logWriter.WriteLine("Execution failures:");
+ ExecutionFailureBuckets.WriteToStream(logWriter, detailed: false);
+ }
+ }
+
+ WriteFoldersBlockedWithIssues(logWriter);
+ }
+
+ private void WritePerFolderStatistics(StreamWriter logWriter)
+ {
+ string baseFolder = _options.InputDirectory.FullName;
+ int baseOffset = baseFolder.Length + (baseFolder.Length > 0 && baseFolder[baseFolder.Length - 1] == Path.DirectorySeparatorChar ? 0 : 1);
+ HashSet<string> folders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ string relativeFolder = "";
+ if (folder.InputFolder.Length > baseFolder.Length)
+ {
+ relativeFolder = folder.InputFolder.Substring(baseOffset);
+ }
+ int endPos = relativeFolder.IndexOf(Path.DirectorySeparatorChar);
+ if (endPos < 0)
+ {
+ endPos = relativeFolder.Length;
+ }
+ folders.Add(relativeFolder.Substring(0, endPos));
+ }
+ if (folders.Count <= 1)
+ {
+ // Just one folder - no per folder statistics needed
+ return;
+ }
+
+ List<string> folderList = new List<string>(folders);
+ folderList.Sort(StringComparer.OrdinalIgnoreCase);
+ logWriter.WriteLine();
+ logWriter.WriteLine("Folder statistics:");
+ logWriter.WriteLine("#ILC | PASS | FAIL | #EXE | PASS | FAIL | PATH");
+ logWriter.WriteLine("----------------------------------------------");
+
+ foreach (string relativeFolder in folderList)
+ {
+ string folder = Path.Combine(baseFolder, relativeFolder);
+ int ilcCount = 0;
+ int exeCount = 0;
+ int exeFail = 0;
+ int ilcFail = 0;
+ foreach (BuildFolder buildFolder in FoldersToBuild)
+ {
+ string buildFolderPath = buildFolder.InputFolder;
+ if (buildFolderPath.Equals(folder, StringComparison.OrdinalIgnoreCase) ||
+ buildFolderPath.StartsWith(folder, StringComparison.OrdinalIgnoreCase) &&
+ buildFolderPath[folder.Length] == Path.DirectorySeparatorChar)
+ {
+ foreach (ProcessInfo[] compilation in buildFolder.Compilations)
+ {
+ bool anyIlcFail = false;
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ if (compilation[(int)runner.Index] != null && !compilation[(int)runner.Index].Succeeded)
+ {
+ anyIlcFail = true;
+ break;
+ }
+ }
+ ilcCount++;
+ if (anyIlcFail)
+ {
+ ilcFail++;
+ }
+ }
+ foreach (ProcessInfo[] execution in buildFolder.Executions)
+ {
+ bool anyExeFail = false;
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ if (execution[(int)runner.Index] != null && !execution[(int)runner.Index].Succeeded)
+ {
+ anyExeFail = true;
+ break;
+ }
+ }
+ exeCount++;
+ if (anyExeFail)
+ {
+ exeFail++;
+ }
+ }
+ }
+ }
+ logWriter.WriteLine($"{ilcCount,4} | {(ilcCount - ilcFail),4} | {ilcFail,4} | {exeCount,4} | {(exeCount - exeFail),4} | {exeFail,4} | {relativeFolder}");
+ }
+ }
+
+ class ExeSizeInfo
+ {
+ public readonly string CpaotPath;
+ public readonly long CpaotSize;
+ public readonly string CrossgenPath;
+ public readonly long CrossgenSize;
+
+ public ExeSizeInfo(string cpaotPath, long cpaotSize, string crossgenPath, long crossgenSize)
+ {
+ CpaotPath = cpaotPath;
+ CpaotSize = cpaotSize;
+ CrossgenPath = crossgenPath;
+ CrossgenSize = crossgenSize;
+ }
+ }
+
+ private void WriteExecutableSizeStatistics(StreamWriter logWriter)
+ {
+ List<ExeSizeInfo> sizeStats = new List<ExeSizeInfo>();
+ HashSet<string> libraryHashes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ ProcessInfo crossgenCompilation = compilation[(int)CompilerIndex.Crossgen];
+ ProcessInfo cpaotCompilation = compilation[(int)CompilerIndex.CPAOT];
+ if ((crossgenCompilation?.Succeeded ?? false) &&
+ (cpaotCompilation?.Succeeded ?? false))
+ {
+ long cpaotSize;
+ try
+ {
+ cpaotSize = new FileInfo(cpaotCompilation.Parameters.OutputFileName).Length;
+ }
+ catch (Exception)
+ {
+ Console.Error.WriteLine("Cannot find CPAOT output file '{0}', ignoring in size stats", cpaotCompilation.Parameters.OutputFileName);
+ continue;
+ }
+
+ long crossgenSize;
+ try
+ {
+ crossgenSize = new FileInfo(crossgenCompilation.Parameters.OutputFileName).Length;
+ }
+ catch (Exception)
+ {
+ Console.Error.WriteLine("Cannot find Crossgen output file '{0}', ignoring in size stats", crossgenCompilation.Parameters.OutputFileName);
+ continue;
+ }
+
+ string ext = Path.GetExtension(cpaotCompilation.Parameters.OutputFileName).ToLower();
+ if (ext == ".dll" || ext == ".so")
+ {
+ string hash = $"{Path.GetFileName(cpaotCompilation.Parameters.OutputFileName)}#{cpaotSize}#{crossgenSize}";
+ if (!libraryHashes.Add(hash))
+ {
+ // We ignore libraries with the same "simple name" if it has the same compiled size as many tests
+ // use support libraries that get separately compiled into their respective folders but semantically
+ // are "the same thing" so it doesn't make too much sense to report them multiple times.
+ continue;
+ }
+ }
+
+ sizeStats.Add(new ExeSizeInfo(
+ cpaotPath: cpaotCompilation.Parameters.OutputFileName,
+ cpaotSize: cpaotSize,
+ crossgenPath: crossgenCompilation.Parameters.OutputFileName,
+ crossgenSize: crossgenSize));
+
+ }
+ }
+ }
+
+ if (sizeStats.Count == 0)
+ {
+ return;
+ }
+
+ long totalCpaotSize = sizeStats.Sum((stat) => stat.CpaotSize);
+ long totalCrossgenSize = sizeStats.Sum((stat) => stat.CrossgenSize);
+
+ const double MegaByte = 1024 * 1024;
+ double KiloCount = 1024 * sizeStats.Count;
+
+ logWriter.WriteLine();
+ logWriter.WriteLine("Executable size statistics:");
+ logWriter.WriteLine("Total CPAOT size: {0:F3} MB ({1:F3} KB per app on average)", totalCpaotSize / MegaByte, totalCpaotSize / KiloCount);
+ logWriter.WriteLine("Total Crossgen size: {0:F3} MB ({1:F3} KB per app on average)", totalCrossgenSize / MegaByte, totalCrossgenSize / KiloCount);
+
+ long deltaSize = totalCpaotSize - totalCrossgenSize;
+ logWriter.WriteLine("CPAOT - Crossgen: {0:F3} MB ({1:F3} KB per app on average)", deltaSize / MegaByte, deltaSize / KiloCount);
+
+ double percentageSizeRatio = totalCpaotSize * 100.0 / Math.Max(totalCrossgenSize, 1);
+ logWriter.WriteLine("CPAOT / Crossgen: {0:F3}%", percentageSizeRatio);
+
+ sizeStats.Sort((a, b) => (b.CpaotSize - b.CrossgenSize).CompareTo(a.CpaotSize - a.CrossgenSize));
+
+ const int TopExeCount = 10;
+
+ int topCount;
+ int bottomCount;
+
+ if (sizeStats.Count <= 2 * TopExeCount)
+ {
+ topCount = sizeStats.Count;
+ bottomCount = 0;
+ }
+ else
+ {
+ topCount = TopExeCount;
+ bottomCount = TopExeCount;
+ }
+
+ logWriter.WriteLine();
+ logWriter.WriteLine("CPAOT size | Crossgen | CPAOT - CG | Highest exe size deltas");
+ logWriter.WriteLine("--------------------------------------------------------------");
+ foreach (ExeSizeInfo exeSize in sizeStats.Take(topCount))
+ {
+ logWriter.WriteLine(
+ "{0,10} | {1,10} | {2,10} | {3}",
+ exeSize.CpaotSize,
+ exeSize.CrossgenSize,
+ exeSize.CpaotSize - exeSize.CrossgenSize,
+ exeSize.CpaotPath);
+ }
+
+ if (bottomCount > 0)
+ {
+ logWriter.WriteLine();
+ logWriter.WriteLine("CPAOT size | Crossgen | CPAOT - CG | Lowest exe size deltas");
+ logWriter.WriteLine("-------------------------------------------------------------");
+ foreach (ExeSizeInfo exeSize in sizeStats.TakeLast(bottomCount))
+ {
+ logWriter.WriteLine(
+ "{0,10} | {1,10} | {2,10} | {3}",
+ exeSize.CpaotSize,
+ exeSize.CrossgenSize,
+ exeSize.CpaotSize - exeSize.CrossgenSize,
+ exeSize.CpaotPath);
+ }
+ }
+
+ sizeStats.Sort((a, b) => (b.CpaotSize * a.CrossgenSize).CompareTo(a.CpaotSize * b.CrossgenSize));
+
+ logWriter.WriteLine();
+ logWriter.WriteLine("CPAOT size | Crossgen | CPAOT/CG % | Highest exe size ratios");
+ logWriter.WriteLine("--------------------------------------------------------------");
+ foreach (ExeSizeInfo exeSize in sizeStats.Take(topCount))
+ {
+ logWriter.WriteLine(
+ "{0,10} | {1,10} | {2,10:F3} | {3}",
+ exeSize.CpaotSize,
+ exeSize.CrossgenSize,
+ exeSize.CpaotSize * 100.0 / exeSize.CrossgenSize,
+ exeSize.CpaotPath);
+ }
+
+ if (bottomCount > 0)
+ {
+ logWriter.WriteLine();
+ logWriter.WriteLine("CPAOT size | Crossgen | CPAOT/CG % | Lowest exe size ratios");
+ logWriter.WriteLine("-------------------------------------------------------------");
+ foreach (ExeSizeInfo exeSize in sizeStats.TakeLast(bottomCount))
+ {
+ logWriter.WriteLine(
+ "{0,10} | {1,10} | {2,10:F6} | {3}",
+ exeSize.CpaotSize,
+ exeSize.CrossgenSize,
+ exeSize.CpaotSize * 100.0 / exeSize.CrossgenSize,
+ exeSize.CpaotPath);
+ }
+ }
+ }
+
+ private IEnumerable<ProcessInfo> EnumerateCompilations()
+ {
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo compilationProcess = compilation[(int)runner.Index];
+ if (compilationProcess != null)
+ {
+ yield return compilationProcess;
+ }
+ }
+ }
+ }
+ }
+
+ private IEnumerable<ProcessInfo> EnumerateExecutions()
+ {
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ foreach (ProcessInfo[] execution in folder.Executions)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo executionProcess = execution[(int)runner.Index];
+ if (executionProcess != null)
+ {
+ yield return executionProcess;
+ }
+ }
+ }
+ }
+ }
+
+ public void WriteBuildLog(string buildLogPath)
+ {
+ using (StreamWriter buildLogWriter = new StreamWriter(buildLogPath))
+ {
+ WriteBuildStatistics(buildLogWriter);
+ }
+ }
+
+ public void WriteCombinedLog(string outputFile)
+ {
+ using (StreamWriter combinedLog = new StreamWriter(outputFile))
+ {
+ StreamWriter[] perRunnerLog = new StreamWriter[(int)CompilerIndex.Count];
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ string runnerLogPath = Path.ChangeExtension(outputFile, "-" + runner.CompilerName + ".log");
+ perRunnerLog[(int)runner.Index] = new StreamWriter(runnerLogPath);
+ }
+
+ foreach (BuildFolder folder in FoldersToBuild)
+ {
+ bool[] compilationErrorPerRunner = new bool[(int)CompilerIndex.Count];
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo compilationProcess = compilation[(int)runner.Index];
+ if (compilationProcess != null)
+ {
+ string log = $"\nCOMPILE {runner.CompilerName}:{compilationProcess.Parameters.InputFileName}";
+ StreamWriter runnerLog = perRunnerLog[(int)runner.Index];
+ runnerLog.WriteLine(log);
+ combinedLog.WriteLine(log);
+ try
+ {
+ using (Stream input = new FileStream(compilationProcess.Parameters.LogPath, FileMode.Open, FileAccess.Read))
+ {
+ input.CopyTo(combinedLog.BaseStream);
+ input.Seek(0, SeekOrigin.Begin);
+ input.CopyTo(runnerLog.BaseStream);
+ }
+ }
+ catch (Exception ex)
+ {
+ combinedLog.WriteLine(" -> " + ex.Message);
+ runnerLog.WriteLine(" -> " + ex.Message);
+ }
+
+ if (!compilationProcess.Succeeded)
+ {
+ compilationErrorPerRunner[(int)runner.Index] = true;
+ }
+ }
+ }
+ }
+ foreach (ProcessInfo[] execution in folder.Executions)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ if (!compilationErrorPerRunner[(int)runner.Index])
+ {
+ StreamWriter runnerLog = perRunnerLog[(int)runner.Index];
+ ProcessInfo executionProcess = execution[(int)runner.Index];
+ if (executionProcess != null)
+ {
+ string header = $"\nEXECUTE {runner.CompilerName}:{executionProcess.Parameters.InputFileName}";
+ combinedLog.WriteLine(header);
+ runnerLog.WriteLine(header);
+ try
+ {
+ using (Stream input = new FileStream(executionProcess.Parameters.LogPath, FileMode.Open, FileAccess.Read))
+ {
+ input.CopyTo(combinedLog.BaseStream);
+ input.Seek(0, SeekOrigin.Begin);
+ input.CopyTo(runnerLog.BaseStream);
+ }
+ }
+ catch (Exception ex)
+ {
+ combinedLog.WriteLine(" -> " + ex.Message);
+ runnerLog.WriteLine(" -> " + ex.Message);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ perRunnerLog[(int)runner.Index].Dispose();
+ }
+ }
+ }
+
+ private void WriteFoldersBlockedWithIssues(StreamWriter logWriter)
+ {
+ IEnumerable<BuildFolder> blockedFolders = _buildFolders.Where(folder => folder.IsBlockedWithIssue);
+
+ int blockedCount = blockedFolders.Count();
+
+ logWriter.WriteLine();
+ logWriter.WriteLine($"Folders blocked with issues ({blockedCount} total):");
+ logWriter.WriteLine("ISSUE | TEST");
+ logWriter.WriteLine("------------");
+ foreach (BuildFolder folder in blockedFolders)
+ {
+ logWriter.WriteLine($"{folder.IssueID,5} | {folder.InputFolder}");
+ }
+ }
+
+ public void WriteLogs()
+ {
+ string timestamp = DateTime.Now.ToString("MMdd-HHmm");
+
+ string suffix = (_options.Release ? "ret-" : "chk-") + timestamp + ".log";
+
+ string buildLogPath = Path.Combine(_options.OutputDirectory.FullName, "build-" + suffix);
+ WriteBuildLog(buildLogPath);
+
+ string combinedSetLogPath = Path.Combine(_options.OutputDirectory.FullName, "combined-" + suffix);
+ WriteCombinedLog(combinedSetLogPath);
+
+ string frameworkBucketsFile = Path.Combine(_options.OutputDirectory.FullName, "framework-buckets-" + suffix);
+ FrameworkCompilationFailureBuckets.WriteToFile(frameworkBucketsFile, detailed: true);
+
+ string compilationBucketsFile = Path.Combine(_options.OutputDirectory.FullName, "compilation-buckets-" + suffix);
+ CompilationFailureBuckets.WriteToFile(compilationBucketsFile, detailed: true);
+
+ string executionBucketsFile = Path.Combine(_options.OutputDirectory.FullName, "execution-buckets-" + suffix);
+ ExecutionFailureBuckets.WriteToFile(executionBucketsFile, detailed: true);
+
+ string compilationPassedFile = Path.Combine(_options.OutputDirectory.FullName, "compilation-passed-" + suffix);
+ WriteFileListPerCompilationOutcome(compilationPassedFile, CompilationOutcome.PASS);
+
+ string compilationFailedFile = Path.Combine(_options.OutputDirectory.FullName, "compilation-failed-" + suffix);
+ WriteFileListPerCompilationOutcome(compilationFailedFile, CompilationOutcome.FAIL);
+
+ string executionPassedFile = Path.Combine(_options.OutputDirectory.FullName, "execution-passed-" + suffix);
+ WriteFileListPerExecutionOutcome(executionPassedFile, ExecutionOutcome.PASS);
+
+ string executionTimedOutFile = Path.Combine(_options.OutputDirectory.FullName, "execution-timed-out-" + suffix);
+ WriteFileListPerExecutionOutcome(executionTimedOutFile, ExecutionOutcome.TIMED_OUT);
+
+ string executionCrashedFile = Path.Combine(_options.OutputDirectory.FullName, "execution-crashed-" + suffix);
+ WriteFileListPerExecutionOutcome(executionCrashedFile, ExecutionOutcome.CRASHED);
+
+ string executionExitCodeFile = Path.Combine(_options.OutputDirectory.FullName, "execution-exit-code-" + suffix);
+ WriteFileListPerExecutionOutcome(executionExitCodeFile, ExecutionOutcome.EXIT_CODE);
+
+ string cpaotManagedSequentialFile = Path.Combine(_options.OutputDirectory.FullName, "managed-sequential-cpaot-" + suffix);
+ WriterMarkerLog(cpaotManagedSequentialFile, _cpaotManagedSequentialResults);
+
+ string cpaotRequiresMarshalingFile = Path.Combine(_options.OutputDirectory.FullName, "requires-marshaling-cpaot-" + suffix);
+ WriterMarkerLog(cpaotRequiresMarshalingFile, _cpaotRequiresMarshalingResults);
+
+ if (_options.Crossgen)
+ {
+ string crossgenManagedSequentialFile = Path.Combine(_options.OutputDirectory.FullName, "managed-sequential-crossgen-" + suffix);
+ WriterMarkerLog(crossgenManagedSequentialFile, _crossgenManagedSequentialResults);
+
+ string crossgenRequiresMarshalingFile = Path.Combine(_options.OutputDirectory.FullName, "requires-marshaling-crossgen-" + suffix);
+ WriterMarkerLog(crossgenRequiresMarshalingFile, _crossgenRequiresMarshalingResults);
+
+ string managedSequentialDiffFile = Path.Combine(_options.OutputDirectory.FullName, "managed-sequential-diff-" + suffix);
+ WriterMarkerDiff(managedSequentialDiffFile, _cpaotManagedSequentialResults, _crossgenManagedSequentialResults);
+
+ string requiresMarshalingDiffFile = Path.Combine(_options.OutputDirectory.FullName, "requires-marshaling-diff-" + suffix);
+ WriterMarkerDiff(requiresMarshalingDiffFile, _cpaotRequiresMarshalingResults, _crossgenRequiresMarshalingResults);
+ }
+ }
+
+ private static void WriterMarkerLog(string fileName, Dictionary<string, byte> markerResults)
+ {
+ if (markerResults.Count == 0)
+ {
+ // Don't emit marker logs when the instrumentation is off
+ return;
+ }
+
+ using (StreamWriter logWriter = new StreamWriter(fileName))
+ {
+ foreach (KeyValuePair<string, byte> kvp in markerResults.OrderBy((kvp) => kvp.Key))
+ {
+ logWriter.WriteLine("{0}:{1}", kvp.Value, kvp.Key);
+ }
+ }
+ }
+
+ private static void WriterMarkerDiff(string fileName, Dictionary<string, byte> cpaot, Dictionary<string, byte> crossgen)
+ {
+ if (cpaot.Count == 0 && crossgen.Count == 0)
+ {
+ // Don't emit empty marker diffs just polluting the output folder
+ return;
+ }
+
+ using (StreamWriter logWriter = new StreamWriter(fileName))
+ {
+ int cpaotCount = cpaot.Count();
+ logWriter.WriteLine("Objects queried by CPAOT: {0}", cpaotCount);
+ logWriter.WriteLine("CPAOT conflicting results: {0}", cpaot.Count(kvp => kvp.Value == 2));
+ int crossgenCount = crossgen.Count();
+ logWriter.WriteLine("Objects queried by Crossgen: {0}", crossgenCount);
+ logWriter.WriteLine("Crossgen conflicting results: {0}", crossgen.Count(kvp => kvp.Value == 2));
+ int matchCount = cpaot.Count(kvp => crossgen.ContainsKey(kvp.Key) && crossgen[kvp.Key] == kvp.Value);
+ int bothCount = cpaot.Count(kvp => crossgen.ContainsKey(kvp.Key));
+ logWriter.WriteLine("Objects queried by both: {0}", bothCount);
+ logWriter.WriteLine("Matching results: {0} ({1:F3}%)", matchCount, matchCount * 100.0 / Math.Max(bothCount, 1));
+ logWriter.WriteLine("Mismatched results: {0}",
+ cpaot.Count(kvp => crossgen.ContainsKey(kvp.Key) && crossgen[kvp.Key] != kvp.Value));
+ logWriter.WriteLine("Objects not queried by Crossgen: {0}", cpaot.Count(kvp => !crossgen.ContainsKey(kvp.Key)));
+ logWriter.WriteLine("Objects not queried by CPAOT: {0}", crossgen.Count(kvp => !cpaot.ContainsKey(kvp.Key)));
+ logWriter.WriteLine();
+
+ WriterMarkerDiffSection(
+ logWriter,
+ "CPAOT = TRUE / CROSSGEN = FALSE",
+ cpaot
+ .Where(kvp => kvp.Value == 1 && crossgen.ContainsKey(kvp.Key) && crossgen[kvp.Key] == 0)
+ .Select(kvp => kvp.Key));
+
+ WriterMarkerDiffSection(
+ logWriter,
+ "CPAOT = FALSE / CROSSGEN = TRUE",
+ cpaot
+ .Where(kvp => kvp.Value == 0 && crossgen.ContainsKey(kvp.Key) && crossgen[kvp.Key] == 1)
+ .Select(kvp => kvp.Key));
+
+ WriterMarkerDiffSection(
+ logWriter,
+ "CROSSGEN - NO RESULT",
+ cpaot
+ .Where(kvp => !crossgen.ContainsKey(kvp.Key))
+ .Select(kvp => (kvp.Value.ToString() + ":" + kvp.Key)));
+
+ WriterMarkerDiffSection(
+ logWriter,
+ "CPAOT - NO RESULT",
+ crossgen
+ .Where(kvp => !cpaot.ContainsKey(kvp.Key))
+ .Select(kvp => (kvp.Value.ToString() + ":" + kvp.Key)));
+ }
+ }
+
+ private static void WriterMarkerDiffSection(StreamWriter logWriter, string title, IEnumerable<string> items)
+ {
+ bool first = true;
+ foreach (string item in items)
+ {
+ if (first)
+ {
+ logWriter.WriteLine();
+ logWriter.WriteLine(title);
+ logWriter.WriteLine(new string('-', title.Length));
+ first = false;
+ }
+ logWriter.WriteLine(item);
+ }
+ }
+
+ private void WriteFileListPerCompilationOutcome(string outputFileName, CompilationOutcome outcome)
+ {
+ List<string> filteredTestList = new List<string>();
+ foreach (BuildFolder folder in _buildFolders)
+ {
+ foreach (ProcessInfo[] compilation in folder.Compilations)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo compilationPerRunner = compilation[(int)runner.Index];
+ if (compilationPerRunner != null &&
+ GetCompilationOutcome(compilationPerRunner) == outcome &&
+ compilationPerRunner.Parameters != null)
+ {
+ filteredTestList.Add(compilationPerRunner.Parameters.OutputFileName);
+ }
+ }
+ }
+ }
+
+ filteredTestList.Sort(StringComparer.OrdinalIgnoreCase);
+ File.WriteAllLines(outputFileName, filteredTestList);
+ }
+
+ private void WriteFileListPerExecutionOutcome(string outputFileName, ExecutionOutcome outcome)
+ {
+ List<string> filteredTestList = new List<string>();
+ foreach (BuildFolder folder in _buildFolders)
+ {
+ foreach (ProcessInfo[] execution in folder.Executions)
+ {
+ foreach (CompilerRunner runner in _compilerRunners)
+ {
+ ProcessInfo executionPerRunner = execution[(int)runner.Index];
+ if (executionPerRunner != null &&
+ GetExecutionOutcome(executionPerRunner) == outcome &&
+ executionPerRunner.Parameters != null)
+ {
+ filteredTestList.Add(executionPerRunner.Parameters.InputFileName);
+ }
+ }
+ }
+ }
+
+ filteredTestList.Sort(StringComparer.OrdinalIgnoreCase);
+ File.WriteAllLines(outputFileName, filteredTestList);
+ }
+
+ public IEnumerable<BuildFolder> FoldersToBuild => _buildFolders.Where(folder => !folder.IsBlockedWithIssue);
+
+ public Buckets FrameworkCompilationFailureBuckets => _frameworkCompilationFailureBuckets;
+
+ public Buckets CompilationFailureBuckets => _compilationFailureBuckets;
+
+ public Buckets ExecutionFailureBuckets => _executionFailureBuckets;
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace ReadyToRun.SuperIlc
+{
+ public class BuildOptions
+ {
+ public DirectoryInfo InputDirectory { get; set; }
+ public DirectoryInfo OutputDirectory { get; set; }
+ public DirectoryInfo CoreRootDirectory { get; set; }
+ public DirectoryInfo CpaotDirectory { get; set; }
+ public bool Crossgen { get; set; }
+ public bool NoJit { get; set; }
+ public bool NoExe { get; set; }
+ public bool NoEtw { get; set; }
+ public bool NoCleanup { get; set; }
+ public FileInfo PackageList { get; set; }
+ public int DegreeOfParallelism { get; set; }
+ public bool Sequential { get; set; }
+ public bool Framework { get; set; }
+ public bool UseFramework { get; set; }
+ public bool Release { get; set; }
+ public bool LargeBubble { get; set; }
+ public int CompilationTimeoutMinutes { get; set; }
+ public int ExecutionTimeoutMinutes { get; set; }
+ public DirectoryInfo[] ReferencePath { get; set; }
+ public FileInfo[] IssuesPath { get; set; }
+ public FileInfo R2RDumpPath { get; set; }
+ public FileInfo CrossgenResponseFile { get; set; }
+ public DirectoryInfo[] RewriteOldPath { get;set; }
+ public DirectoryInfo[] RewriteNewPath { get;set; }
+ public string ConfigurationSuffix => (Release ? "-ret.out" : "-chk.out");
+
+ public IEnumerable<string> ReferencePaths()
+ {
+ if (ReferencePath != null)
+ {
+ foreach (DirectoryInfo referencePath in ReferencePath)
+ {
+ yield return referencePath.FullName;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Construct CoreRoot native path for a given compiler runner.
+ /// </summary>
+ /// <param name="index">Compiler runner index</param>
+ /// <returns></returns>
+ public string CoreRootOutputPath(CompilerIndex index, bool isFramework)
+ {
+ if (CoreRootDirectory == null)
+ {
+ return null;
+ }
+
+ string outputPath = CoreRootDirectory.FullName;
+ if (!isFramework && (Framework || UseFramework))
+ {
+ outputPath = Path.Combine(outputPath, index.ToString() + ConfigurationSuffix);
+ }
+ return outputPath;
+ }
+
+ /// <summary>
+ /// Creates compiler runner instances for each supported compiler based on the populated BuildOptions.
+ /// </summary>
+ /// <param name="isFramework">True if compiling the CoreFX framework assemblies</param>
+ /// <param name="referencePaths">Optional set of reference paths to use instead of BuildOptions.ReferencePaths()</param>
+ public IEnumerable<CompilerRunner> CompilerRunners(bool isFramework, IEnumerable<string> overrideReferencePaths = null)
+ {
+ List<CompilerRunner> runners = new List<CompilerRunner>();
+
+ if (CpaotDirectory != null)
+ {
+ List<string> referencePaths = new List<string>();
+ referencePaths.Add(CoreRootOutputPath(CompilerIndex.CPAOT, isFramework));
+ referencePaths.AddRange(overrideReferencePaths != null ? overrideReferencePaths : ReferencePaths());
+ runners.Add(new CpaotRunner(this, referencePaths));
+ }
+
+ if (Crossgen)
+ {
+ if (CoreRootDirectory == null)
+ {
+ throw new Exception("-coreroot folder not specified, cannot use Crossgen runner");
+ }
+ List<string> referencePaths = new List<string>();
+ referencePaths.Add(CoreRootOutputPath(CompilerIndex.Crossgen, isFramework));
+ referencePaths.AddRange(overrideReferencePaths != null ? overrideReferencePaths : ReferencePaths());
+ runners.Add(new CrossgenRunner(this, referencePaths));
+ }
+
+ if (!NoJit)
+ {
+ runners.Add(new JitRunner(this));
+ }
+
+ return runners;
+ }
+
+ public string CoreRunPath(CompilerIndex index, bool isFramework)
+ {
+ string coreRunDir = CoreRootOutputPath(index, isFramework);
+ string coreRunExe = "corerun".OSExeSuffix();
+ string coreRunPath = Path.Combine(coreRunDir, coreRunExe);
+ if (!File.Exists(coreRunPath))
+ {
+ Console.Error.WriteLine($@"{coreRunExe} not found in {coreRunDir}, explicit exe launches won't work");
+ }
+ return coreRunPath;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.CommandLine;
+using System.CommandLine.Builder;
+using System.CommandLine.Invocation;
+using System.IO;
+
+namespace ReadyToRun.SuperIlc
+{
+ internal static class CommandLineOptions
+ {
+ public static CommandLineBuilder Build()
+ {
+ var parser = new CommandLineBuilder()
+ .AddCommand(CompileFolder())
+ .AddCommand(CompileSubtree())
+ .AddCommand(CompileNugetPackages())
+ .AddCommand(CompileCrossgenRsp());
+
+ return parser;
+
+ Command CompileFolder() =>
+ new Command("compile-directory", "Compile all assemblies in directory",
+ new Option[]
+ {
+ InputDirectory(),
+ OutputDirectory(),
+ CoreRootDirectory(),
+ CpaotDirectory(),
+ Crossgen(),
+ NoJit(),
+ NoExe(),
+ NoEtw(),
+ NoCleanup(),
+ DegreeOfParallelism(),
+ Sequential(),
+ Framework(),
+ UseFramework(),
+ Release(),
+ LargeBubble(),
+ ReferencePath(),
+ IssuesPath(),
+ CompilationTimeoutMinutes(),
+ ExecutionTimeoutMinutes(),
+ R2RDumpPath(),
+ },
+ handler: CommandHandler.Create<BuildOptions>(CompileDirectoryCommand.CompileDirectory));
+
+ Command CompileSubtree() =>
+ new Command("compile-subtree", "Build each directory in a given subtree containing any managed assemblies as a separate app",
+ new Option[]
+ {
+ InputDirectory(),
+ OutputDirectory(),
+ CoreRootDirectory(),
+ CpaotDirectory(),
+ Crossgen(),
+ NoJit(),
+ NoExe(),
+ NoEtw(),
+ NoCleanup(),
+ DegreeOfParallelism(),
+ Sequential(),
+ Framework(),
+ UseFramework(),
+ Release(),
+ LargeBubble(),
+ ReferencePath(),
+ IssuesPath(),
+ CompilationTimeoutMinutes(),
+ ExecutionTimeoutMinutes(),
+ R2RDumpPath(),
+ },
+ handler: CommandHandler.Create<BuildOptions>(CompileSubtreeCommand.CompileSubtree));
+
+ Command CompileNugetPackages() =>
+ new Command("compile-nuget", "Restore a list of Nuget packages into an empty console app, publish, and optimize with Crossgen / CPAOT",
+ new Option[]
+ {
+ R2RDumpPath(),
+ InputDirectory(),
+ OutputDirectory(),
+ PackageList(),
+ CoreRootDirectory(),
+ Crossgen(),
+ CpaotDirectory(),
+ NoCleanup(),
+ DegreeOfParallelism(),
+ CompilationTimeoutMinutes(),
+ ExecutionTimeoutMinutes(),
+ },
+ handler: CommandHandler.Create<BuildOptions>(CompileNugetCommand.CompileNuget));
+
+ Command CompileCrossgenRsp() =>
+ new Command("compile-crossgen-rsp", "Use existing Crossgen .rsp file(s) to build assmeblies, optionally rewriting base paths",
+ new Option[]
+ {
+ InputDirectory(),
+ CrossgenResponseFile(),
+ OutputDirectory(),
+ CoreRootDirectory(),
+ Crossgen(),
+ CpaotDirectory(),
+ NoCleanup(),
+ DegreeOfParallelism(),
+ CompilationTimeoutMinutes(),
+ RewriteOldPath(),
+ RewriteNewPath(),
+ },
+ handler: CommandHandler.Create<BuildOptions>(CompileFromCrossgenRspCommand.CompileFromCrossgenRsp));
+
+ // Todo: Input / Output directories should be required arguments to the command when they're made available to handlers
+ // https://github.com/dotnet/command-line-api/issues/297
+ Option InputDirectory() =>
+ new Option(new[] { "--input-directory", "-in" }, "Folder containing assemblies to optimize", new Argument<DirectoryInfo>().ExistingOnly());
+
+ Option OutputDirectory() =>
+ new Option(new[] { "--output-directory", "-out" }, "Folder to emit compiled assemblies", new Argument<DirectoryInfo>().LegalFilePathsOnly());
+
+ Option CoreRootDirectory() =>
+ new Option(new[] { "--core-root-directory", "-cr" }, "Location of the CoreCLR CORE_ROOT folder", new Argument<DirectoryInfo>().ExistingOnly());
+
+ Option CpaotDirectory() =>
+ new Option(new[] { "--cpaot-directory", "-cpaot" }, "Folder containing the CPAOT compiler", new Argument<DirectoryInfo>().ExistingOnly());
+
+ Option ReferencePath() =>
+ new Option(new[] { "--reference-path", "-r" }, "Folder containing assemblies to reference during compilation", new Argument<DirectoryInfo[]>() { Arity = ArgumentArity.ZeroOrMore }.ExistingOnly());
+
+ Option Crossgen() =>
+ new Option(new[] { "--crossgen" }, "Compile the apps using Crossgen in the CORE_ROOT folder", new Argument<bool>());
+
+ Option NoJit() =>
+ new Option(new[] { "--nojit" }, "Don't run tests in JITted mode", new Argument<bool>());
+
+ Option NoEtw() =>
+ new Option(new[] { "--noetw" }, "Don't capture jitted methods using ETW", new Argument<bool>());
+
+ Option NoExe() =>
+ new Option(new[] { "--noexe" }, "Compilation-only mode (don't execute the built apps)", new Argument<bool>());
+
+ Option NoCleanup() =>
+ new Option(new[] { "--nocleanup" }, "Don't clean up compilation artifacts after test runs", new Argument<bool>());
+
+ Option DegreeOfParallelism() =>
+ new Option(new[] { "--degree-of-parallelism", "-dop" }, "Override default compilation / execution DOP (default = logical processor count)", new Argument<int>());
+
+ Option Sequential() =>
+ new Option(new[] { "--sequential" }, "Run tests sequentially", new Argument<bool>());
+
+ Option Framework() =>
+ new Option(new[] { "--framework" }, "Precompile and use native framework", new Argument<bool>());
+
+ Option UseFramework() =>
+ new Option(new[] { "--use-framework" }, "Use native framework (don't precompile, assume previously compiled)", new Argument<bool>());
+
+ Option Release() =>
+ new Option(new[] { "--release" }, "Build the tests in release mode", new Argument<bool>());
+
+ Option LargeBubble() =>
+ new Option(new[] { "--large-bubble" }, "Assume all input files as part of one version bubble", new Argument<bool>());
+
+ Option IssuesPath() =>
+ new Option(new[] { "--issues-path", "-ip" }, "Path to issues.targets", new Argument<FileInfo[]>() { Arity = ArgumentArity.ZeroOrMore });
+
+ Option CompilationTimeoutMinutes() =>
+ new Option(new[] { "--compilation-timeout-minutes", "-ct" }, "Compilation timeout (minutes)", new Argument<int>());
+
+ Option ExecutionTimeoutMinutes() =>
+ new Option(new[] { "--execution-timeout-minutes", "-et" }, "Execution timeout (minutes)", new Argument<int>());
+
+ Option R2RDumpPath() =>
+ new Option(new[] { "--r2r-dump-path", "-r2r" }, "Path to R2RDump.exe/dll", new Argument<FileInfo>().ExistingOnly());
+
+ Option CrossgenResponseFile() =>
+ new Option(new [] { "--crossgen-response-file", "-rsp" }, "Response file to transpose", new Argument<FileInfo>().ExistingOnly());
+
+ Option RewriteOldPath() =>
+ new Option(new [] { "--rewrite-old-path" }, "Path substring to replace", new Argument<DirectoryInfo[]>(){ Arity = ArgumentArity.ZeroOrMore });
+
+ Option RewriteNewPath() =>
+ new Option(new [] { "--rewrite-new-path" }, "Path substring to use instead", new Argument<DirectoryInfo[]>(){ Arity = ArgumentArity.ZeroOrMore });
+
+ //
+ // compile-nuget specific options
+ //
+ Option PackageList() =>
+ new Option(new[] { "--package-list", "-pl" }, "Text file containing a package name on each line", new Argument<FileInfo>().ExistingOnly());
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace ReadyToRun.SuperIlc
+{
+ class CompileDirectoryCommand
+ {
+ public static int CompileDirectory(BuildOptions options)
+ {
+ if (options.InputDirectory == null)
+ {
+ Console.Error.WriteLine("--input-directory is a required argument.");
+ return 1;
+ }
+
+ if (options.OutputDirectory == null)
+ {
+ options.OutputDirectory = options.InputDirectory;
+ }
+
+ if (options.OutputDirectory.IsParentOf(options.InputDirectory))
+ {
+ Console.Error.WriteLine("Error: Input and output folders must be distinct, and the output directory (which gets deleted) better not be a parent of the input directory.");
+ return 1;
+ }
+
+ IEnumerable<CompilerRunner> runners = options.CompilerRunners(isFramework: false);
+
+ PathExtensions.DeleteOutputFolders(options.OutputDirectory.FullName, options.CoreRootDirectory.FullName, recursive: false);
+
+ BuildFolder folder = BuildFolder.FromDirectory(options.InputDirectory.FullName, runners, options.OutputDirectory.FullName, options);
+ if (folder == null)
+ {
+ Console.Error.WriteLine($"No managed app found in {options.InputDirectory.FullName}");
+ }
+
+ BuildFolderSet folderSet = new BuildFolderSet(new BuildFolder[] { folder }, runners, options);
+ bool success = folderSet.Build(runners);
+ folderSet.WriteLogs();
+
+ if (!options.NoCleanup)
+ {
+ PathExtensions.DeleteOutputFolders(options.OutputDirectory.FullName, options.CoreRootDirectory.FullName, recursive: false);
+ }
+
+ return success ? 0 : 1;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace ReadyToRun.SuperIlc
+{
+ class CompileFromCrossgenRspCommand
+ {
+ /// <summary>
+ /// Utility mode that allows compilation of a set of assemblies using their existing Crossgen response files.
+ /// This is currently useful for workloads like Bing which have a large complicated web of binaries in different folders
+ /// with potentially different sets of reference paths used for different assemblies.
+ /// </summary>
+ public static int CompileFromCrossgenRsp(BuildOptions options)
+ {
+ if (options.CrossgenResponseFile == null && options.InputDirectory == null)
+ {
+ Console.Error.WriteLine("Specify --response-file or --input-directory containing multiple response files.");
+ return 1;
+ }
+
+ if (options.OutputDirectory == null)
+ {
+ if (options.InputDirectory != null)
+ {
+ options.OutputDirectory = options.InputDirectory;
+ }
+ else
+ {
+ options.OutputDirectory = new DirectoryInfo(Path.GetDirectoryName(options.CrossgenResponseFile.FullName));
+ }
+ } else if (options.InputDirectory != null && options.OutputDirectory.IsParentOf(options.InputDirectory))
+ {
+ Console.Error.WriteLine("Error: Input and output folders must be distinct, and the output directory (which gets deleted) better not be a parent of the input directory.");
+ return 1;
+ }
+
+ // This command does not work in the context of an app, just a loose set of rsp files so don't execute anything we compile
+ options.NoJit = true;
+ options.NoEtw = true;
+
+ //
+ // Determine whether we're compiling a single .rsp or a folder of them
+ //
+ var responseFiles = new List<string>();
+ if (options.CrossgenResponseFile != null)
+ {
+ responseFiles.Add(options.CrossgenResponseFile.FullName);
+ }
+ else
+ {
+ responseFiles = Directory.EnumerateFiles(options.InputDirectory.FullName, "*.rsp", SearchOption.TopDirectoryOnly).ToList();
+ }
+
+ Dictionary<string, string> pathReplacements = new Dictionary<string, string>();
+
+ if ((options.RewriteOldPath == null) != (options.RewriteNewPath == null))
+ {
+ Console.Error.WriteLine("Error: --rewrite-old-path and --rewrite-new-path must both be specified if either is used.");
+ return 1;
+ }
+
+ if (options.RewriteOldPath != null && options.RewriteNewPath != null)
+ {
+ if (options.RewriteOldPath.Length != options.RewriteNewPath.Length)
+ {
+ Console.Error.WriteLine("Error: --rewrite-old-path and --rewrite-new-path were specified a different number of times.");
+ return 1;
+ }
+
+ for (int i = 0; i < options.RewriteNewPath.Length; i++)
+ {
+ pathReplacements.Add(options.RewriteOldPath[i].FullName, options.RewriteNewPath[i].FullName);
+ Console.WriteLine($"Re-writing path {options.RewriteOldPath[i].FullName} as {options.RewriteNewPath[i].FullName}");
+ }
+ }
+
+ bool success = true;
+ int compilationFailures = 0;
+ int totalCompilations = 0;
+ // Collect all the compilations first
+ foreach (var inputRsp in responseFiles)
+ {
+ var crossgenArguments = CrossgenArguments.ParseFromResponseFile(inputRsp)
+ .ReplacePaths(pathReplacements);
+
+ Console.WriteLine($"{inputRsp} -> {crossgenArguments.InputFile}");
+ var compilerRunners = options.CompilerRunners(false, crossgenArguments.ReferencePaths);
+
+ string responseFileOuputPath = Path.Combine(options.OutputDirectory.FullName, Path.GetFileNameWithoutExtension(inputRsp));
+ responseFileOuputPath.RecreateDirectory();
+
+ List<ProcessInfo> fileCompilations = new List<ProcessInfo>();
+ foreach (CompilerRunner runner in compilerRunners)
+ {
+ var compilationProcess = new ProcessInfo(new CompilationProcessConstructor(runner, responseFileOuputPath, crossgenArguments.InputFile));
+ fileCompilations.Add(compilationProcess);
+ }
+
+ ParallelRunner.Run(fileCompilations, options.DegreeOfParallelism);
+ totalCompilations++;
+
+ foreach (var compilationProcess in fileCompilations)
+ {
+ if (!compilationProcess.Succeeded)
+ {
+ success = false;
+ compilationFailures++;
+
+ Console.WriteLine($"Failed compiling {compilationProcess.Parameters.InputFileName}");
+ }
+ }
+ }
+
+ Console.WriteLine("Rsp Compilation Results");
+ Console.WriteLine($"Total compilations: {totalCompilations}");
+ Console.WriteLine($"Compilation failures: {compilationFailures}");
+
+ return success ? 0 : 1;
+ }
+
+ private class CrossgenArguments
+ {
+ public string InputFile;
+ public List<string> ReferencePaths = new List<string>();
+
+ public static CrossgenArguments ParseFromResponseFile(string responseFile)
+ {
+ var arguments = new CrossgenArguments();
+
+ string[] tokenizedArguments = TokenizeArguments(responseFile);
+
+ for (int i = 0; i < tokenizedArguments.Length; i++)
+ {
+ string arg = tokenizedArguments[i];
+ if (MatchParameter("in", arg))
+ {
+ arguments.InputFile = tokenizedArguments[i + 1];
+ }
+ else if (MatchParameter("App_Paths", arg) || MatchParameter("Platform_Assemblies_Paths", arg))
+ {
+ string appPaths = tokenizedArguments[i + 1];
+ arguments.ReferencePaths.AddRange(appPaths.Split(';').TakeWhile(x => !string.IsNullOrWhiteSpace(x)));
+ ++i;
+ }
+ else if (MatchParameter("verbose", arg)
+ || MatchParameter("readytorun", arg))
+ {
+ // Skip unparameterized switches
+ continue;
+ }
+ else if (MatchParameter("jitpath", arg))
+ {
+ // Skip switches with one parameter
+ ++i;
+ continue;
+ }
+ else if (!IsSwitch(arg))
+ {
+ Debug.Assert(arguments.InputFile == null);
+ arguments.InputFile = arg;
+ }
+ }
+
+ return arguments;
+ }
+
+ public CrossgenArguments ReplacePaths(Dictionary<string, string> replacementPaths)
+ {
+ foreach (var replacePath in replacementPaths)
+ {
+ if (InputFile.StartsWith(replacePath.Key, ignoreCase: Environment.OSVersion.Platform == PlatformID.Win32NT, culture: null))
+ {
+ InputFile = InputFile.Replace(replacePath.Key, replacePath.Value, ignoreCase: Environment.OSVersion.Platform == PlatformID.Win32NT, culture: null);
+ }
+
+ for (int i = 0; i < ReferencePaths.Count; i++)
+ {
+ if (ReferencePaths[i].StartsWith(replacePath.Key, ignoreCase: Environment.OSVersion.Platform == PlatformID.Win32NT, culture: null))
+ {
+ ReferencePaths[i] = ReferencePaths[i].Replace(replacePath.Key, replacePath.Value, ignoreCase: Environment.OSVersion.Platform == PlatformID.Win32NT, culture: null);
+ }
+ }
+ }
+
+ return this;
+ }
+
+ private static bool MatchParameter(string paramName, string inputArg)
+ {
+ if (inputArg.Length == 0)
+ return false;
+
+ if (!IsSwitch(inputArg))
+ return false;
+
+ return string.Equals(paramName, inputArg.Substring(1), StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsSwitch(string inputArg) => inputArg.StartsWith('/') || inputArg.StartsWith('-');
+ private static string[] TokenizeArguments(string responseFile)
+ {
+ var arguments = new List<string>();
+ using (TextReader reader = File.OpenText(responseFile))
+ {
+ StringBuilder sb = new StringBuilder();
+ while (true)
+ {
+ int nextChar = reader.Read();
+ if (nextChar == -1)
+ {
+ break;
+ }
+
+ char currentChar = (char)nextChar;
+ if (!char.IsWhiteSpace(currentChar))
+ {
+ sb.Append(currentChar);
+ }
+ else
+ {
+ if (sb.Length > 0)
+ {
+ arguments.Add(sb.ToString());
+ sb.Clear();
+ }
+ }
+ }
+
+ // Flush everything after the last white space
+ arguments.Add(sb.ToString());
+ }
+
+ return arguments.ToArray();
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace ReadyToRun.SuperIlc
+{
+ /// <summary>
+ /// Adds a list of Nuget packages to an empty console app, publishes the app, and runs Crossgen / CPAOT
+ /// on the published assemblies.
+ /// </summary>
+ class CompileNugetCommand
+ {
+ public static int CompileNuget(BuildOptions options)
+ {
+ // We don't want to launch these apps when building the folder set below
+ options.NoExe = true;
+ options.NoJit = true;
+ options.InputDirectory = options.OutputDirectory;
+
+ IList<string> packageList = ReadPackageNames(options.PackageList.FullName);
+
+ if (options.OutputDirectory == null)
+ {
+ Console.Error.WriteLine("--output-directory is a required argument.");
+ return 1;
+ }
+
+ IEnumerable<string> referencePaths = options.ReferencePaths();
+ IEnumerable<CompilerRunner> runners = options.CompilerRunners(false);
+
+ PathExtensions.DeleteOutputFolders(options.OutputDirectory.FullName, options.CoreRootDirectory.FullName, recursive: false);
+
+ string nugetOutputFolder = Path.Combine(options.OutputDirectory.FullName, "nuget.out");
+ Directory.CreateDirectory(nugetOutputFolder);
+
+ var publishedAppFoldersToCompile = new List<BuildFolder>();
+ using (StreamWriter nugetLog = File.CreateText(Path.Combine(nugetOutputFolder, "nugetLog.txt")))
+ {
+ foreach (var package in packageList)
+ {
+ nugetLog.WriteLine($"Creating empty app for {package}");
+
+ // Create an app folder
+ string appFolder = Path.Combine(nugetOutputFolder, $"{package}.TestApp");
+ Directory.CreateDirectory(appFolder);
+
+ int exitCode = DotnetCli.New(appFolder, "console", nugetLog);
+ if (exitCode != 0)
+ {
+ nugetLog.WriteLine($"dotnet new console for {package} failed with exit code {exitCode}");
+ continue;
+ }
+
+ exitCode = DotnetCli.AddPackage(appFolder, package, nugetLog);
+ if (exitCode != 0)
+ {
+ nugetLog.WriteLine($"dotnet add package {package} failed with exit code {exitCode}");
+ continue;
+ }
+
+ exitCode = DotnetCli.Publish(appFolder, nugetLog);
+ if (exitCode != 0)
+ {
+ nugetLog.WriteLine($"dotnet publish failed with exit code {exitCode}");
+ continue;
+ }
+
+ // This is not a reliable way of building the publish folder
+ string publishFolder = Path.Combine(appFolder, @"bin\Debug\netcoreapp3.0\publish");
+ if (!Directory.Exists(publishFolder))
+ {
+ nugetLog.WriteLine($"Could not find folder {publishFolder} containing the published app.");
+ continue;
+ }
+
+ publishedAppFoldersToCompile.Add(BuildFolder.FromDirectory(publishFolder, runners, appFolder, options));
+ }
+
+ BuildFolderSet folderSet = new BuildFolderSet(publishedAppFoldersToCompile, runners, options);
+ bool success = folderSet.Build(runners);
+ folderSet.WriteLogs();
+
+ if (!options.NoCleanup)
+ {
+ PathExtensions.DeleteOutputFolders(options.OutputDirectory.FullName, options.CoreRootDirectory.FullName, recursive: false);
+ }
+
+ return success ? 0 : 1;
+ }
+ }
+
+ private static IList<string> ReadPackageNames(string packageListFile) => File.ReadAllLines(packageListFile);
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ReadyToRun.SuperIlc
+{
+ class CompileSubtreeCommand
+ {
+ public static int CompileSubtree(BuildOptions options)
+ {
+ if (options.InputDirectory == null)
+ {
+ Console.WriteLine("--input-directory is a required argument.");
+ return 1;
+ }
+
+ if (options.OutputDirectory == null)
+ {
+ options.OutputDirectory = options.InputDirectory;
+ }
+
+ if (options.OutputDirectory.IsParentOf(options.InputDirectory))
+ {
+ Console.WriteLine("Error: Input and output folders must be distinct, and the output directory (which gets deleted) better not be a parent of the input directory.");
+ return 1;
+ }
+
+ IEnumerable<CompilerRunner> runners = options.CompilerRunners(isFramework: false);
+
+ PathExtensions.DeleteOutputFolders(options.OutputDirectory.FullName, options.CoreRootDirectory.FullName, recursive: true);
+
+ string[] directories = LocateSubtree(
+ options.InputDirectory.FullName,
+ (options.Framework || options.UseFramework) ? options.CoreRootDirectory.FullName : null)
+ .ToArray();
+
+ ConcurrentBag<BuildFolder> folders = new ConcurrentBag<BuildFolder>();
+ int relativePathOffset = options.InputDirectory.FullName.Length;
+ if (relativePathOffset > 0 && options.InputDirectory.FullName[relativePathOffset - 1] != Path.DirectorySeparatorChar)
+ {
+ relativePathOffset++;
+ }
+
+ int folderCount = 0;
+ int compilationCount = 0;
+ int executionCount = 0;
+ Parallel.ForEach(directories, (string directory) =>
+ {
+ string outputDirectoryPerFolder = options.OutputDirectory.FullName;
+ if (directory.Length > relativePathOffset)
+ {
+ outputDirectoryPerFolder = Path.Combine(outputDirectoryPerFolder, directory.Substring(relativePathOffset));
+ }
+ try
+ {
+ BuildFolder folder = BuildFolder.FromDirectory(directory.ToString(), runners, outputDirectoryPerFolder, options);
+ if (folder != null)
+ {
+ folders.Add(folder);
+ Interlocked.Add(ref compilationCount, folder.Compilations.Count);
+ Interlocked.Add(ref executionCount, folder.Executions.Count);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("Error scanning folder {0}: {1}", directory, ex.Message);
+ }
+ int currentCount = Interlocked.Increment(ref folderCount);
+ if (currentCount % 100 == 0)
+ {
+ StringBuilder lineReport = new StringBuilder();
+ lineReport.Append($@"Found {folders.Count} folders to build ");
+ lineReport.Append($@"({compilationCount} compilations, ");
+ if (!options.NoExe)
+ {
+ lineReport.Append($@"{executionCount} executions, ");
+ }
+ lineReport.Append($@"{currentCount} / {directories.Length} folders scanned)");
+ Console.WriteLine(lineReport.ToString());
+ }
+ });
+ Console.Write($@"Found {folders.Count} folders to build ({compilationCount} compilations, ");
+ if (!options.NoExe)
+ {
+ Console.Write($@"{executionCount} executions, ");
+ }
+ Console.WriteLine($@"{directories.Length} folders scanned)");
+
+ BuildFolderSet folderSet = new BuildFolderSet(folders, runners, options);
+ bool success = folderSet.Build(runners);
+ folderSet.WriteLogs();
+
+ if (!options.NoCleanup)
+ {
+ PathExtensions.DeleteOutputFolders(options.OutputDirectory.FullName, options.CoreRootDirectory.FullName, recursive: true);
+ }
+
+ return success ? 0 : 1;
+ }
+
+ private static string[] LocateSubtree(string folder, string coreRootFolder)
+ {
+ ConcurrentBag<string> directories = new ConcurrentBag<string>();
+ LocateSubtreeAsync(folder, coreRootFolder, directories).Wait();
+ return directories.ToArray();
+ }
+
+ private static async Task LocateSubtreeAsync(string folder, string coreRootFolder, ConcurrentBag<string> directories)
+ {
+ if (!Path.GetExtension(folder).Equals(".out", StringComparison.OrdinalIgnoreCase))
+ {
+ if (coreRootFolder == null || !folder.Equals(coreRootFolder, StringComparison.OrdinalIgnoreCase))
+ {
+ directories.Add(folder);
+ }
+ List<Task> subfolderTasks = new List<Task>();
+ foreach (string subdir in Directory.EnumerateDirectories(folder))
+ {
+ subfolderTasks.Add(Task.Run(() => LocateSubtreeAsync(subdir, coreRootFolder, directories)));
+ }
+ await Task.WhenAll(subfolderTasks);
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace ReadyToRun.SuperIlc
+{
+ public enum CompilerIndex
+ {
+ CPAOT,
+ Crossgen,
+ Jit,
+
+ Count
+ }
+
+ public abstract class CompilerRunner
+ {
+ /// <summary>
+ /// Timeout for running R2R Dump to disassemble compilation outputs.
+ /// </summary>
+ public const int R2RDumpTimeoutMilliseconds = 60 * 1000;
+
+ protected readonly BuildOptions _options;
+ protected readonly string _compilerPath;
+ protected readonly IEnumerable<string> _referenceFolders;
+
+ public CompilerRunner(BuildOptions options, string compilerFolder, IEnumerable<string> referenceFolders)
+ {
+ _options = options;
+ _compilerPath = compilerFolder;
+ _referenceFolders = referenceFolders;
+ }
+
+ public IEnumerable<string> ReferenceFolders => _referenceFolders;
+
+ public abstract CompilerIndex Index { get; }
+
+ public string CompilerName => Index.ToString();
+
+ protected abstract string CompilerFileName { get; }
+ protected abstract IEnumerable<string> BuildCommandLineArguments(string assemblyFileName, string outputFileName);
+
+ public virtual ProcessParameters CompilationProcess(string outputRoot, string assemblyFileName)
+ {
+ CreateOutputFolder(outputRoot);
+
+ string outputFileName = GetOutputFileName(outputRoot, assemblyFileName);
+ string responseFile = GetResponseFileName(outputRoot, assemblyFileName);
+ var commandLineArgs = BuildCommandLineArguments(assemblyFileName, outputFileName);
+ CreateResponseFile(responseFile, commandLineArgs);
+
+ ProcessParameters processParameters = new ProcessParameters();
+ processParameters.ProcessPath = Path.Combine(_compilerPath, CompilerFileName);
+ processParameters.Arguments = $"@{responseFile}";
+ if (_options.CompilationTimeoutMinutes != 0)
+ {
+ processParameters.TimeoutMilliseconds = _options.CompilationTimeoutMinutes * 60 * 1000;
+ }
+ else
+ {
+ processParameters.TimeoutMilliseconds = ProcessParameters.DefaultIlcTimeout;
+ }
+ processParameters.LogPath = outputFileName + ".ilc.log";
+ processParameters.InputFileName = assemblyFileName;
+ processParameters.OutputFileName = outputFileName;
+ processParameters.CompilationCostHeuristic = new FileInfo(assemblyFileName).Length;
+
+ return processParameters;
+ }
+
+ public ProcessParameters CompilationR2RDumpProcess(string compiledExecutable, bool naked)
+ {
+ if (_options.R2RDumpPath == null)
+ {
+ return null;
+ }
+
+ StringBuilder commonBuilder = new StringBuilder();
+
+ commonBuilder.Append($@"""{_options.R2RDumpPath.FullName}""");
+
+ commonBuilder.Append(" --normalize");
+ commonBuilder.Append(" --sc");
+ commonBuilder.Append(" --disasm");
+
+ foreach (string referencePath in _options.ReferencePaths())
+ {
+ commonBuilder.Append($@" --rp ""{referencePath}""");
+ }
+
+ if (_options.CoreRootDirectory != null)
+ {
+ commonBuilder.Append($@" --rp ""{_options.CoreRootDirectory.FullName}""");
+ }
+
+ commonBuilder.Append($@" --in ""{compiledExecutable}""");
+
+ StringBuilder builder = new StringBuilder(commonBuilder.ToString());
+ if (naked)
+ {
+ builder.Append(" --naked");
+ }
+
+ string outputFileName = compiledExecutable + (naked ? ".naked.r2r" : ".raw.r2r");
+ builder.Append($@" --out ""{outputFileName}""");
+
+ ProcessParameters param = new ProcessParameters();
+ param.ProcessPath = "dotnet";
+ param.Arguments = builder.ToString();
+ param.TimeoutMilliseconds = R2RDumpTimeoutMilliseconds;
+ param.LogPath = compiledExecutable + (naked ? ".naked.r2r.log" : ".raw.r2r.log");
+ param.InputFileName = compiledExecutable;
+ param.OutputFileName = outputFileName;
+ try
+ {
+ param.CompilationCostHeuristic = new FileInfo(compiledExecutable).Length;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("File not found: {0}: {1}", compiledExecutable, ex);
+ param.CompilationCostHeuristic = 0;
+ }
+
+ return param;
+ }
+
+ protected virtual ProcessParameters ExecutionProcess(IEnumerable<string> modules, IEnumerable<string> folders, bool noEtw)
+ {
+ ProcessParameters processParameters = new ProcessParameters();
+
+ if (_options.ExecutionTimeoutMinutes != 0)
+ {
+ processParameters.TimeoutMilliseconds = _options.ExecutionTimeoutMinutes * 60 * 1000;
+ }
+ else if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("__GCSTRESSLEVEL")))
+ {
+ processParameters.TimeoutMilliseconds = ProcessParameters.DefaultExeTimeout;
+ }
+ else
+ {
+ processParameters.TimeoutMilliseconds = ProcessParameters.DefaultExeTimeoutGCStress;
+ }
+
+ // TODO: support for tier jitting - for now we just turn it off as it may distort the JIT statistics
+ processParameters.EnvironmentOverrides["COMPLUS_TieredCompilation"] = "0";
+
+ processParameters.CollectJittedMethods = !noEtw;
+ if (!noEtw)
+ {
+ processParameters.MonitorModules = modules;
+ processParameters.MonitorFolders = folders;
+ }
+
+ return processParameters;
+ }
+
+ public virtual ProcessParameters ScriptExecutionProcess(string outputRoot, string scriptPath, IEnumerable<string> modules, IEnumerable<string> folders)
+ {
+ string scriptToRun = GetOutputFileName(outputRoot, scriptPath);
+ ProcessParameters processParameters = ExecutionProcess(modules, folders, _options.NoEtw);
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ processParameters.ProcessPath = scriptToRun;
+ processParameters.Arguments = null;
+ }
+ else
+ {
+ Linux.MakeExecutable(scriptToRun);
+ processParameters.ProcessPath = "bash";
+ processParameters.Arguments = "-c " + scriptToRun;
+ }
+
+ processParameters.InputFileName = scriptToRun;
+ processParameters.LogPath = scriptToRun + ".log";
+ processParameters.EnvironmentOverrides["CORE_ROOT"] = _options.CoreRootOutputPath(Index, isFramework: false);
+ return processParameters;
+ }
+
+ public virtual ProcessParameters AppExecutionProcess(string outputRoot, string appPath, IEnumerable<string> modules, IEnumerable<string> folders)
+ {
+ string exeToRun = GetOutputFileName(outputRoot, appPath);
+ ProcessParameters processParameters = ExecutionProcess(modules, folders, _options.NoEtw);
+ processParameters.ProcessPath = _options.CoreRunPath(Index, isFramework: false);
+ processParameters.Arguments = exeToRun;
+ processParameters.InputFileName = exeToRun;
+ processParameters.LogPath = exeToRun + ".log";
+ processParameters.ExpectedExitCode = 100;
+ return processParameters;
+ }
+
+ public void CreateOutputFolder(string outputRoot)
+ {
+ string outputPath = GetOutputPath(outputRoot);
+ if (!Directory.Exists(outputPath))
+ {
+ Directory.CreateDirectory(outputPath);
+ }
+ }
+
+ protected void CreateResponseFile(string responseFile, IEnumerable<string> commandLineArguments)
+ {
+ using (TextWriter tw = File.CreateText(responseFile))
+ {
+ foreach (var arg in commandLineArguments)
+ {
+ tw.WriteLine(arg);
+ }
+ }
+ }
+
+ public string GetOutputPath(string outputRoot) => Path.Combine(outputRoot, CompilerName + _options.ConfigurationSuffix);
+
+ // <input>\a.dll -> <output>\a.dll
+ public string GetOutputFileName(string outputRoot, string fileName) =>
+ Path.Combine(GetOutputPath(outputRoot), $"{Path.GetFileName(fileName)}");
+
+ public string GetResponseFileName(string outputRoot, string assemblyFileName) =>
+ Path.Combine(GetOutputPath(outputRoot), Path.GetFileName(assemblyFileName) + ".rsp");
+ }
+
+ public abstract class CompilerRunnerProcessConstructor : ProcessConstructor
+ {
+ protected readonly CompilerRunner _runner;
+
+ public CompilerRunnerProcessConstructor(CompilerRunner runner)
+ {
+ _runner = runner;
+ }
+ }
+
+ public class CompilationProcessConstructor : CompilerRunnerProcessConstructor
+ {
+ private readonly string _outputRoot;
+ private readonly string _assemblyFileName;
+
+ public CompilationProcessConstructor(CompilerRunner runner, string outputRoot, string assemblyFileName)
+ : base(runner)
+ {
+ _outputRoot = outputRoot;
+ _assemblyFileName = assemblyFileName;
+ }
+
+ public override ProcessParameters Construct()
+ {
+ return _runner.CompilationProcess(_outputRoot, _assemblyFileName);
+ }
+ }
+
+ public class R2RDumpProcessConstructor : CompilerRunnerProcessConstructor
+ {
+ private readonly string _compiledExecutable;
+ private readonly bool _naked;
+
+ public R2RDumpProcessConstructor(CompilerRunner runner, string compiledExecutable, bool naked)
+ : base(runner)
+ {
+ _compiledExecutable = compiledExecutable;
+ _naked = naked;
+ }
+
+ public override ProcessParameters Construct()
+ {
+ return _runner.CompilationR2RDumpProcess(_compiledExecutable, _naked);
+ }
+ }
+
+ public sealed class ScriptExecutionProcessConstructor : CompilerRunnerProcessConstructor
+ {
+ private readonly string _outputRoot;
+ private readonly string _scriptPath;
+ private readonly IEnumerable<string> _modules;
+ private readonly IEnumerable<string> _folders;
+
+ public ScriptExecutionProcessConstructor(CompilerRunner runner, string outputRoot, string scriptPath, IEnumerable<string> modules, IEnumerable<string> folders)
+ : base(runner)
+ {
+ _outputRoot = outputRoot;
+ _scriptPath = scriptPath;
+ _modules = modules;
+ _folders = folders;
+ }
+
+ public override ProcessParameters Construct()
+ {
+ return _runner.ScriptExecutionProcess(_outputRoot, _scriptPath, _modules, _folders);
+ }
+ }
+
+ public sealed class AppExecutionProcessConstructor : CompilerRunnerProcessConstructor
+ {
+ private readonly string _outputRoot;
+ private readonly string _appPath;
+ private readonly IEnumerable<string> _modules;
+ private readonly IEnumerable<string> _folders;
+
+ public AppExecutionProcessConstructor(CompilerRunner runner, string outputRoot, string appPath, IEnumerable<string> modules, IEnumerable<string> folders)
+ : base(runner)
+ {
+ _outputRoot = outputRoot;
+ _appPath = appPath;
+ _modules = modules;
+ _folders = folders;
+ }
+
+ public override ProcessParameters Construct()
+ {
+ return _runner.AppExecutionProcess(_outputRoot, _appPath, _modules, _folders);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+class ComputeManagedAssemblies
+{
+ public static IEnumerable<string> GetManagedAssembliesInFolder(string folder)
+ {
+ foreach (string file in Directory.EnumerateFiles(folder))
+ {
+ if (IsManaged(file))
+ {
+ yield return file;
+ }
+ }
+ }
+
+ static ConcurrentDictionary<string, bool> _isManagedCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
+
+ public static bool IsManaged(string file)
+ {
+ // Only files named *.dll and *.exe are considered as possible assemblies
+ if (!Path.HasExtension(file) || (Path.GetExtension(file) != ".dll" && Path.GetExtension(file) != ".exe"))
+ return false;
+
+ bool isManaged;
+ lock (_isManagedCache)
+ {
+ if (_isManagedCache.TryGetValue(file, out isManaged))
+ {
+ return isManaged;
+ }
+ }
+
+ try
+ {
+ using (FileStream moduleStream = File.OpenRead(file))
+ using (var module = new PEReader(moduleStream))
+ {
+ if (module.HasMetadata)
+ {
+ MetadataReader moduleMetadataReader = module.GetMetadataReader();
+ if (moduleMetadataReader.IsAssembly)
+ {
+ string culture = moduleMetadataReader.GetString(moduleMetadataReader.GetAssemblyDefinition().Culture);
+
+ if (culture == "" || culture.Equals("neutral", StringComparison.OrdinalIgnoreCase))
+ {
+ isManaged = true;
+ }
+ }
+ }
+ }
+ }
+ catch (BadImageFormatException)
+ {
+ isManaged = false;
+ }
+
+ _isManagedCache[file] = isManaged;
+
+ return isManaged;
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace ReadyToRun.SuperIlc
+{
+
+ /// <summary>
+ /// Compiles assemblies using the Cross-Platform AOT compiler
+ /// </summary>
+ class CpaotRunner : CompilerRunner
+ {
+ public override CompilerIndex Index => CompilerIndex.CPAOT;
+
+ protected override string CompilerFileName => "crossgen2".OSExeSuffix();
+
+ private List<string> _resolvedReferences;
+
+ public CpaotRunner(BuildOptions options, IEnumerable<string> referencePaths)
+ : base(options, options.CpaotDirectory.FullName, referencePaths)
+ { }
+
+ protected override ProcessParameters ExecutionProcess(IEnumerable<string> modules, IEnumerable<string> folders, bool noEtw)
+ {
+ ProcessParameters processParameters = base.ExecutionProcess(modules, folders, noEtw);
+ processParameters.EnvironmentOverrides["COMPLUS_ReadyToRun"] = "1";
+ return processParameters;
+ }
+
+ protected override IEnumerable<string> BuildCommandLineArguments(string assemblyFileName, string outputFileName)
+ {
+ // The file to compile
+ yield return assemblyFileName;
+
+ // Output
+ yield return $"-o:{outputFileName}";
+
+ // Todo: Allow control of some of these
+ yield return "--targetarch=x64";
+
+ if (_options.Release)
+ {
+ yield return "-O";
+ }
+
+ if (_options.LargeBubble)
+ {
+ yield return "--inputbubble";
+ }
+
+ foreach (var reference in ComputeManagedAssemblies.GetManagedAssembliesInFolder(Path.GetDirectoryName(assemblyFileName)))
+ {
+ yield return $"-r:{reference}";
+ }
+
+ if (_resolvedReferences == null)
+ {
+ _resolvedReferences = ResolveReferences();
+ }
+
+ foreach (string asmRef in _resolvedReferences)
+ {
+ yield return asmRef;
+ }
+ }
+
+ private List<string> ResolveReferences()
+ {
+ List<string> references = new List<string>();
+ foreach (var referenceFolder in _referenceFolders)
+ {
+ foreach (var reference in ComputeManagedAssemblies.GetManagedAssembliesInFolder(referenceFolder))
+ {
+ references.Add($"-r:{reference}");
+ }
+ }
+ return references;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace ReadyToRun.SuperIlc
+{
+ /// <summary>
+ /// Compiles assemblies using the Cross-Platform AOT compiler
+ /// </summary>
+ class CrossgenRunner : CompilerRunner
+ {
+ public override CompilerIndex Index => CompilerIndex.Crossgen;
+
+ protected override string CompilerFileName => "crossgen".OSExeSuffix();
+
+ public CrossgenRunner(BuildOptions options, IEnumerable<string> referencePaths)
+ : base(options, options.CoreRootDirectory.FullName, referencePaths) { }
+
+ protected override ProcessParameters ExecutionProcess(IEnumerable<string> modules, IEnumerable<string> folders, bool noEtw)
+ {
+ ProcessParameters processParameters = base.ExecutionProcess(modules, folders, noEtw);
+ processParameters.EnvironmentOverrides["COMPLUS_ReadyToRun"] = "1";
+ processParameters.EnvironmentOverrides["COMPLUS_NoGuiOnAssert"] = "1";
+ return processParameters;
+ }
+
+ protected override IEnumerable<string> BuildCommandLineArguments(string assemblyFileName, string outputFileName)
+ {
+ // The file to compile
+ yield return "/in";
+ yield return assemblyFileName;
+
+ // Output
+ yield return "/out";
+ yield return outputFileName;
+
+ if (_options.LargeBubble)
+ {
+ yield return "/largeversionbubble";
+ }
+
+ yield return "/platform_assemblies_paths";
+
+ IEnumerable<string> paths = new string[] { Path.GetDirectoryName(assemblyFileName) }.Concat(_referenceFolders);
+
+ yield return paths.ConcatenatePaths();
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace ReadyToRun.SuperIlc
+{
+ /// <summary>
+ /// Helpers to call dotnet CLI via Process.Start
+ /// </summary>
+ static class DotnetCli
+ {
+ // Default 30s timeout for CLI commands
+ private const int DotnetCliTimeout = 30 * 1000;
+
+ public static int New(string workingDirectory, string projectType, StreamWriter logWriter)
+ {
+ return RunProcess("dotnet", $"new {projectType}", workingDirectory, DotnetCliTimeout, logWriter);
+ }
+
+ public static int AddPackage(string workingDirectory, string packageName, StreamWriter logWriter)
+ {
+ return RunProcess("dotnet", $"add package {packageName}", workingDirectory, DotnetCliTimeout, logWriter);
+ }
+
+ public static int Publish(string workingDirectory, StreamWriter logWriter)
+ {
+ return RunProcess("dotnet", "publish", workingDirectory, DotnetCliTimeout, logWriter);
+ }
+
+ private static int RunProcess(string processPath, string arguments, string workingDirectory, int timeout, StreamWriter logWriter)
+ {
+ ProcessStartInfo psi = new ProcessStartInfo()
+ {
+ FileName = processPath,
+ UseShellExecute = false,
+ WorkingDirectory = workingDirectory,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+
+ Process p = new Process();
+ p.StartInfo = psi;
+ p.OutputDataReceived += new DataReceivedEventHandler((sender, eventArgs) =>
+ {
+ if (!string.IsNullOrEmpty(eventArgs.Data))
+ {
+ logWriter.WriteLine(eventArgs.Data);
+ }
+ });
+
+ p.ErrorDataReceived += new DataReceivedEventHandler((sender, eventArgs) =>
+ {
+ if (!string.IsNullOrEmpty(eventArgs.Data))
+ {
+ logWriter.WriteLine(eventArgs.Data);
+ }
+ });
+
+ p.Start();
+ p.BeginErrorReadLine();
+ p.BeginOutputReadLine();
+
+ if (p.WaitForExit(timeout))
+ {
+ return p.ExitCode;
+ }
+ else
+ {
+ try
+ {
+ p.Kill();
+ }
+ catch (Exception)
+ {
+ // Silently ignore exceptions during this call to Kill as
+ // the process may have exited in the meantime.
+ }
+ logWriter.WriteLine($"{processPath} {arguments} timed out after {timeout}ms.");
+ return 1;
+ }
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace ReadyToRun.SuperIlc
+{
+ /// <summary>
+ /// No-op runner keeping the original IL assemblies to be directly run with full jitting.
+ /// </summary>
+ class JitRunner : CompilerRunner
+ {
+ public override CompilerIndex Index => CompilerIndex.Jit;
+
+ protected override string CompilerFileName => "clrjit.dll";
+
+ public JitRunner(BuildOptions options)
+ : base(options, null, new string[] { options.CoreRootDirectory.FullName }.Concat(options.ReferencePaths())) { }
+
+ /// <summary>
+ /// JIT runner has no compilation process as it doesn't transform the source IL code in any manner.
+ /// </summary>
+ /// <returns></returns>
+ public override ProcessParameters CompilationProcess(string outputRoot, string assemblyFileName)
+ {
+ File.Copy(assemblyFileName, GetOutputFileName(outputRoot, assemblyFileName), overwrite: true);
+ return null;
+ }
+
+ protected override ProcessParameters ExecutionProcess(IEnumerable<string> modules, IEnumerable<string> folders, bool noEtw)
+ {
+ ProcessParameters processParameters = base.ExecutionProcess(modules, folders, noEtw);
+ processParameters.EnvironmentOverrides["COMPLUS_ReadyToRun"] = "0";
+ return processParameters;
+ }
+
+ protected override IEnumerable<string> BuildCommandLineArguments(string assemblyFileName, string outputFileName)
+ {
+ // This should never get called as the overridden CompilationProcess returns null
+ throw new NotImplementedException();
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace ReadyToRun.SuperIlc
+{
+ internal static class Linux
+ {
+ [Flags]
+ private enum Permissions : byte
+ {
+ Read = 1,
+ Write = 2,
+ Execute = 4,
+
+ ReadExecute = Read | Execute,
+
+ ReadWriteExecute = Read | Write | Execute,
+ }
+
+ private enum PermissionGroupShift : int
+ {
+ Owner = 6,
+ Group = 3,
+ Other = 0,
+ }
+
+ [DllImport("libc", SetLastError = true)]
+ private static extern int chmod(string path, int flags);
+
+ public static void MakeExecutable(string path)
+ {
+ int errno = chmod(path,
+ ((byte)Permissions.ReadWriteExecute << (int)PermissionGroupShift.Owner) |
+ ((byte)Permissions.ReadExecute << (int)PermissionGroupShift.Group) |
+ ((byte)Permissions.ReadExecute << (int)PermissionGroupShift.Other));
+
+ if (errno != 0)
+ {
+ throw new Exception($@"Failed to set permissions on {path}: error code {errno}");
+ }
+ }
+ }
+
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Parsers;
+using Microsoft.Diagnostics.Tracing.Session;
+
+/// <summary>
+/// Execute a given number of mutually independent build subprocesses represented by an array of
+/// command lines with a given degree of parallelization.
+/// </summary>
+public sealed class ParallelRunner
+{
+ /// <summary>
+ /// Helper class for launching mutually independent build subprocesses in parallel.
+ /// It supports launching the processes and optionally redirecting their standard and
+ /// error output streams to prevent them from interleaving in the final build output log.
+ /// Multiple instances of this class representing the individual running processes
+ /// can exist at the same time.
+ /// </summmary>
+ class ProcessSlot
+ {
+ /// <summary>
+ /// Process slot index (used for diagnostic purposes)
+ /// </summary>
+ private readonly int _slotIndex;
+
+ /// <summary>
+ /// Event used to report that a process has exited
+ /// </summary>
+ private readonly AutoResetEvent _processExitEvent;
+
+ /// <summary>
+ /// Process object
+ /// </summary>
+ private ProcessRunner _processRunner;
+
+ /// <summary>
+ /// Constructor stores global slot parameters and initializes the slot state machine
+ /// </summary>
+ /// <param name="slotIndex">Process slot index used for diagnostic purposes</param>
+ /// <param name="processExitEvent">Event used to report process exit</param>
+ public ProcessSlot(int slotIndex, AutoResetEvent processExitEvent)
+ {
+ _slotIndex = slotIndex;
+ _processExitEvent = processExitEvent;
+ }
+
+ /// <summary>
+ /// Launch a new process.
+ /// </summary>
+ /// <param name="processInfo">application to execute</param>
+ /// <param name="jittedMethods">Jitted method collector</param>
+ /// <param name="processIndex">Numeric index used to prefix messages pertaining to this process in the console output</param>
+ /// <param name="processCount">Total number of processes being executed (used for displaying progress)</param>
+ /// <param name="progressIndex">Number of processes that have already finished (for displaying progress)</param>
+ /// <param name="failureCount">Number of pre-existing failures in this parallel build step (for displaying progress)</param>
+ public void Launch(ProcessInfo processInfo, ReadyToRunJittedMethods jittedMethods, int processIndex, int processCount, int progressIndex, int failureCount)
+ {
+ Debug.Assert(_processRunner == null);
+ Console.WriteLine($"{processIndex} / {processCount} ({(progressIndex * 100 / processCount)}%, {failureCount} failed): " +
+ $"launching: {processInfo.Parameters.ProcessPath} {processInfo.Parameters.Arguments}");
+
+ _processRunner = new ProcessRunner(processInfo, processIndex, processCount, jittedMethods, _processExitEvent);
+ }
+
+ public bool IsAvailable(ref int progressIndex, ref int failureCount)
+ {
+ if (_processRunner == null)
+ {
+ return true;
+ }
+ if (!_processRunner.IsAvailable(ref progressIndex, ref failureCount))
+ {
+ return false;
+ }
+ _processRunner.Dispose();
+ _processRunner = null;
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Execute a given set of mutually independent build commands with given degree of
+ /// parallelism.
+ /// </summary>
+ /// <param name="processesToRun">Processes to execute in parallel</param>
+ /// <param name="degreeOfParallelism">Maximum number of processes to execute in parallel, 0 = logical processor count</param>
+ public static void Run(IEnumerable<ProcessInfo> processesToRun, int degreeOfParallelism = 0)
+ {
+ if (degreeOfParallelism == 0)
+ {
+ degreeOfParallelism = Environment.ProcessorCount;
+ }
+
+ List<ProcessInfo> processList = new List<ProcessInfo>();
+ bool collectEtwTraces = false;
+ foreach (ProcessInfo process in processesToRun)
+ {
+ process.Construct();
+ processList.Add(process);
+ collectEtwTraces |= process.Parameters.CollectJittedMethods;
+ }
+
+ processList.Sort((a, b) => b.Parameters.CompilationCostHeuristic.CompareTo(a.Parameters.CompilationCostHeuristic));
+
+ int processCount = processList.Count;
+ if (processCount < degreeOfParallelism)
+ {
+ // We never need a higher DOP than the number of process to execute
+ degreeOfParallelism = processCount;
+ }
+
+ if (collectEtwTraces)
+ {
+ // In ETW collection mode, separate the processes to run into smaller batches as we need to keep
+ // the process objects alive for the entire duration of the parallel execution, otherwise PID's
+ // may get recycled by the OS and we can no longer back-translate PIDs in events to the logical
+ // process executions. Without parallelization, we simply run the processes one by one.
+ int etwCollectionBatching = (degreeOfParallelism == 1 ? 1 : 10);
+ int failureCount = 0;
+
+ for (int batchStartIndex = 0; batchStartIndex < processCount; batchStartIndex += etwCollectionBatching)
+ {
+ int batchEndIndex = Math.Min(batchStartIndex + etwCollectionBatching, processCount);
+ BuildEtwProcesses(
+ startIndex: batchStartIndex,
+ endIndex: batchEndIndex,
+ totalCount: processCount,
+ failureCount: failureCount,
+ processList,
+ degreeOfParallelism);
+
+ for (int processIndex = batchStartIndex; processIndex < batchEndIndex; processIndex++)
+ {
+ if (!processList[processIndex].Succeeded)
+ {
+ failureCount++;
+ }
+ }
+ }
+ }
+ else
+ {
+ BuildProjects(startIndex: 0, endIndex: processCount, totalCount: processCount, failureCount: 0, processList, null, degreeOfParallelism);
+ }
+ }
+
+ private static void BuildEtwProcesses(int startIndex, int endIndex, int totalCount, int failureCount, List<ProcessInfo> processList, int degreeOfParallelism)
+ {
+ using (TraceEventSession traceEventSession = new TraceEventSession("ReadyToRunTestSession"))
+ {
+ traceEventSession.EnableProvider(ClrTraceEventParser.ProviderGuid, TraceEventLevel.Verbose, (ulong)(ClrTraceEventParser.Keywords.Jit | ClrTraceEventParser.Keywords.Loader));
+ using (ReadyToRunJittedMethods jittedMethods = new ReadyToRunJittedMethods(traceEventSession, processList, startIndex, endIndex))
+ {
+ Task.Run(() =>
+ {
+ BuildProjects(startIndex, endIndex, totalCount, failureCount, processList, jittedMethods, degreeOfParallelism);
+ traceEventSession.Stop();
+ });
+ }
+ traceEventSession.Source.Process();
+ }
+
+ // Append jitted method info to the logs
+ for (int index = startIndex; index < endIndex; index++)
+ {
+ ProcessInfo processInfo = processList[index];
+ if (processInfo.Parameters.CollectJittedMethods)
+ {
+ using (StreamWriter processLogWriter = new StreamWriter(processInfo.Parameters.LogPath, append: true))
+ {
+ if (processInfo.JittedMethods != null)
+ {
+ processLogWriter.WriteLine($"Jitted methods ({processInfo.JittedMethods.Sum(moduleMethodsKvp => moduleMethodsKvp.Value.Count)} total):");
+ foreach (KeyValuePair<string, HashSet<string>> jittedMethodsPerModule in processInfo.JittedMethods)
+ {
+ foreach (string method in jittedMethodsPerModule.Value)
+ {
+ processLogWriter.WriteLine(jittedMethodsPerModule.Key + " -> " + method);
+ }
+ }
+ }
+ else
+ {
+ processLogWriter.WriteLine("Jitted method info not available");
+ }
+ }
+ }
+ }
+ }
+
+ private static void BuildProjects(int startIndex, int endIndex, int totalCount, int failureCount, List<ProcessInfo> processList, ReadyToRunJittedMethods jittedMethods, int degreeOfParallelism)
+ {
+ using (AutoResetEvent processExitEvent = new AutoResetEvent(initialState: false))
+ {
+ ProcessSlot[] processSlots = new ProcessSlot[degreeOfParallelism];
+ for (int index = 0; index < degreeOfParallelism; index++)
+ {
+ processSlots[index] = new ProcessSlot(index, processExitEvent);
+ }
+
+ int progressIndex = startIndex;
+ for (int index = startIndex; index < endIndex; index++)
+ {
+ ProcessInfo processInfo = processList[index];
+
+ // Allocate a process slot, potentially waiting on the exit event
+ // when all slots are busy (running)
+ ProcessSlot freeSlot = null;
+ do
+ {
+ foreach (ProcessSlot slot in processSlots)
+ {
+ if (slot.IsAvailable(ref progressIndex, ref failureCount))
+ {
+ freeSlot = slot;
+ break;
+ }
+ }
+ if (freeSlot == null)
+ {
+ // All slots are busy - wait for a process to finish
+ processExitEvent.WaitOne(200);
+ }
+ }
+ while (freeSlot == null);
+
+ freeSlot.Launch(processInfo, jittedMethods, index, totalCount, progressIndex, failureCount);
+ }
+
+ // We have launched all the commands, now wait for all processes to finish
+ bool activeProcessesExist;
+ do
+ {
+ activeProcessesExist = false;
+ foreach (ProcessSlot slot in processSlots)
+ {
+ if (!slot.IsAvailable(ref progressIndex, ref failureCount))
+ {
+ activeProcessesExist = true;
+ }
+ }
+ if (activeProcessesExist)
+ {
+ processExitEvent.WaitOne();
+ }
+ }
+ while (activeProcessesExist);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+/// <summary>
+/// A set of helper to manipulate paths into a canonicalized form to ensure user-provided paths
+/// match those in the ETW log.
+/// </summary>
+static class PathExtensions
+{
+ /// <summary>
+ /// Millisecond timeout for file / directory deletion.
+ /// </summary>
+ const int DeletionTimeoutMilliseconds = 10000;
+
+ /// <summary>
+ /// Back-off for repeated checks for directory deletion. According to my local experience [trylek],
+ /// when the directory is opened in the file explorer, the propagation typically takes 2 seconds.
+ /// </summary>
+ const int DirectoryDeletionBackoffMilliseconds = 500;
+
+ internal static string OSExeSuffix(this string path) => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? path + ".exe" : path);
+
+ internal static string ToAbsolutePath(this string argValue) => Path.GetFullPath(argValue);
+
+ internal static string ToAbsoluteDirectoryPath(this string argValue) => argValue.ToAbsolutePath().StripTrailingDirectorySeparators();
+
+ internal static string StripTrailingDirectorySeparators(this string str)
+ {
+ if (String.IsNullOrWhiteSpace(str))
+ {
+ return str;
+ }
+
+ while (str.Length > 0 && str[str.Length - 1] == Path.DirectorySeparatorChar)
+ {
+ str = str.Remove(str.Length - 1);
+ }
+
+ return str;
+ }
+
+ internal static string ConcatenatePaths(this IEnumerable<string> paths)
+ {
+ return string.Join(Path.PathSeparator, paths);
+ }
+
+ // TODO: this assumes we're running tests from the CoreRT root
+ internal static string DotNetAppPath => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet" : "Tools/dotnetcli/dotnet";
+
+ internal static void RecreateDirectory(this string path)
+ {
+ if (Directory.Exists(path))
+ {
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+ Task<bool> deleteSubtreeTask = path.DeleteSubtree();
+ deleteSubtreeTask.Wait();
+ if (deleteSubtreeTask.Result)
+ {
+ Console.WriteLine("Deleted {0} in {1} msecs", path, stopwatch.ElapsedMilliseconds);
+ }
+ else
+ {
+ throw new Exception($"Error: Could not delete output folder {path}");
+ }
+ }
+
+ Directory.CreateDirectory(path);
+ }
+
+ internal static bool IsParentOf(this DirectoryInfo outputPath, DirectoryInfo inputPath)
+ {
+ DirectoryInfo parentInfo = inputPath.Parent;
+ while (parentInfo != null)
+ {
+ if (parentInfo == outputPath)
+ return true;
+
+ parentInfo = parentInfo.Parent;
+ }
+
+ return false;
+ }
+
+ public static string FindFile(this string fileName, IEnumerable<string> paths)
+ {
+ foreach (string path in paths)
+ {
+ string fileOnPath = Path.Combine(path, fileName);
+ if (File.Exists(fileOnPath))
+ {
+ return fileOnPath;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Parallel deletion of multiple disjunct subtrees.
+ /// </summary>
+ /// <param name="path">List of directories to delete</param>
+ /// <returns>Task returning true on success, false on failure</returns>
+ public static bool DeleteSubtrees(this string[] paths)
+ {
+ return DeleteSubtreesAsync(paths).Result;
+ }
+
+ private static async Task<bool> DeleteSubtreesAsync(this string[] paths)
+ {
+ bool succeeded = true;
+
+ var tasks = new List<Task<bool>>();
+ foreach (string path in paths)
+ {
+ try
+ {
+ if (!Directory.Exists(path))
+ {
+ // Non-existent folders are harmless w.r.t. deletion
+ Console.WriteLine("Skipping non-existent folder: '{0}'", path);
+ }
+ else
+ {
+ Console.WriteLine("Deleting '{0}'", path);
+ tasks.Add(path.DeleteSubtree());
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine("Error deleting '{0}': {1}", path, ex.Message);
+ succeeded = false;
+ }
+ }
+
+ await Task<bool>.WhenAll(tasks);
+
+ foreach (var task in tasks)
+ {
+ if (!task.Result)
+ {
+ succeeded = false;
+ break;
+ }
+ }
+ return succeeded;
+ }
+
+ private static async Task<bool> DeleteSubtree(this string folder)
+ {
+ Task<bool>[] subtasks = new []
+ {
+ DeleteSubtreesAsync(Directory.GetDirectories(folder)),
+ DeleteFiles(Directory.GetFiles(folder))
+ };
+
+ await Task<bool>.WhenAll(subtasks);
+ bool succeeded = subtasks.All(subtask => subtask.Result);
+
+ if (succeeded)
+ {
+ Stopwatch folderDeletion = new Stopwatch();
+ folderDeletion.Start();
+ while (Directory.Exists(folder))
+ {
+ try
+ {
+ Directory.Delete(folder, recursive: false);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // Directory not found is OK (the directory might have been deleted during the back-off delay).
+ }
+ catch (Exception)
+ {
+ Console.WriteLine("Folder deletion failure, maybe transient ({0} msecs): '{1}'", folderDeletion.ElapsedMilliseconds, folder);
+ }
+
+ if (!Directory.Exists(folder))
+ {
+ break;
+ }
+
+ if (folderDeletion.ElapsedMilliseconds > DeletionTimeoutMilliseconds)
+ {
+ Console.Error.WriteLine("Timed out trying to delete directory '{0}'", folder);
+ succeeded = false;
+ break;
+ }
+
+ Thread.Sleep(DirectoryDeletionBackoffMilliseconds);
+ }
+ }
+
+ return succeeded;
+ }
+
+ private static async Task<bool> DeleteFiles(string[] files)
+ {
+ Task<bool>[] tasks = new Task<bool>[files.Length];
+ for (int i = 0; i < files.Length; i++)
+ {
+ int temp = i;
+ tasks[i] = Task<bool>.Run(() => files[temp].DeleteFile());
+ }
+ await Task<bool>.WhenAll(tasks);
+ return tasks.All(task => task.Result);
+ }
+
+ private static bool DeleteFile(this string file)
+ {
+ try
+ {
+ File.Delete(file);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"{file}: {ex.Message}");
+ return false;
+ }
+ }
+
+ public static string[] LocateOutputFolders(string folder, string coreRootFolder, bool recursive)
+ {
+ ConcurrentBag<string> directories = new ConcurrentBag<string>();
+ LocateOutputFoldersAsync(folder, coreRootFolder, recursive, directories).Wait();
+ return directories.ToArray();
+ }
+
+ private static async Task LocateOutputFoldersAsync(string folder, string coreRootFolder, bool recursive, ConcurrentBag<string> directories)
+ {
+ if (coreRootFolder == null || !StringComparer.OrdinalIgnoreCase.Equals(folder, coreRootFolder))
+ {
+ List<Task> subfolderTasks = new List<Task>();
+ foreach (string dir in Directory.EnumerateDirectories(folder))
+ {
+ if (Path.GetExtension(dir).Equals(".out", StringComparison.OrdinalIgnoreCase))
+ {
+ directories.Add(dir);
+ }
+ else if (recursive)
+ {
+ subfolderTasks.Add(Task.Run(() => LocateOutputFoldersAsync(dir, coreRootFolder, recursive, directories)));
+ }
+ }
+ await Task.WhenAll(subfolderTasks);
+ }
+ }
+
+ public static bool DeleteOutputFolders(string folder, string coreRootFolder, bool recursive)
+ {
+ Stopwatch stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ Console.WriteLine("Locating output {0} {1}", (recursive ? "subtree" : "folder"), folder);
+ string[] outputFolders = LocateOutputFolders(folder, coreRootFolder, recursive);
+ Console.WriteLine("Deleting {0} output folders", outputFolders.Length);
+
+ if (DeleteSubtrees(outputFolders))
+ {
+ Console.WriteLine("Successfully deleted {0} output folders in {1} msecs", outputFolders.Length, stopwatch.ElapsedMilliseconds);
+ return true;
+ }
+ else
+ {
+ Console.Error.WriteLine("Failed deleting {0} output folders in {1} msecs", outputFolders.Length, stopwatch.ElapsedMilliseconds);
+ return false;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+public class ProcessParameters
+{
+ /// <summary>
+ /// 2 minutes should be plenty for a CPAOT / Crossgen compilation.
+ /// </summary>
+ public const int DefaultIlcTimeout = 2 * 60 * 1000;
+
+ /// <summary>
+ /// Test execution timeout.
+ /// </summary>
+ public const int DefaultExeTimeout = 200 * 1000;
+
+ /// <summary>
+ /// Test execution timeout under GC stress mode.
+ /// </summary>
+ public const int DefaultExeTimeoutGCStress = 2000 * 1000;
+
+ public string ProcessPath;
+ public string Arguments;
+ public Dictionary<string, string> EnvironmentOverrides = new Dictionary<string, string>();
+ public string LogPath;
+ public int TimeoutMilliseconds;
+ public int ExpectedExitCode;
+ public string InputFileName;
+ public string OutputFileName;
+ public long CompilationCostHeuristic;
+ public bool CollectJittedMethods;
+ public IEnumerable<string> MonitorModules;
+ public IEnumerable<string> MonitorFolders;
+}
+
+public abstract class ProcessConstructor
+{
+ public abstract ProcessParameters Construct();
+}
+
+public class ProcessInfo
+{
+ public ProcessConstructor Constructor;
+ public ProcessParameters Parameters;
+
+ public bool Finished;
+ public bool Succeeded;
+ public bool TimedOut;
+ public int DurationMilliseconds;
+ public int ExitCode;
+ public Dictionary<string, HashSet<string>> JittedMethods;
+
+ public bool Crashed => ExitCode < -1000 * 1000;
+
+ public ProcessInfo(ProcessConstructor constructor)
+ {
+ Constructor = constructor;
+ }
+
+ public void Construct()
+ {
+ Parameters = Constructor.Construct();
+ Constructor = null;
+ }
+}
+
+public class ProcessRunner : IDisposable
+{
+ public const int StateIdle = 0;
+ public const int StateRunning = 1;
+ public const int StateFinishing = 2;
+
+ public const int TimeoutExitCode = -103;
+
+ private readonly ProcessInfo _processInfo;
+
+ private readonly AutoResetEvent _processExitEvent;
+
+ private readonly int _processIndex;
+
+ private readonly int _processCount;
+
+ private Process _process;
+
+ private ReadyToRunJittedMethods _jittedMethods;
+
+ private readonly Stopwatch _stopwatch;
+
+ /// <summary>
+ /// This is actually a boolean flag but we're using int to let us use CPU-native interlocked exchange.
+ /// </summary>
+ private int _state;
+
+ private TextWriter _logWriter;
+
+ private CancellationTokenSource _cancellationTokenSource;
+
+ public ProcessRunner(ProcessInfo processInfo, int processIndex, int processCount, ReadyToRunJittedMethods jittedMethods, AutoResetEvent processExitEvent)
+ {
+ _processInfo = processInfo;
+ _processIndex = processIndex;
+ _processCount = processCount;
+ _jittedMethods = jittedMethods;
+ _processExitEvent = processExitEvent;
+
+ _cancellationTokenSource = new CancellationTokenSource();
+
+ _stopwatch = new Stopwatch();
+ _stopwatch.Start();
+ _state = StateIdle;
+
+ _logWriter = new StreamWriter(_processInfo.Parameters.LogPath);
+
+ if (_processInfo.Parameters.ProcessPath.Contains(' '))
+ {
+ _logWriter.Write($"\"{_processInfo.Parameters.ProcessPath}\"");
+ }
+ else
+ {
+ _logWriter.Write(_processInfo.Parameters.ProcessPath);
+ }
+ _logWriter.Write(' ');
+ _logWriter.WriteLine(_processInfo.Parameters.Arguments);
+ _logWriter.WriteLine("<<<<");
+
+ ProcessStartInfo psi = new ProcessStartInfo()
+ {
+ FileName = _processInfo.Parameters.ProcessPath,
+ Arguments = _processInfo.Parameters.Arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ foreach (KeyValuePair<string, string> environmentOverride in _processInfo.Parameters.EnvironmentOverrides)
+ {
+ psi.EnvironmentVariables[environmentOverride.Key] = environmentOverride.Value;
+ }
+
+ _process = new Process();
+ _process.StartInfo = psi;
+ _process.EnableRaisingEvents = true;
+ _process.Exited += new EventHandler(ExitEventHandler);
+
+ Interlocked.Exchange(ref _state, StateRunning);
+
+ _process.Start();
+ if (_processInfo.Parameters.CollectJittedMethods)
+ {
+ _jittedMethods.AddProcessMapping(_processInfo, _process);
+ }
+
+ _process.OutputDataReceived += new DataReceivedEventHandler(StandardOutputEventHandler);
+ _process.BeginOutputReadLine();
+
+ _process.ErrorDataReceived += new DataReceivedEventHandler(StandardErrorEventHandler);
+ _process.BeginErrorReadLine();
+
+ Task.Run(TimeoutWatchdog);
+ }
+
+ public void Dispose()
+ {
+ CleanupProcess();
+ CleanupLogWriter();
+ }
+
+ private void TimeoutWatchdog()
+ {
+ try
+ {
+ CancellationTokenSource source = _cancellationTokenSource;
+ if (source != null)
+ {
+ Task.Delay(_processInfo.Parameters.TimeoutMilliseconds, source.Token).Wait();
+ StopProcessAtomic();
+ }
+ }
+ catch (AggregateException ae) when (ae.InnerException is TaskCanceledException)
+ {
+ // Ignore cancellation
+ }
+ }
+
+ private void CleanupProcess()
+ {
+ if (_cancellationTokenSource != null)
+ {
+ _cancellationTokenSource?.Cancel();
+ _cancellationTokenSource = null;
+ }
+
+ // In ETW collection mode, the disposal is carried out in ReadyToRunJittedMethods
+ // as we need to keep the process alive for the entire lifetime of the trace event
+ // session, otherwise PID's may get recycled and we couldn't reliably back-translate
+ // them into the logical process executions.
+ if (_process != null && !_processInfo.Parameters.CollectJittedMethods)
+ {
+ _process.Dispose();
+ _process = null;
+ }
+ }
+
+ private void CleanupLogWriter()
+ {
+ if (_logWriter != null)
+ {
+ _logWriter.Dispose();
+ _logWriter = null;
+ }
+ }
+
+ private void ExitEventHandler(object sender, EventArgs eventArgs)
+ {
+ StopProcessAtomic();
+ }
+
+ private void StopProcessAtomic()
+ {
+ if (Interlocked.CompareExchange(ref _state, StateFinishing, StateRunning) == StateRunning)
+ {
+ _cancellationTokenSource.Cancel();
+ _processInfo.DurationMilliseconds = (int)_stopwatch.ElapsedMilliseconds;
+
+ _processExitEvent?.Set();
+ }
+ }
+
+ private void StandardOutputEventHandler(object sender, DataReceivedEventArgs eventArgs)
+ {
+ string data = eventArgs?.Data;
+ if (!string.IsNullOrEmpty(data))
+ {
+ lock (_logWriter)
+ {
+ _logWriter.WriteLine(data);
+ }
+ }
+ }
+
+ private void StandardErrorEventHandler(object sender, DataReceivedEventArgs eventArgs)
+ {
+ string data = eventArgs?.Data;
+ if (!string.IsNullOrEmpty(data))
+ {
+ lock (_logWriter)
+ {
+ _logWriter.WriteLine(data);
+ }
+ }
+ }
+
+ public bool IsAvailable(ref int progressIndex, ref int failureCount)
+ {
+ if (_state != StateFinishing)
+ {
+ return _state == StateIdle;
+ }
+
+ string processSpec;
+ if (!string.IsNullOrEmpty(_processInfo.Parameters.Arguments))
+ {
+ processSpec = Path.GetFileName(_processInfo.Parameters.ProcessPath) + " " + _processInfo.Parameters.Arguments;
+ }
+ else
+ {
+ processSpec = _processInfo.Parameters.ProcessPath;
+ }
+
+ _processInfo.TimedOut = !_process.WaitForExit(0);
+ if (_processInfo.TimedOut)
+ {
+ KillProcess();
+ }
+ _processInfo.ExitCode = (_processInfo.TimedOut ? TimeoutExitCode : _process.ExitCode);
+ _processInfo.Succeeded = (!_processInfo.TimedOut && _processInfo.ExitCode == _processInfo.Parameters.ExpectedExitCode);
+ _logWriter.WriteLine(">>>>");
+
+ if (!_processInfo.Succeeded)
+ {
+ failureCount++;
+ }
+
+ string linePrefix = $"{_processIndex} / {_processCount} ({(++progressIndex * 100 / _processCount)}%, {failureCount} failed): ";
+
+ if (_processInfo.Succeeded)
+ {
+ string successMessage = linePrefix + $"succeeded in {_processInfo.DurationMilliseconds} msecs";
+
+ _logWriter.WriteLine(successMessage);
+ Console.WriteLine(successMessage + $": {processSpec}");
+ _processInfo.Succeeded = true;
+ }
+ else
+ {
+ string failureMessage;
+ if (_processInfo.TimedOut)
+ {
+ failureMessage = linePrefix + $"timed out in {_processInfo.DurationMilliseconds} msecs";
+ }
+ else
+ {
+ failureMessage = linePrefix + $"failed in {_processInfo.DurationMilliseconds} msecs, exit code {_processInfo.ExitCode}";
+ if (_processInfo.ExitCode < 0)
+ {
+ failureMessage += $" = 0x{_processInfo.ExitCode:X8}";
+ }
+ failureMessage += $", expected {_processInfo.Parameters.ExpectedExitCode}";
+ }
+ _logWriter.WriteLine(failureMessage);
+ Console.Error.WriteLine(failureMessage + $": {processSpec}");
+ }
+
+ CleanupProcess();
+
+ _processInfo.Finished = true;
+
+ _logWriter.Flush();
+ _logWriter.Close();
+
+ CleanupLogWriter();
+
+ _state = StateIdle;
+ return true;
+ }
+
+ /// <summary>
+ /// Kills process execution. This may be called from a different thread.
+ /// </summary>
+ private void KillProcess()
+ {
+ try
+ {
+ _process?.Kill();
+ }
+ catch (Exception)
+ {
+ // Silently ignore exceptions during this call to Kill as
+ // the process may have exited in the meantime.
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.CommandLine.Builder;
+using System.CommandLine.Invocation;
+using System.Threading.Tasks;
+
+namespace ReadyToRun.SuperIlc
+{
+ class Program
+ {
+ static async Task<int> Main(string[] args)
+ {
+ var parser = CommandLineOptions.Build().UseDefaults().Build();
+
+ return await parser.InvokeAsync(args);
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ <NoWarn>$(NoWarn);NU1701</NoWarn>
+ <!-- Force C# 7.1 so we can use async Main -->
+ <LangVersion>latest</LangVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Build" Version="16.0.461" />
+ <PackageReference Include="Microsoft.Build.Framework" Version="16.0.461" />
+ <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.38" />
+ <PackageReference Include="System.CommandLine.Experimental" Version="0.2.0-alpha.19174.3" />
+ <PackageReference Include="System.Reflection.Metadata" Version="1.6.0" />
+ </ItemGroup>
+
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Diagnostics.Tracing;
+using Microsoft.Diagnostics.Tracing.Parsers.Clr;
+using Microsoft.Diagnostics.Tracing.Parsers;
+using Microsoft.Diagnostics.Tracing.Session;
+
+/// <summary>
+/// Intercept module loads for assemblies we want to collect method Jit info for.
+/// Each Method that gets Jitted from a ready-to-run assembly is interesting to look at.
+/// For a fully r2r'd assembly, there should be no such methods, so that would be a test failure.
+/// </summary>
+public class ReadyToRunJittedMethods : IDisposable
+{
+ /// <summary>
+ /// When collecting ETW traces, we need to keep all processes alive before the trace event session
+ /// is shut down and all events have been processes because otherwise the OS may recycle the PIDs
+ /// and prevent us from back-translating the events to the actual processes being executed.
+ /// </summary>
+ private List<Process> _etwProcesses;
+
+ private Dictionary<int, ProcessInfo> _pidToProcess;
+ private HashSet<string> _testModuleNames;
+ private HashSet<string> _testFolderNames;
+ private List<long> _testModuleIds = new List<long>();
+ private Dictionary<long, string> _testModuleIdToName = new Dictionary<long, string>();
+ private Dictionary<string, HashSet<string>> _methodsJitted = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
+
+ public ReadyToRunJittedMethods(TraceEventSession session, List<ProcessInfo> processList, int startIndex, int endIndex)
+ {
+ _etwProcesses = new List<Process>();
+ _pidToProcess = new Dictionary<int, ProcessInfo>();
+ _testModuleNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ _testFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ for (int index = startIndex; index < endIndex; index++)
+ {
+ ProcessInfo process = processList[index];
+ if (process.Parameters.CollectJittedMethods)
+ {
+ _testFolderNames.UnionWith(process.Parameters.MonitorFolders);
+ _testModuleNames.UnionWith(process.Parameters.MonitorModules);
+ }
+ }
+
+ session.Source.Clr.LoaderModuleLoad += delegate (ModuleLoadUnloadTraceData data)
+ {
+ if (ShouldMonitorModule(data))
+ {
+ // The console & method logging is normally too noisy to be turned on by default but
+ // it's sometimes useful for debugging purposes.
+ // Console.WriteLine($"Tracking module {data.ModuleILFileName} with Id {data.ModuleID}");
+ _testModuleIds.Add(data.ModuleID);
+ _testModuleIdToName[data.ModuleID] = Path.GetFileNameWithoutExtension(data.ModuleILFileName);
+ }
+ };
+
+ session.Source.Clr.MethodLoadVerbose += delegate (MethodLoadUnloadVerboseTraceData data)
+ {
+ ProcessInfo processInfo;
+ if (data.IsJitted && _pidToProcess.TryGetValue(data.ProcessID, out processInfo) && _testModuleIds.Contains(data.ModuleID))
+ {
+ // Console.WriteLine($"Method loaded {GetName(data)} - {data}");
+ string methodName = GetName(data);
+ string moduleName = _testModuleIdToName[data.ModuleID];
+ if (processInfo.JittedMethods == null)
+ {
+ processInfo.JittedMethods = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
+ }
+ HashSet<string> methodsForModule;
+ if (!processInfo.JittedMethods.TryGetValue(moduleName, out methodsForModule))
+ {
+ methodsForModule = new HashSet<string>();
+ processInfo.JittedMethods.Add(moduleName, methodsForModule);
+ }
+ methodsForModule.Add(methodName);
+ }
+ };
+ }
+
+ public void Dispose()
+ {
+ foreach (Process process in _etwProcesses)
+ {
+ process.Dispose();
+ }
+ }
+
+ public void AddProcessMapping(ProcessInfo processInfo, Process process)
+ {
+ _pidToProcess[process.Id] = processInfo;
+ _etwProcesses.Add(process);
+ }
+
+ private bool ShouldMonitorModule(ModuleLoadUnloadTraceData data)
+ {
+ if (!_pidToProcess.ContainsKey(data.ProcessID))
+ return false;
+
+ if (File.Exists(data.ModuleILPath) && _testFolderNames.Contains(Path.GetDirectoryName(data.ModuleILPath).ToAbsoluteDirectoryPath()))
+ return true;
+
+ if (_testModuleNames.Contains(data.ModuleILPath) || _testModuleNames.Contains(data.ModuleNativePath))
+ return true;
+
+ return false;
+ }
+
+ public IReadOnlyDictionary<string, HashSet<string>> JittedMethods => _methodsJitted;
+
+ /// <summary>
+ /// Returns the number of test assemblies that were loaded by the runtime
+ /// </summary>
+ public int AssembliesWithEventsCount => _testModuleIds.Count;
+
+ //
+ // Builds a method name from event data of the form Class.Method(arg1, arg2)
+ //
+ private static string GetName(MethodLoadUnloadVerboseTraceData data)
+ {
+ var signature = "";
+ var signatureWithReturnType = data.MethodSignature;
+ var openParenIndex = signatureWithReturnType.IndexOf('(');
+
+ if (0 <= openParenIndex)
+ {
+ signature = signatureWithReturnType.Substring(openParenIndex);
+ }
+
+ var className = data.MethodNamespace;
+ var firstBox = className.IndexOf('[');
+ var lastDot = className.LastIndexOf('.', firstBox >= 0 ? firstBox : className.Length - 1);
+ if (0 <= lastDot)
+ {
+ className = className.Substring(lastDot + 1);
+ }
+
+ var optionalSeparator = ".";
+ if (className.Length == 0)
+ {
+ optionalSeparator = "";
+ }
+
+ return className + optionalSeparator + data.MethodName + signature;
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Xml.Linq;
+
+using Microsoft.Build.Construction;
+using Microsoft.Build.Evaluation;
+
+namespace ReadyToRun.SuperIlc
+{
+ /// <summary>
+ /// This class represents a single test exclusion read from the issues.targets file.
+ /// </summary>
+ public class TestExclusion
+ {
+ /// <summary>
+ /// Path components (the individual directory levels read from the issues.targets file).
+ /// </summary>
+ public readonly string[] PathComponents;
+
+ /// <summary>
+ /// True when an issues.targets exclusion spec ends with an '**'.
+ /// </summary>
+ public readonly bool OpenEnd;
+
+ /// <summary>
+ /// Issue ID for the exclusion.
+ /// </summary>
+ public readonly string IssueID;
+
+ /// <summary>
+ /// Initialize a test exclusion record read from the issues.targets file.
+ /// </summary>
+ /// <param name="pathComponents">Path components for this test exclusion</param>
+ /// <param name="openEnd">True when the entry ends with '**'</param>
+ /// <param name="issueID">ID of the exclusion issue</param>
+ public TestExclusion(string[] pathComponents, bool openEnd, string issueID)
+ {
+ PathComponents = pathComponents;
+ OpenEnd = openEnd;
+ IssueID = issueID;
+ }
+
+ /// <summary>
+ /// Check whether the test exclusion entry matches a particular test folder / name.
+ /// </summary>
+ /// <param name="pathComponents">Components (directory levels) representing the test path</param>
+ /// <param name="firstComponent">Index of first element in pathComponents to analyze</param>
+ /// <returns></returns>
+ public bool Matches(string[] pathComponents, int firstComponent)
+ {
+ if (pathComponents[firstComponent].Equals(PathComponents[0], StringComparison.OrdinalIgnoreCase) &&
+ pathComponents.Length >= firstComponent + PathComponents.Length &&
+ (OpenEnd || pathComponents.Length == firstComponent + PathComponents.Length))
+ {
+ for (int matchIndex = 1; matchIndex < PathComponents.Length; matchIndex++)
+ {
+ if (!pathComponents[firstComponent + matchIndex].Equals(PathComponents[matchIndex], StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Map of test exclusions with search acceleration.
+ /// </summary>
+ public class TestExclusionMap
+ {
+ public readonly Dictionary<string, List<TestExclusion>> _folderToExclusions;
+
+ public TestExclusionMap()
+ {
+ _folderToExclusions = new Dictionary<string, List<TestExclusion>>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Add a single test exclusion to the map.
+ /// </summary>
+ /// <param name="exclusion"></param>
+ public void Add(TestExclusion exclusion)
+ {
+ if (!_folderToExclusions.TryGetValue(exclusion.PathComponents[0], out List<TestExclusion> exclusionsPerFolder))
+ {
+ exclusionsPerFolder = new List<TestExclusion>();
+ _folderToExclusions.Add(exclusion.PathComponents[0], exclusionsPerFolder);
+ }
+ exclusionsPerFolder.Add(exclusion);
+ }
+
+ /// <summary>
+ /// Locate the issue ID for a given test path if it exists; return false when not.
+ /// </summary>
+ /// <param name="pathComponents">Path components representing the test path to check</param>
+ /// <param name="issueID">Output issue ID when found, null otherwise</param>
+ /// <returns>True when the test was found in the exclusion list, false otherwise</returns>
+ public bool TryGetIssue(string[] pathComponents, out string issueID)
+ {
+ for (int firstComponent = 0; firstComponent < pathComponents.Length; firstComponent++)
+ {
+ if (_folderToExclusions.TryGetValue(pathComponents[firstComponent], out List<TestExclusion> exclusions))
+ {
+ foreach (TestExclusion exclusion in exclusions)
+ {
+ if (exclusion.Matches(pathComponents, firstComponent))
+ {
+ issueID = exclusion.IssueID;
+ return true;
+ }
+ }
+ }
+ }
+ issueID = null;
+ return false;
+ }
+
+ public bool TryGetIssue(string path, out string issueID)
+ {
+ string[] pathComponents = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
+ return TryGetIssue(pathComponents, out issueID);
+ }
+
+ private static XNamespace s_xmlNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";
+
+ public static TestExclusionMap Create(BuildOptions options)
+ {
+ TestExclusionMap outputMap = new TestExclusionMap();
+
+ if (options.IssuesPath != null)
+ {
+ Dictionary<string, List<TestExclusion>> exclusionsByCondition = new Dictionary<string, List<TestExclusion>>();
+
+ foreach (FileInfo issuesProject in options.IssuesPath)
+ {
+ string issuesProjectPath = issuesProject.FullName;
+ XDocument issuesXml = XDocument.Load(issuesProjectPath);
+ foreach (XElement itemGroupElement in issuesXml.Root.Elements(s_xmlNamespace + "ItemGroup"))
+ {
+ string condition = itemGroupElement.Attribute("Condition")?.Value ?? "";
+ List<TestExclusion> exclusions;
+ if (!exclusionsByCondition.TryGetValue(condition, out exclusions))
+ {
+ exclusions = new List<TestExclusion>();
+ exclusionsByCondition.Add(condition, exclusions);
+ }
+ foreach (XElement excludeListElement in itemGroupElement.Elements(s_xmlNamespace + "ExcludeList"))
+ {
+ string testPath = excludeListElement.Attribute("Include")?.Value ?? "";
+ string issueID = excludeListElement.Element(s_xmlNamespace + "Issue")?.Value ?? "N/A";
+ exclusions.Add(CreateTestExclusion(testPath, issueID));
+ }
+ }
+ }
+
+ Project project = new Project();
+ project.SetGlobalProperty("XunitTestBinBase", "*");
+ project.SetGlobalProperty("BuildArch", "x64");
+ // TODO: cross-OS CPAOT
+ project.SetGlobalProperty("TargetsWindows", (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "true" : "false"));
+ project.SetGlobalProperty("AltJitArch", "x64");
+ project.SetGlobalProperty("RunTestViaIlLink", "false");
+
+ ProjectRootElement root = project.Xml;
+ root.AddTarget("GetListOfTestCmds");
+
+ ProjectPropertyGroupElement propertyGroup = root.AddPropertyGroup();
+
+ // Generate properties into the project to make it evaluate all conditions found in the targets file
+ List<List<TestExclusion>> testExclusionLists = new List<List<TestExclusion>>();
+ testExclusionLists.Capacity = exclusionsByCondition.Count;
+ foreach (KeyValuePair<string, List<TestExclusion>> kvp in exclusionsByCondition)
+ {
+ string propertyName = "Condition_" + testExclusionLists.Count.ToString();
+ bool emptyKey = string.IsNullOrEmpty(kvp.Key);
+ propertyGroup.AddProperty(propertyName, emptyKey ? "true" : "false");
+ if (!emptyKey)
+ {
+ propertyGroup.AddProperty(propertyName, "true").Condition = kvp.Key;
+ }
+ testExclusionLists.Add(kvp.Value);
+ }
+
+ project.Build();
+ for (int exclusionListIndex = 0; exclusionListIndex < testExclusionLists.Count; exclusionListIndex++)
+ {
+ string conditionValue = project.GetProperty("Condition_" + exclusionListIndex.ToString()).EvaluatedValue;
+ if (conditionValue.Equals("true", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (TestExclusion exclusion in testExclusionLists[exclusionListIndex])
+ {
+ outputMap.Add(exclusion);
+ }
+ }
+ }
+ }
+
+ return outputMap;
+ }
+
+ private static TestExclusion CreateTestExclusion(string testPath, string issueId)
+ {
+ string[] pathComponents = testPath.Split(new char[] { '/' });
+ int begin = 0;
+ if (begin < pathComponents.Length && pathComponents[begin] == "$(XunitTestBinBase)")
+ {
+ begin++;
+ }
+ int end = pathComponents.Length;
+ while (end > begin && (pathComponents[end - 1] == "*" || pathComponents[end - 1] == "**"))
+ {
+ end--;
+ }
+ bool openEnd = (end < pathComponents.Length && pathComponents[end] == "**");
+ string[] outputComponents = new string[end - begin];
+ Array.Copy(pathComponents, begin, outputComponents, 0, end - begin);
+
+ return new TestExclusion(outputComponents, openEnd, issueId);
+ }
+ }
+}