Move the ReadyToRun.SuperIlc tool to CoreCLR
authordotnet-bot <dotnet-bot@microsoft.com>
Wed, 11 Sep 2019 13:40:40 +0000 (15:40 +0200)
committerTomáš Rylek <trylek@microsoft.com>
Thu, 12 Sep 2019 12:05:36 +0000 (05:05 -0700)
After Michal moved the Crossgen2 compiler source code to CoreCLR,
this testing tool is the last bit that only exists in CoreRT so
I'm moving it too. This initial commit amounts to a straightforward
copy of SuperIlc source code from the CoreRT repo.

Thanks

Tomas

Commit migrated from https://github.com/dotnet/coreclr/commit/5f04a3d720be9bd673c874c73517d4322934a10b

23 files changed:
src/coreclr/src/tools/ReadyToRun.SuperIlc/Buckets.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildFolder.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildFolderSet.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildOptions.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/CommandLineOptions.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileDirectoryCommand.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileFromCrossgenRspCommand.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileNugetCommand.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileSubtreeCommand.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/ComputeManagedAssemblies.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/CpaotRunner.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/CrossgenRunner.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/DotnetCli.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/JitRunner.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/Linux.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/ParallelRunner.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/PathHelpers.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/ProcessRunner.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/Program.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/ReadyToRun.SuperIlc.csproj [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/ReadyToRunJittedMethods.cs [new file with mode: 0644]
src/coreclr/src/tools/ReadyToRun.SuperIlc/TestExclusion.cs [new file with mode: 0644]

diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Buckets.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Buckets.cs
new file mode 100644 (file)
index 0000000..c95d589
--- /dev/null
@@ -0,0 +1,185 @@
+// 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;
+            }
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildFolder.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildFolder.cs
new file mode 100644 (file)
index 0000000..7841a66
--- /dev/null
@@ -0,0 +1,251 @@
+// 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;
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildFolderSet.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildFolderSet.cs
new file mode 100644 (file)
index 0000000..e8259f0
--- /dev/null
@@ -0,0 +1,1338 @@
+// 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;
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildOptions.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/BuildOptions.cs
new file mode 100644 (file)
index 0000000..ac3c404
--- /dev/null
@@ -0,0 +1,119 @@
+// 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;
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/CommandLineOptions.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/CommandLineOptions.cs
new file mode 100644 (file)
index 0000000..c803af4
--- /dev/null
@@ -0,0 +1,193 @@
+// 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());
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileDirectoryCommand.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileDirectoryCommand.cs
new file mode 100644 (file)
index 0000000..4602598
--- /dev/null
@@ -0,0 +1,56 @@
+// 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;
+        }
+    }    
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileFromCrossgenRspCommand.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileFromCrossgenRspCommand.cs
new file mode 100644 (file)
index 0000000..508ddc6
--- /dev/null
@@ -0,0 +1,246 @@
+// 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();
+            }
+        }
+    }    
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileNugetCommand.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileNugetCommand.cs
new file mode 100644 (file)
index 0000000..5015700
--- /dev/null
@@ -0,0 +1,100 @@
+// 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);
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileSubtreeCommand.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Commands/CompileSubtreeCommand.cs
new file mode 100644 (file)
index 0000000..723578e
--- /dev/null
@@ -0,0 +1,134 @@
+// 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);
+            }
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs
new file mode 100644 (file)
index 0000000..899e32a
--- /dev/null
@@ -0,0 +1,318 @@
+// 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);
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/ComputeManagedAssemblies.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/ComputeManagedAssemblies.cs
new file mode 100644 (file)
index 0000000..94dd078
--- /dev/null
@@ -0,0 +1,71 @@
+// 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;
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/CpaotRunner.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/CpaotRunner.cs
new file mode 100644 (file)
index 0000000..5ed300a
--- /dev/null
@@ -0,0 +1,85 @@
+// 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;
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/CrossgenRunner.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/CrossgenRunner.cs
new file mode 100644 (file)
index 0000000..a9517c7
--- /dev/null
@@ -0,0 +1,55 @@
+// 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();
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/DotnetCli.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/DotnetCli.cs
new file mode 100644 (file)
index 0000000..99b8de2
--- /dev/null
@@ -0,0 +1,91 @@
+// 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;
+            }
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/JitRunner.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/JitRunner.cs
new file mode 100644 (file)
index 0000000..17f93a8
--- /dev/null
@@ -0,0 +1,47 @@
+// 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();
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Linux.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Linux.cs
new file mode 100644 (file)
index 0000000..595e85d
--- /dev/null
@@ -0,0 +1,51 @@
+// 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}");
+            }
+        }
+    }
+
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/ParallelRunner.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/ParallelRunner.cs
new file mode 100644 (file)
index 0000000..206f29c
--- /dev/null
@@ -0,0 +1,262 @@
+// 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);
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/PathHelpers.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/PathHelpers.cs
new file mode 100644 (file)
index 0000000..2caf53d
--- /dev/null
@@ -0,0 +1,281 @@
+// 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;
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/ProcessRunner.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/ProcessRunner.cs
new file mode 100644 (file)
index 0000000..845d6b3
--- /dev/null
@@ -0,0 +1,349 @@
+// 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.
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/Program.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/Program.cs
new file mode 100644 (file)
index 0000000..1bdbaa2
--- /dev/null
@@ -0,0 +1,21 @@
+// 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);
+        }
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/ReadyToRun.SuperIlc.csproj b/src/coreclr/src/tools/ReadyToRun.SuperIlc/ReadyToRun.SuperIlc.csproj
new file mode 100644 (file)
index 0000000..94a6e83
--- /dev/null
@@ -0,0 +1,19 @@
+<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>
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/ReadyToRunJittedMethods.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/ReadyToRunJittedMethods.cs
new file mode 100644 (file)
index 0000000..a08639e
--- /dev/null
@@ -0,0 +1,152 @@
+// 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;
+    }
+}
diff --git a/src/coreclr/src/tools/ReadyToRun.SuperIlc/TestExclusion.cs b/src/coreclr/src/tools/ReadyToRun.SuperIlc/TestExclusion.cs
new file mode 100644 (file)
index 0000000..3afcdc5
--- /dev/null
@@ -0,0 +1,229 @@
+// 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);
+        }
+    }
+}