Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Pipes for host and benchmark process communication #2092

Merged
merged 23 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c528ab1
use Named Pipes for inter process communication
adamsitnik Aug 28, 2022
2cb73c0
checking whether diagnoser requires blocking acknowledgments is not n…
adamsitnik Aug 28, 2022
3f54c1a
use NamedPipeClientStream on Unix
adamsitnik Aug 29, 2022
54bfb75
dont use NamedPipeClient as its unsupported on WASM, use mkfifo and t…
adamsitnik Aug 29, 2022
4f746bf
fix Windows implementation
adamsitnik Aug 29, 2022
f4b5b78
use Anonymous Pipes
adamsitnik Aug 29, 2022
30f7337
we can't use pipes on WASM
adamsitnik Sep 12, 2022
1883b52
ensure benchmark process output is redirected, read the output in non…
adamsitnik Sep 12, 2022
862525b
WasmExecutor needs to log output as well
adamsitnik Sep 12, 2022
60a5e01
Merge branch 'master' into namedPipes
adamsitnik Sep 12, 2022
55156a8
the tests should be using .StandardOutput property now
adamsitnik Sep 13, 2022
fcd5594
remove tests covered by AllSetupAndCleanupTest
adamsitnik Sep 13, 2022
b479681
remove tests covered by SetupAndCleanupTests
adamsitnik Sep 13, 2022
72be7dd
remove test that has been disabled for years
adamsitnik Sep 13, 2022
6895d1e
remove test covered by ArgumentsTests
adamsitnik Sep 13, 2022
418ba0b
fold InnerClassTest into ValuesReturnedByBenchmarkTest
adamsitnik Sep 13, 2022
ee8971c
fold ParamsAttributeStaticTest into ParamsTests, simplify the tests
adamsitnik Sep 13, 2022
cc6031d
CanExecute ensures that at least one benchmark has executed successfu…
adamsitnik Sep 13, 2022
1cb4a85
lock OutputLogger to see if it helps with flaky tests
adamsitnik Sep 13, 2022
581098a
use CancelRead when process does not exit on time, disable FileStream…
adamsitnik Sep 13, 2022
691d11a
start reading the output as soon as process is started
adamsitnik Sep 14, 2022
5394383
Before the last signal is reported and the benchmark process exits,
adamsitnik Sep 14, 2022
3906381
FSharp tests: don't use standard output for benchmark failure verific…
adamsitnik Sep 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ public void DisplayResults(ILogger logger)
logger.WriteLineInfo("DO remember that this Diagnoser just tries to mimic the CVCollectionCmd.exe and you need to have Visual Studio with Concurrency Visualizer plugin installed to visualize the data.");
}

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => true;

public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
etwProfiler.Handle(signal, parameters);
Expand Down
1 change: 0 additions & 1 deletion src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ namespace BenchmarkDotNet.Diagnostics.Windows
protected readonly Dictionary<BenchmarkCase, int> BenchmarkToProcess = new Dictionary<BenchmarkCase, int>();
protected readonly ConcurrentDictionary<int, TStats> StatsPerProcess = new ConcurrentDictionary<int, TStats>();

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => true;
public virtual RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.ExtraRun;

public virtual IEnumerable<IExporter> Exporters => Array.Empty<IExporter>();
Expand Down
2 changes: 0 additions & 2 deletions src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ public EtwProfiler(EtwProfilerConfig config)
public IEnumerable<ValidationError> Validate(ValidationParameters validationParameters)
=> HardwareCounters.Validate(validationParameters, mandatory: false);

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => false;

public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
// it's crucial to start the trace before the process starts and stop it after the benchmarked process stops to have all of the necessary events in the trace file!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ public void DisplayResults(ILogger resultLogger)
resultLogger.Write(line.Kind, line.Text);
}

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => etwProfiler.RequiresBlockingAcknowledgments(benchmarkCase);

public void Handle(HostSignal signal, DiagnoserActionParameters parameters) => etwProfiler.Handle(signal, parameters);

public RunMode GetRunMode(BenchmarkCase benchmarkCase) => etwProfiler.GetRunMode(benchmarkCase);
Expand Down
3 changes: 0 additions & 3 deletions src/BenchmarkDotNet/Code/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ internal static string Generate(BuildPartition buildPartition)
else if (buildPartition.IsWasm)
extraDefines.Add("#define WASM");

if (buildPartition.NoAcknowledgments)
extraDefines.Add("#define NO_ACK");

string benchmarkProgramContent = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkProgram.txt"))
.Replace("$ShadowCopyDefines$", useShadowCopy ? "#define SHADOWCOPY" : null).Replace("$ShadowCopyFolderPath$", shadowCopyFolderPath)
.Replace("$ExtraDefines$", string.Join(Environment.NewLine, extraDefines))
Expand Down
3 changes: 0 additions & 3 deletions src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ public IEnumerable<IExporter> Exporters
public IEnumerable<IAnalyser> Analysers
=> diagnosers.SelectMany(diagnoser => diagnoser.Analysers);

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase)
=> diagnosers.Any(diagnoser => diagnoser.RequiresBlockingAcknowledgments(benchmarkCase));

public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
foreach (var diagnoser in diagnosers)
Expand Down
2 changes: 0 additions & 2 deletions src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ public IEnumerable<ValidationError> Validate(ValidationParameters validationPara
}
}

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => true;

public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
if (signal != HostSignal.BeforeAnythingElse)
Expand Down
2 changes: 0 additions & 2 deletions src/BenchmarkDotNet/Diagnosers/IDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ public interface IDiagnoser

RunMode GetRunMode(BenchmarkCase benchmarkCase);

bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase);

void Handle(HostSignal signal, DiagnoserActionParameters parameters);

IEnumerable<Metric> ProcessResults(DiagnoserResults results);
Expand Down
1 change: 0 additions & 1 deletion src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public void DisplayResults(ILogger logger) { }
public IEnumerable<ValidationError> Validate(ValidationParameters validationParameters) => Array.Empty<ValidationError>();

// the action takes places in other process, and the values are gathered by Engine
public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => false;
public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { }

public IEnumerable<Metric> ProcessResults(DiagnoserResults diagnoserResults)
Expand Down
2 changes: 0 additions & 2 deletions src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ public void DisplayResults(ILogger logger) { }

public RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.NoOverhead;

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => false;

public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { }

public IEnumerable<Metric> ProcessResults(DiagnoserResults results)
Expand Down
1 change: 0 additions & 1 deletion src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public class UnresolvedDiagnoser : IDiagnoser
public IEnumerable<string> Ids => Array.Empty<string>();
public IEnumerable<IExporter> Exporters => Array.Empty<IExporter>();
public IEnumerable<IAnalyser> Analysers => Array.Empty<IAnalyser>();
public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => false;
public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { }
public IEnumerable<Metric> ProcessResults(DiagnoserResults _) => Array.Empty<Metric>();

Expand Down
2 changes: 0 additions & 2 deletions src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ public RunMode GetRunMode(BenchmarkCase benchmarkCase)
return RunMode.None;
}

public bool RequiresBlockingAcknowledgments(BenchmarkCase benchmarkCase) => GetRunMode(benchmarkCase) != RunMode.None;

public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
var benchmark = parameters.BenchmarkCase;
Expand Down
79 changes: 79 additions & 0 deletions src/BenchmarkDotNet/Engines/AnonymousPipesHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using BenchmarkDotNet.Validators;
using System;
using System.IO;
using System.Text;
using Microsoft.Win32.SafeHandles;
using JetBrains.Annotations;

namespace BenchmarkDotNet.Engines
{
public class AnonymousPipesHost : IHost
{
internal const string AnonymousPipesDescriptors = "--anonymousPipes";
internal static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

private readonly StreamWriter outWriter;
private readonly StreamReader inReader;

public AnonymousPipesHost(string writHandle, string readHandle)
{
outWriter = new StreamWriter(OpenAnonymousPipe(writHandle, FileAccess.Write), UTF8NoBOM);
// Flush the data to the Stream after each write, otherwise the host process will wait for input endlessly!
outWriter.AutoFlush = true;
inReader = new StreamReader(OpenAnonymousPipe(readHandle, FileAccess.Read), UTF8NoBOM, detectEncodingFromByteOrderMarks: false);
}

private Stream OpenAnonymousPipe(string fileHandle, FileAccess access)
=> new FileStream(new SafeFileHandle(new IntPtr(int.Parse(fileHandle)), ownsHandle: true), access, bufferSize: 1);

public void Dispose()
{
outWriter.Dispose();
inReader.Dispose();
}

public void Write(string message) => outWriter.Write(message);

public void WriteLine() => outWriter.WriteLine();

public void WriteLine(string message) => outWriter.WriteLine(message);

public void SendSignal(HostSignal hostSignal)
{
if (hostSignal == HostSignal.AfterAll)
{
// Before the last signal is reported and the benchmark process exits,
// add an artificial sleep to increase the chance of host process reading all std output.
System.Threading.Thread.Sleep(1);
}

WriteLine(Engine.Signals.ToMessage(hostSignal));

// read the response from Parent process, make the communication blocking
string acknowledgment = inReader.ReadLine();
if (acknowledgment != Engine.Signals.Acknowledgment)
throw new NotSupportedException($"Unknown Acknowledgment: {acknowledgment}");
}

public void SendError(string message) => outWriter.WriteLine($"{ValidationErrorReporter.ConsoleErrorPrefix} {message}");

public void ReportResults(RunResults runResults) => runResults.Print(outWriter);

[PublicAPI] // called from generated code
public static bool TryGetFileHandles(string[] args, out string writeHandle, out string readHandle)
{
for (int i = 0; i < args.Length; i++)
{
if (args[i] == AnonymousPipesDescriptors)
{
writeHandle = args[i + 1]; // IndexOutOfRangeException means a bug (incomplete data)
readHandle = args[i + 2];
return true;
}
}

writeHandle = readHandle = null;
return false;
}
}
}
41 changes: 0 additions & 41 deletions src/BenchmarkDotNet/Engines/ConsoleHost.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Engines/GcStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace BenchmarkDotNet.Engines
{
public struct GcStats : IEquatable<GcStats>
{
internal const string ResultsLinePrefix = "GC: ";
internal const string ResultsLinePrefix = "// GC: ";

public static readonly long AllocationQuantum = CalculateAllocationQuantumSize();

Expand Down
5 changes: 3 additions & 2 deletions src/BenchmarkDotNet/Engines/IHost.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;

namespace BenchmarkDotNet.Engines
{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public interface IHost
public interface IHost : IDisposable
{
void Write(string message);
void WriteLine();
Expand Down
14 changes: 7 additions & 7 deletions src/BenchmarkDotNet/Engines/NoAcknowledgementConsoleHost.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
using System;
using System.IO;
using BenchmarkDotNet.Validators;
using JetBrains.Annotations;

namespace BenchmarkDotNet.Engines
{
/* This class is used by wasm because wasm cannot read from the console.
* This potentially breaks communication with Diagnosers. */
// this class is used only when somebody manually launches benchmarking .exe without providing anonymous pipes file descriptors
public sealed class NoAcknowledgementConsoleHost : IHost
{
private readonly TextWriter outWriter;

public NoAcknowledgementConsoleHost([NotNull]TextWriter outWriter)
{
this.outWriter = outWriter ?? throw new ArgumentNullException(nameof(outWriter));
}
public NoAcknowledgementConsoleHost() => outWriter = Console.Out;

public void Write(string message) => outWriter.Write(message);

Expand All @@ -27,5 +22,10 @@ public NoAcknowledgementConsoleHost([NotNull]TextWriter outWriter)
public void SendError(string message) => outWriter.WriteLine($"{ValidationErrorReporter.ConsoleErrorPrefix} {message}");

public void ReportResults(RunResults runResults) => runResults.Print(outWriter);

public void Dispose()
{
// do nothing on purpose - there is no point in closing STD OUT
}
}
}
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Engines/ThreadingStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace BenchmarkDotNet.Engines
{
public struct ThreadingStats : IEquatable<ThreadingStats>
{
internal const string ResultsLinePrefix = "Threading: ";
internal const string ResultsLinePrefix = "// Threading: ";

// BDN targets .NET Standard 2.0, these properties are not part of .NET Standard 2.0, were added in .NET Core 3.0
private static readonly Func<long> GetCompletedWorkItemCountDelegate = CreateGetterDelegate(typeof(ThreadPool), nameof(CompletedWorkItemCount));
Expand Down
25 changes: 17 additions & 8 deletions src/BenchmarkDotNet/Loggers/AsyncProcessOutputReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ internal class AsyncProcessOutputReader : IDisposable
{
private readonly Process process;
private readonly ConcurrentQueue<string> output, error;
private readonly bool logOutput, readStandardError;
private readonly ILogger logger;

private long status;
private bool logOutput;
private ILogger logger;

internal AsyncProcessOutputReader(Process process, bool logOutput = false, ILogger logger = null)
internal AsyncProcessOutputReader(Process process, bool logOutput = false, ILogger logger = null, bool readStandardError = true)
{
if (!process.StartInfo.RedirectStandardOutput)
throw new NotSupportedException("set RedirectStandardOutput to true first");
if (!process.StartInfo.RedirectStandardError)
if (readStandardError && !process.StartInfo.RedirectStandardError)
throw new NotSupportedException("set RedirectStandardError to true first");
if (logOutput && logger == null)
throw new ArgumentException($"{nameof(logger)} cannot be null when {nameof(logOutput)} is true");
Expand All @@ -31,6 +31,7 @@ internal AsyncProcessOutputReader(Process process, bool logOutput = false, ILogg
status = (long)Status.Created;
this.logOutput = logOutput;
this.logger = logger;
this.readStandardError = readStandardError;
}

public void Dispose()
Expand All @@ -48,7 +49,9 @@ internal void BeginRead()
Attach();

process.BeginOutputReadLine();
process.BeginErrorReadLine();

if (readStandardError)
process.BeginErrorReadLine();
}

internal void CancelRead()
Expand All @@ -57,7 +60,9 @@ internal void CancelRead()
throw new InvalidOperationException("Only a started reader can be stopped");

process.CancelOutputRead();
process.CancelErrorRead();

if (readStandardError)
process.CancelErrorRead();

Detach();
}
Expand All @@ -83,13 +88,17 @@ internal void StopRead()
private void Attach()
{
process.OutputDataReceived += ProcessOnOutputDataReceived;
process.ErrorDataReceived += ProcessOnErrorDataReceived;

if (readStandardError)
process.ErrorDataReceived += ProcessOnErrorDataReceived;
}

private void Detach()
{
process.OutputDataReceived -= ProcessOnOutputDataReceived;
process.ErrorDataReceived -= ProcessOnErrorDataReceived;

if (readStandardError)
process.ErrorDataReceived -= ProcessOnErrorDataReceived;
}

private void ProcessOnOutputDataReceived(object sender, DataReceivedEventArgs e)
Expand Down
Loading