Skip to content

Commit b4bda14

Browse files
authored
Dispose parameters, kill benchmarking process when it hangs after printing the results (#1571)
* add test * dispose unused dispsable parameters * stop the benchmark process output stream processing after receiving the last signal * don't try to read the standard error as it might also hang the benchmark process is catching all exceptions and writing to standard output, so it's OK (this is what the class Executor does already) * kill benchmarking process if it does not quit within 250ms after finishing benchmarking * exit code can be != 0 for benchmarks that have executed fine but hanged after printing the results * some benchmarks might be using parameters that have locking finalizers so we need to dispose them after we are done running the benchmarks see #1383 and dotnet/runtime#314 for more
1 parent 0f9bb33 commit b4bda14

File tree

11 files changed

+146
-33
lines changed

11 files changed

+146
-33
lines changed

src/BenchmarkDotNet/Helpers/ConsoleExitHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private void Detach()
5757
// the user has closed the console window so we kill the entire process tree
5858
private void ProcessExitEventHandlerHandlerCallback(object sender, EventArgs e) => KillProcessTree();
5959

60-
private void KillProcessTree()
60+
internal void KillProcessTree()
6161
{
6262
try
6363
{

src/BenchmarkDotNet/Loggers/SynchronousProcessOutputLoggerWithDiagnoser.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ internal void ProcessInput()
5555
{
5656
diagnoser?.Handle(signal, diagnoserActionParameters);
5757
process.StandardInput.WriteLine(Engine.Signals.Acknowledgment);
58+
59+
if (signal == HostSignal.AfterAll)
60+
{
61+
// we have received the last signal so we can stop reading the output
62+
// if the process won't exit after this, its hung and needs to be killed
63+
return;
64+
}
5865
}
5966
else if (!string.IsNullOrEmpty(line))
6067
{

src/BenchmarkDotNet/Parameters/ParameterInstance.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace BenchmarkDotNet.Parameters
1010
{
11-
public class ParameterInstance
11+
public class ParameterInstance : IDisposable
1212
{
1313
public const string NullParameterTextRepresentation = "?";
1414

@@ -24,6 +24,8 @@ public ParameterInstance(ParameterDefinition definition, object value, SummarySt
2424
maxParameterColumnWidth = summaryStyle?.MaxParameterColumnWidth ?? SummaryStyle.DefaultMaxParameterColumnWidth;
2525
}
2626

27+
public void Dispose() => (Value as IDisposable)?.Dispose();
28+
2729
public string Name => Definition.Name;
2830
public bool IsStatic => Definition.IsStatic;
2931
public bool IsArgument => Definition.IsArgument;

src/BenchmarkDotNet/Parameters/ParameterInstances.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using BenchmarkDotNet.Extensions;
45

56
namespace BenchmarkDotNet.Parameters
67
{
7-
public class ParameterInstances
8+
public class ParameterInstances : IDisposable
89
{
910
public IReadOnlyList<ParameterInstance> Items { get; }
1011
public int Count => Items.Count;
@@ -18,6 +19,14 @@ public ParameterInstances(IReadOnlyList<ParameterInstance> items)
1819
Items = items;
1920
}
2021

22+
public void Dispose()
23+
{
24+
foreach (var parameterInstance in Items)
25+
{
26+
parameterInstance.Dispose();
27+
}
28+
}
29+
2130
public string FolderInfo => string.Join("_", Items.Select(p => $"{p.Name}-{p.ToDisplayText()}")).AsValidFileName();
2231

2332
public string DisplayInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.ToDisplayText()}")) + "]" : "";

src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
24
using System.Diagnostics.CodeAnalysis;
35
using System.Linq;
46
using System.Reflection;
@@ -97,9 +99,8 @@ public string ToSourceCode()
9799
? $"[{argumentIndex}]" // IEnumerable<object[]>
98100
: string.Empty; // IEnumerable<object>
99101

100-
// we just execute (cast)source.ToArray()[case][argumentIndex];
101-
// we know that source is IEnumerable so we can do that!
102-
return $"{cast}System.Linq.Enumerable.ToArray({source.Name}{callPostfix})[{sourceIndex}]{indexPostfix};";
102+
// we do something like enumerable.ElementAt(sourceIndex)[argumentIndex];
103+
return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({source.Name}{callPostfix}, {sourceIndex}){indexPostfix};";
103104
}
104105
}
105106

@@ -129,8 +130,38 @@ public string ToSourceCode()
129130

130131
string callPostfix = source is PropertyInfo ? string.Empty : "()";
131132

132-
// we just execute (cast)source.ToArray()[index];
133-
return $"{cast}System.Linq.Enumerable.ToArray({instancePrefix}.{source.Name}{callPostfix})[{index}];";
133+
// we so something like enumerable.ElementAt(index);
134+
return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({instancePrefix}.{source.Name}{callPostfix}, {index});";
135+
}
136+
}
137+
138+
public static class ParameterExtractor
139+
{
140+
[EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public so we can call it form the boilerplate code
141+
public static T GetParameter<T>(IEnumerable<T> parameters, int index)
142+
{
143+
int count = 0;
144+
145+
foreach (T parameter in parameters)
146+
{
147+
if (count == index)
148+
{
149+
return parameter;
150+
}
151+
152+
if (parameter is IDisposable disposable)
153+
{
154+
// parameters might contain locking finalizers which might cause the benchmarking process to hung at the end
155+
// to avoid that, we dispose the parameters that were created, but won't be used
156+
// (for every test case we have to enumerate the underlying source enumerator and stop when we reach index of given test case)
157+
// See https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more
158+
disposable.Dispose();
159+
}
160+
161+
count++;
162+
}
163+
164+
throw new InvalidOperationException("We should never get here!");
134165
}
135166
}
136167
}

src/BenchmarkDotNet/Running/BenchmarkCase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace BenchmarkDotNet.Running
88
{
9-
public class BenchmarkCase : IComparable<BenchmarkCase>
9+
public class BenchmarkCase : IComparable<BenchmarkCase>, IDisposable
1010
{
1111
public Descriptor Descriptor { get; }
1212
public Job Job { get; }
@@ -26,6 +26,8 @@ private BenchmarkCase(Descriptor descriptor, Job job, ParameterInstances paramet
2626
Config = config;
2727
}
2828

29+
public void Dispose() => Parameters.Dispose();
30+
2931
public int CompareTo(BenchmarkCase other) => string.Compare(FolderInfo, other.FolderInfo, StringComparison.Ordinal);
3032

3133
public bool HasParameters => Parameters != null && Parameters.Items.Any();

src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace BenchmarkDotNet.Running
55
{
6-
public class BenchmarkRunInfo
6+
public class BenchmarkRunInfo : IDisposable
77
{
88
public BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConfig config)
99
{
@@ -12,6 +12,14 @@ public BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConf
1212
Config = config;
1313
}
1414

15+
public void Dispose()
16+
{
17+
foreach (var benchmarkCase in BenchmarksCases)
18+
{
19+
benchmarkCase.Dispose();
20+
}
21+
}
22+
1523
public BenchmarkCase[] BenchmarksCases { get; }
1624
public Type Type { get; }
1725
public ImmutableConfig Config { get; }

src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos)
107107
}
108108
finally
109109
{
110+
// some benchmarks might be using parameters that have locking finalizers
111+
// so we need to dispose them after we are done running the benchmarks
112+
// see https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more
113+
foreach (var benchmarkInfo in benchmarkRunInfos)
114+
{
115+
benchmarkInfo.Dispose();
116+
}
117+
110118
compositeLogger.WriteLineHeader("// * Artifacts cleanup *");
111119
Cleanup(new HashSet<string>(artifactsToCleanup.Distinct()));
112120
compositeLogger.Flush();
@@ -509,10 +517,12 @@ private static ExecuteResult RunExecute(ILogger logger, BenchmarkCase benchmarkC
509517
logger.WriteLineError($"Executable {buildResult.ArtifactsPaths.ExecutablePath} not found");
510518
}
511519

512-
if (executeResult.ExitCode != 0)
520+
// exit code can be different than 0 if the process has hanged at the end
521+
// so we check if some results were reported, if not then it was a failure
522+
if (executeResult.ExitCode != 0 && executeResult.Data.IsEmpty())
513523
{
514524
success = false;
515-
logger.WriteLineError("ExitCode != 0");
525+
logger.WriteLineError("ExitCode != 0 and no results reported");
516526
}
517527

518528
return executeResult;

src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase,
6969
startInfo.SetEnvironmentVariables(benchmarkCase, resolver);
7070

7171
using (var process = new Process { StartInfo = startInfo })
72-
using (new ConsoleExitHandler(process, logger))
72+
using (var consoleExitHandler = new ConsoleExitHandler(process, logger))
7373
{
7474
var loggerWithDiagnoser = new SynchronousProcessOutputLoggerWithDiagnoser(logger, process, diagnoser, benchmarkCase, benchmarkId);
7575

@@ -86,21 +86,15 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase,
8686
}
8787

8888
loggerWithDiagnoser.ProcessInput();
89-
string standardError = process.StandardError.ReadToEnd();
9089

91-
process.WaitForExit(); // should we add timeout here?
92-
93-
if (process.ExitCode == 0)
90+
if (!process.WaitForExit(milliseconds: 250))
9491
{
95-
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
96-
}
92+
logger.WriteLineInfo("// The benchmarking process did not quit on time, it's going to get force killed now.");
9793

98-
if (!string.IsNullOrEmpty(standardError))
99-
{
100-
logger.WriteError(standardError);
94+
consoleExitHandler.KillProcessTree();
10195
}
10296

103-
return new ExecuteResult(true, process.ExitCode, process.Id, Array.Empty<string>(), Array.Empty<string>());
97+
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
10498
}
10599
}
106100
}

src/BenchmarkDotNet/Toolchains/Executor.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmark
4040
try
4141
{
4242
using (var process = new Process { StartInfo = CreateStartInfo(benchmarkCase, artifactsPaths, args, resolver) })
43-
using (new ConsoleExitHandler(process, logger))
43+
using (var consoleExitHandler = new ConsoleExitHandler(process, logger))
4444
{
4545
var loggerWithDiagnoser = new SynchronousProcessOutputLoggerWithDiagnoser(logger, process, diagnoser, benchmarkCase, benchmarkId);
4646

4747
diagnoser?.Handle(HostSignal.BeforeProcessStart, new DiagnoserActionParameters(process, benchmarkCase, benchmarkId));
4848

49-
return Execute(process, benchmarkCase, loggerWithDiagnoser, logger);
49+
return Execute(process, benchmarkCase, loggerWithDiagnoser, logger, consoleExitHandler);
5050
}
5151
}
5252
finally
@@ -55,7 +55,7 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmark
5555
}
5656
}
5757

58-
private ExecuteResult Execute(Process process, BenchmarkCase benchmarkCase, SynchronousProcessOutputLoggerWithDiagnoser loggerWithDiagnoser, ILogger logger)
58+
private ExecuteResult Execute(Process process, BenchmarkCase benchmarkCase, SynchronousProcessOutputLoggerWithDiagnoser loggerWithDiagnoser, ILogger logger, ConsoleExitHandler consoleExitHandler)
5959
{
6060
logger.WriteLineInfo($"// Execute: {process.StartInfo.FileName} {process.StartInfo.Arguments} in {process.StartInfo.WorkingDirectory}");
6161

@@ -69,17 +69,17 @@ private ExecuteResult Execute(Process process, BenchmarkCase benchmarkCase, Sync
6969

7070
loggerWithDiagnoser.ProcessInput();
7171

72-
process.WaitForExit(); // should we add timeout here?
73-
74-
if (process.ExitCode == 0)
72+
if (!process.WaitForExit(milliseconds: 250))
7573
{
76-
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
74+
logger.WriteLineInfo("// The benchmarking process did not quit on time, it's going to get force killed now.");
75+
76+
consoleExitHandler.KillProcessTree();
7777
}
7878

7979
if (loggerWithDiagnoser.LinesWithResults.Any(line => line.Contains("BadImageFormatException")))
8080
logger.WriteLineError("You are probably missing <PlatformTarget>AnyCPU</PlatformTarget> in your .csproj file.");
8181

82-
return new ExecuteResult(true, process.ExitCode, process.Id, Array.Empty<string>(), Array.Empty<string>());
82+
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
8383
}
8484

8585
private ProcessStartInfo CreateStartInfo(BenchmarkCase benchmarkCase, ArtifactsPaths artifactsPaths, string args, IResolver resolver)

tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,56 @@ public void Test(string first, string second)
770770
}
771771
}
772772

773-
private void CanExecute<T>(IToolchain toolchain) => CanExecute<T>(CreateSimpleConfig(job: Job.Dry.With(toolchain)));
773+
[Fact]
774+
public void UnusedDisposableParamsAreDisposed() => CanExecute<WithDisposableArguments>(Job.Default.GetToolchain());
775+
776+
public class WithDisposableArguments
777+
{
778+
public IEnumerable<Disposable> GetDisposables()
779+
{
780+
yield return new Disposable(0);
781+
yield return new Disposable(1);
782+
}
783+
784+
[ParamsSource(nameof(GetDisposables))]
785+
public Disposable used;
786+
787+
[Benchmark]
788+
public void CheckDisposed()
789+
{
790+
if (used.Id == 0)
791+
{
792+
if (Disposable.Created != 1)
793+
throw new ArgumentException("Only one instance should be created so far!");
794+
if (Disposable.Disposed != 0)
795+
throw new ArgumentException("None should be disposed as only one was created and is still in use");
796+
}
797+
if (used.Id == 1)
798+
{
799+
if (Disposable.Created != 2)
800+
throw new ArgumentException("Two instances should be created so far!");
801+
if (Disposable.Disposed != 1)
802+
throw new ArgumentException("The first one should be disposed as it's not used");
803+
}
804+
}
805+
806+
public class Disposable : IDisposable
807+
{
808+
public static int Created = 0;
809+
public static int Disposed = 0;
810+
811+
public int Id { get; private set; }
812+
813+
public Disposable(int id)
814+
{
815+
Id = id;
816+
++Created;
817+
}
818+
819+
public void Dispose() => ++Disposed;
820+
}
821+
}
822+
823+
private void CanExecute<T>(IToolchain toolchain) => CanExecute<T>(CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)));
774824
}
775825
}

0 commit comments

Comments
 (0)