Skip to content

Commit 47b8b72

Browse files
authored
Avoid hangs when starting benchmark process fails (#2207)
* add test * add simple, but not 100% reliable fix * use a fix that does not add a delay to every benchmark
1 parent 90c82bb commit 47b8b72

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

src/BenchmarkDotNet/Loggers/Broker.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
45
using System.IO.Pipes;
6+
using System.Threading;
7+
using System.Threading.Tasks;
58
using BenchmarkDotNet.Diagnosers;
69
using BenchmarkDotNet.Engines;
710
using BenchmarkDotNet.Running;
@@ -11,28 +14,66 @@ namespace BenchmarkDotNet.Loggers
1114
internal class Broker
1215
{
1316
private readonly ILogger logger;
17+
private readonly Process process;
1418
private readonly IDiagnoser diagnoser;
1519
private readonly AnonymousPipeServerStream inputFromBenchmark, acknowledgments;
1620
private readonly DiagnoserActionParameters diagnoserActionParameters;
21+
private readonly ManualResetEvent finished;
1722

1823
public Broker(ILogger logger, Process process, IDiagnoser diagnoser,
1924
BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, AnonymousPipeServerStream inputFromBenchmark, AnonymousPipeServerStream acknowledgments)
2025
{
2126
this.logger = logger;
27+
this.process = process;
2228
this.diagnoser = diagnoser;
2329
this.inputFromBenchmark = inputFromBenchmark;
2430
this.acknowledgments = acknowledgments;
2531
diagnoserActionParameters = new DiagnoserActionParameters(process, benchmarkCase, benchmarkId);
32+
finished = new ManualResetEvent(false);
2633

2734
Results = new List<string>();
2835
PrefixedOutput = new List<string>();
36+
37+
process.EnableRaisingEvents = true;
38+
process.Exited += OnProcessExited;
2939
}
3040

3141
internal List<string> Results { get; }
3242

3343
internal List<string> PrefixedOutput { get; }
3444

3545
internal void ProcessData()
46+
{
47+
// When the process fails to start, there is no pipe to read from.
48+
// If we try to read from such pipe, the read blocks and BDN hangs.
49+
// We can't use async methods with cancellation tokens because Anonymous Pipes don't support async IO.
50+
51+
// Usually, this property is not set yet.
52+
if (process.HasExited)
53+
{
54+
return;
55+
}
56+
57+
Task.Run(ProcessDataBlocking);
58+
59+
finished.WaitOne();
60+
finished.Dispose();
61+
}
62+
63+
private void OnProcessExited(object sender, EventArgs e)
64+
{
65+
process.Exited -= OnProcessExited;
66+
67+
// Dispose all the pipes to let reading from pipe finish with EOF and avoid a reasource leak.
68+
inputFromBenchmark.DisposeLocalCopyOfClientHandle();
69+
inputFromBenchmark.Dispose();
70+
acknowledgments.DisposeLocalCopyOfClientHandle();
71+
acknowledgments.Dispose();
72+
73+
finished.Set();
74+
}
75+
76+
private void ProcessDataBlocking()
3677
{
3778
using StreamReader reader = new (inputFromBenchmark, AnonymousPipesHost.UTF8NoBOM, detectEncodingFromByteOrderMarks: false);
3879
using StreamWriter writer = new (acknowledgments, AnonymousPipesHost.UTF8NoBOM, bufferSize: 1);
@@ -67,6 +108,8 @@ internal void ProcessData()
67108
{
68109
// we have received the last signal so we can stop reading from the pipe
69110
// if the process won't exit after this, its hung and needs to be killed
111+
process.Exited -= OnProcessExited;
112+
finished.Set();
70113
return;
71114
}
72115
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Environments;
3+
using BenchmarkDotNet.Jobs;
4+
using BenchmarkDotNet.Portability;
5+
using System.Linq;
6+
using Xunit;
7+
using Xunit.Abstractions;
8+
9+
namespace BenchmarkDotNet.IntegrationTests
10+
{
11+
public class FailingProcessSpawnTests : BenchmarkTestExecutor
12+
{
13+
public FailingProcessSpawnTests(ITestOutputHelper output) : base(output)
14+
{
15+
}
16+
17+
[Fact]
18+
public void NoHangs()
19+
{
20+
Platform wrongPlatform = RuntimeInformation.GetCurrentPlatform() switch
21+
{
22+
Platform.X64 or Platform.X86 => Platform.Arm64,
23+
_ => Platform.X64
24+
};
25+
26+
var invalidPlatformJob = Job.Dry.WithPlatform(wrongPlatform);
27+
var config = CreateSimpleConfig(job: invalidPlatformJob);
28+
29+
var summary = CanExecute<Simple>(config, fullValidation: false);
30+
31+
var executeResults = summary.Reports.Single().ExecuteResults.Single();
32+
33+
Assert.True(executeResults.FoundExecutable);
34+
Assert.False(executeResults.IsSuccess);
35+
Assert.NotEqual(0, executeResults.ExitCode);
36+
}
37+
38+
public class Simple
39+
{
40+
[Benchmark]
41+
public void DoNothing() { }
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)