diff --git a/tests/src/tools/ReadyToRun.SuperIlc/CompileDirectoryCommand.cs b/tests/src/tools/ReadyToRun.SuperIlc/CompileDirectoryCommand.cs index bab34456912..03a725d3987 100644 --- a/tests/src/tools/ReadyToRun.SuperIlc/CompileDirectoryCommand.cs +++ b/tests/src/tools/ReadyToRun.SuperIlc/CompileDirectoryCommand.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; @@ -13,6 +14,9 @@ class CompileDirectoryCommand { public static int CompileDirectory(DirectoryInfo toolDirectory, DirectoryInfo inputDirectory, DirectoryInfo outputDirectory, bool crossgen, bool cpaot, DirectoryInfo[] referencePath) { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + if (toolDirectory == null) { Console.WriteLine("--tool-directory is a required argument."); @@ -27,8 +31,7 @@ public static int CompileDirectory(DirectoryInfo toolDirectory, DirectoryInfo in if (outputDirectory == null) { - Console.WriteLine("--output-directory is a required argument."); - return 1; + outputDirectory = inputDirectory; } if (OutputPathIsParentOfInputPath(inputDirectory, outputDirectory)) @@ -47,11 +50,12 @@ public static int CompileDirectory(DirectoryInfo toolDirectory, DirectoryInfo in runner = new CrossgenRunner(toolDirectory.ToString(), inputDirectory.ToString(), outputDirectory.ToString(), referencePath?.Select(x => x.ToString())?.ToList()); } - if (outputDirectory.Exists) + string runnerOutputPath = runner.GetOutputPath(); + if (Directory.Exists(runnerOutputPath)) { try { - outputDirectory.Delete(recursive: true); + Directory.Delete(runnerOutputPath, recursive: true); } catch (Exception ex) when ( ex is UnauthorizedAccessException @@ -59,44 +63,51 @@ ex is UnauthorizedAccessException || ex is IOException ) { - Console.WriteLine($"Error: Could not delete output folder {outputDirectory.FullName}. {ex.Message}"); + Console.WriteLine($"Error: Could not delete output folder {runnerOutputPath}. {ex.Message}"); return 1; } } - outputDirectory.Create(); + Directory.CreateDirectory(runnerOutputPath); - bool success = true; - List failedCompilationAssemblies = new List(); - int successfulCompileCount = 0; + List compilationsToRun = new List(); // Copy unmanaged files (runtime, native dependencies, resources, etc) foreach (string file in Directory.EnumerateFiles(inputDirectory.FullName)) { if (ComputeManagedAssemblies.IsManaged(file)) { - // Compile managed code - if (runner.CompileAssembly(file)) - { - ++successfulCompileCount; - } - else - { - success = false; - failedCompilationAssemblies.Add(file); - - // On compile failure, pass through the input IL assembly so the output is still usable - File.Copy(file, Path.Combine(outputDirectory.FullName, Path.GetFileName(file))); - } + ProcessInfo compilationToRun = runner.CompilationProcess(file); + compilationToRun.InputFileName = file; + compilationsToRun.Add(compilationToRun); } else { // Copy through all other files - File.Copy(file, Path.Combine(outputDirectory.FullName, Path.GetFileName(file))); + File.Copy(file, Path.Combine(runnerOutputPath, Path.GetFileName(file))); + } + } + + ParallelRunner.Run(compilationsToRun); + + bool success = true; + List failedCompilationAssemblies = new List(); + int successfulCompileCount = 0; + + foreach (ProcessInfo processInfo in compilationsToRun) + { + if (processInfo.Succeeded) + { + successfulCompileCount++; + } + else + { + File.Copy(processInfo.InputFileName, Path.Combine(runnerOutputPath, Path.GetFileName(processInfo.InputFileName))); + failedCompilationAssemblies.Add(processInfo.InputFileName); } } - Console.WriteLine($"Compiled {successfulCompileCount}/{successfulCompileCount + failedCompilationAssemblies.Count} assemblies."); + Console.WriteLine($"Compiled {successfulCompileCount}/{successfulCompileCount + failedCompilationAssemblies.Count} assemblies in {stopwatch.ElapsedMilliseconds} msecs."); if (failedCompilationAssemblies.Count > 0) { @@ -112,9 +123,6 @@ ex is UnauthorizedAccessException static bool OutputPathIsParentOfInputPath(DirectoryInfo inputPath, DirectoryInfo outputPath) { - if (inputPath == outputPath) - return true; - DirectoryInfo parentInfo = inputPath.Parent; while (parentInfo != null) { @@ -122,7 +130,6 @@ static bool OutputPathIsParentOfInputPath(DirectoryInfo inputPath, DirectoryInfo return true; parentInfo = parentInfo.Parent; - } return false; diff --git a/tests/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs b/tests/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs index 3b3be82e6d2..659385f9f9e 100644 --- a/tests/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs +++ b/tests/src/tools/ReadyToRun.SuperIlc/CompilerRunner.cs @@ -25,49 +25,30 @@ public CompilerRunner(string compilerFolder, string inputFolder, string outputFo protected abstract string CompilerFileName {get;} protected abstract IEnumerable BuildCommandLineArguments(string assemblyFileName, string outputFileName); - public bool CompileAssembly(string assemblyFileName) + public ProcessInfo CompilationProcess(string assemblyFileName) { CreateOutputFolder(); + string outputFileName = GetOutputFileName(assemblyFileName); string responseFile = GetResponseFileName(assemblyFileName); var commandLineArgs = BuildCommandLineArguments(assemblyFileName, outputFileName); CreateResponseFile(responseFile, commandLineArgs); - using (var process = new Process()) - { - process.StartInfo.FileName = Path.Combine(_compilerPath, CompilerFileName); - process.StartInfo.Arguments = $"@{responseFile}"; - process.StartInfo.UseShellExecute = false; - - process.Start(); - - process.OutputDataReceived += delegate (object sender, DataReceivedEventArgs args) - { - Console.WriteLine(args.Data); - }; - - process.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs args) - { - Console.WriteLine(args.Data); - }; - - process.WaitForExit(); + ProcessInfo processInfo = new ProcessInfo(); + processInfo.ProcessPath = Path.Combine(_compilerPath, CompilerFileName); + processInfo.Arguments = $"@{responseFile}"; + processInfo.UseShellExecute = false; + processInfo.LogPath = Path.ChangeExtension(outputFileName, ".log"); - if (process.ExitCode != 0) - { - Console.WriteLine($"Compilation of {Path.GetFileName(assemblyFileName)} failed with exit code {process.ExitCode}"); - return false; - } - } - - return true; + return processInfo; } protected void CreateOutputFolder() { - if (!Directory.Exists(_outputPath)) + string outputPath = GetOutputPath(); + if (!Directory.Exists(outputPath)) { - Directory.CreateDirectory(_outputPath); + Directory.CreateDirectory(outputPath); } } @@ -82,9 +63,13 @@ protected void CreateResponseFile(string responseFile, IEnumerable comma } } + public string GetOutputPath() => + Path.Combine(_outputPath, Path.GetFileNameWithoutExtension(CompilerFileName)); + // \a.dll -> \a.dll - protected string GetOutputFileName(string assemblyFileName) => - Path.Combine(_outputPath, $"{Path.GetFileName(assemblyFileName)}"); - protected string GetResponseFileName(string assemblyFileName) => - Path.Combine(_outputPath, $"{Path.GetFileNameWithoutExtension(assemblyFileName)}.{Path.GetFileNameWithoutExtension(CompilerFileName)}.rsp"); + public string GetOutputFileName(string fileName) => + Path.Combine(GetOutputPath(), $"{Path.GetFileName(fileName)}"); + + public string GetResponseFileName(string assemblyFileName) => + Path.Combine(GetOutputPath(), Path.GetFileNameWithoutExtension(assemblyFileName) + ".rsp"); } diff --git a/tests/src/tools/ReadyToRun.SuperIlc/ParallelRunner.cs b/tests/src/tools/ReadyToRun.SuperIlc/ParallelRunner.cs new file mode 100644 index 00000000000..8025774b57a --- /dev/null +++ b/tests/src/tools/ReadyToRun.SuperIlc/ParallelRunner.cs @@ -0,0 +1,164 @@ +// 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; + +/// +/// Execute a given number of mutually independent build subprocesses represented by an array of +/// command lines with a given degree of parallelization. +/// +public sealed class ParallelRunner +{ + /// + /// 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. + /// + class ProcessSlot + { + /// + /// Process slot index (used for diagnostic purposes) + /// + readonly int _slotIndex; + + /// + /// Event used to report that a process has exited + /// + readonly AutoResetEvent _processExitEvent; + + /// + /// Process object + /// + ProcessRunner _processRunner; + + /// + /// Constructor stores global slot parameters and initializes the slot state machine + /// + /// Process slot index used for diagnostic purposes + /// Event used to report process exit + public ProcessSlot(int slotIndex, AutoResetEvent processExitEvent) + { + _slotIndex = slotIndex; + _processExitEvent = processExitEvent; + } + + /// + /// Launch a new process. + /// + /// application to execute + /// Numeric index used to prefix messages pertaining to this process in the console output + public void Launch(ProcessInfo processInfo, int processIndex) + { + Debug.Assert(_processRunner == null); + Console.WriteLine($"{processIndex}: launching: {processInfo.ProcessPath} {processInfo.Arguments}"); + + _processRunner = new ProcessRunner(processInfo, processIndex, _processExitEvent); + } + + public bool IsAvailable() + { + if (_processRunner == null) + { + return true; + } + if (!_processRunner.IsAvailable()) + { + return false; + } + _processRunner.Dispose(); + _processRunner = null; + return true; + } + } + + /// + /// Execute a given set of mutually independent build commands with the default + /// degree of parallelism. + /// + /// Processes to execute in parallel + public static void Run(IEnumerable processesToRun) + { + Run(processesToRun, degreeOfParallelism: Environment.ProcessorCount); + } + + /// + /// Execute a given set of mutually independent build commands with given degree of + /// parallelism. + /// + /// Processes to execute in parallel + /// Maximum number of processes to execute in parallel + public static void Run(IEnumerable processesToRun, int degreeOfParallelism) + { + int processCount = processesToRun.Count(); + if (processCount < degreeOfParallelism) + { + // We never need a higher DOP than the number of process to execute + degreeOfParallelism = processCount; + } + + 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 processIndex = 0; + foreach (ProcessInfo processInfo in processesToRun) + { + // 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()) + { + freeSlot = slot; + break; + } + } + if (freeSlot == null) + { + // All slots are busy - wait for a process to finish + processExitEvent.WaitOne(); + } + } + while (freeSlot == null); + + freeSlot.Launch(processInfo, ++processIndex); + } + + // 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()) + { + activeProcessesExist = true; + } + } + if (activeProcessesExist) + { + processExitEvent.WaitOne(); + } + } + while (activeProcessesExist); + } + } +} diff --git a/tests/src/tools/ReadyToRun.SuperIlc/ProcessRunner.cs b/tests/src/tools/ReadyToRun.SuperIlc/ProcessRunner.cs new file mode 100644 index 00000000000..bf9e543afce --- /dev/null +++ b/tests/src/tools/ReadyToRun.SuperIlc/ProcessRunner.cs @@ -0,0 +1,241 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +public class ProcessInfo +{ + /// + /// 10 minutes should be plenty for a CPAOT / Crossgen compilation. + /// + public const int DefaultTimeout = 600 * 1000; + + public string ProcessPath; + public string Arguments; + public bool UseShellExecute; + public string LogPath; + public int TimeoutMilliseconds = DefaultTimeout; + public int ExpectedExitCode; + public string InputFileName; + + public bool Finished; + public bool Succeeded; + public bool TimedOut; + public int DurationMilliseconds; + public int ExitCode; +} + +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 Process _process; + + private readonly Stopwatch _stopwatch; + + /// + /// This is actually a boolean flag but we're using int to let us use CPU-native interlocked exchange. + /// + private int _state; + + private TextWriter _logWriter; + + private CancellationTokenSource _cancellationTokenSource; + + public ProcessRunner(ProcessInfo processInfo, int processIndex, AutoResetEvent processExitEvent) + { + _processInfo = processInfo; + _processIndex = processIndex; + _processExitEvent = processExitEvent; + + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + _cancellationTokenSource = cancellationTokenSource; + + _stopwatch = new Stopwatch(); + _stopwatch.Start(); + _state = StateIdle; + + _logWriter = new StreamWriter(_processInfo.LogPath); + + if (_processInfo.ProcessPath.Contains(' ')) + { + _logWriter.Write($"\"{_processInfo.ProcessPath}\""); + } + else + { + _logWriter.Write(_processInfo.ProcessPath); + } + _logWriter.Write(' '); + _logWriter.WriteLine(_processInfo.Arguments); + _logWriter.WriteLine("<<<<"); + + ProcessStartInfo psi = new ProcessStartInfo() + { + FileName = _processInfo.ProcessPath, + Arguments = _processInfo.Arguments, + UseShellExecute = _processInfo.UseShellExecute, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + _process = new Process(); + _process.StartInfo = psi; + _process.EnableRaisingEvents = true; + _process.Exited += new EventHandler(ExitEventHandler); + + Interlocked.Exchange(ref _state, StateRunning); + + _process.Start(); + + _process.OutputDataReceived += new DataReceivedEventHandler(StandardOutputEventHandler); + _process.BeginOutputReadLine(); + + _process.ErrorDataReceived += new DataReceivedEventHandler(StandardErrorEventHandler); + _process.BeginErrorReadLine(); + + Task.Run(() => + { + try + { + Task.Delay(_processInfo.TimeoutMilliseconds, cancellationTokenSource.Token).Wait(); + StopProcessAtomic(); + } + catch (TaskCanceledException) + { + // Ignore cancellation + } + }); + } + + public void Dispose() + { + CleanupProcess(); + } + + private void CleanupProcess() + { + if (_cancellationTokenSource != null) + { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource = null; + } + + if (_process != null) + { + _process.Dispose(); + _process = null; + } + + 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) + { + return; + } + + _cancellationTokenSource.Cancel(); + + _processInfo.DurationMilliseconds = (int)_stopwatch.ElapsedMilliseconds; + + bool success; + if (_process.WaitForExit(0)) + { + _process.WaitForExit(); + _processInfo.ExitCode = _process.ExitCode; + success = (_processInfo.ExitCode == _processInfo.ExpectedExitCode); + _logWriter.WriteLine(">>>>"); + if (success) + { + _logWriter.WriteLine($"Succeeded in {_processInfo.DurationMilliseconds} msecs, exit code {_processInfo.ExitCode}"); + Console.WriteLine( + $"{_processIndex}: succeeded in {_processInfo.DurationMilliseconds} msecs; " + + $"exit code {_processInfo.ExitCode}: {_processInfo.ProcessPath} {_processInfo.Arguments}"); + _processInfo.Succeeded = true; + } + else + { + _logWriter.WriteLine($"Failed in {_processInfo.DurationMilliseconds} msecs, exit code {_processInfo.ExitCode}, expected {_processInfo.ExpectedExitCode}"); + Console.Error.WriteLine( + $"{_processIndex}: failed in {_processInfo.DurationMilliseconds} msecs; " + + $"exit code {_processInfo.ExitCode}, expected{_processInfo.ExpectedExitCode}: " + + $"{_processInfo.ProcessPath} {_processInfo.Arguments}"); + } + } + else + { + _process.Kill(); + _process.WaitForExit(); + _processInfo.ExitCode = TimeoutExitCode; + _processInfo.TimedOut = true; + success = false; + _logWriter.WriteLine(">>>>"); + _logWriter.WriteLine($"Timed out in {_processInfo.DurationMilliseconds} msecs"); + Console.Error.WriteLine( + $"{_processIndex}: timed out in {_processInfo.DurationMilliseconds} msecs: " + + $"{_processInfo.ProcessPath} {_processInfo.Arguments}"); + } + + _processInfo.Finished = true; + + _logWriter.Flush(); + _logWriter.Close(); + + CleanupProcess(); + + Interlocked.Exchange(ref _state, StateIdle); + _processExitEvent?.Set(); + } + + private void StandardOutputEventHandler(object sender, DataReceivedEventArgs eventArgs) + { + if (!string.IsNullOrEmpty(eventArgs.Data)) + { + _logWriter.WriteLine(eventArgs.Data); + Console.Out.WriteLine(_processIndex.ToString() + ": " + eventArgs.Data); + } + } + + private void StandardErrorEventHandler(object sender, DataReceivedEventArgs eventArgs) + { + if (!string.IsNullOrEmpty(eventArgs.Data)) + { + _logWriter.WriteLine(eventArgs.Data); + Console.Error.WriteLine(_processIndex.ToString() + ": " + eventArgs.Data); + } + } + + public bool IsAvailable() + { + return _state == StateIdle; + } +}