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

Dispose parameters, kill benchmarking process when it hangs after printing the results #1571

Merged
merged 7 commits into from
Oct 26, 2020
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Helpers/ConsoleExitHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private void Detach()
// the user has closed the console window so we kill the entire process tree
private void ProcessExitEventHandlerHandlerCallback(object sender, EventArgs e) => KillProcessTree();

private void KillProcessTree()
internal void KillProcessTree()
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ internal void ProcessInput()
{
diagnoser?.Handle(signal, diagnoserActionParameters);
process.StandardInput.WriteLine(Engine.Signals.Acknowledgment);

if (signal == HostSignal.AfterAll)
{
// we have received the last signal so we can stop reading the output
// if the process won't exit after this, its hung and needs to be killed
return;
}
}
else if (!string.IsNullOrEmpty(line))
{
Expand Down
4 changes: 3 additions & 1 deletion src/BenchmarkDotNet/Parameters/ParameterInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace BenchmarkDotNet.Parameters
{
public class ParameterInstance
public class ParameterInstance : IDisposable
{
public const string NullParameterTextRepresentation = "?";

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

public void Dispose() => (Value as IDisposable)?.Dispose();

public string Name => Definition.Name;
public bool IsStatic => Definition.IsStatic;
public bool IsArgument => Definition.IsArgument;
Expand Down
13 changes: 11 additions & 2 deletions src/BenchmarkDotNet/Parameters/ParameterInstances.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Extensions;

namespace BenchmarkDotNet.Parameters
{
public class ParameterInstances
public class ParameterInstances : IDisposable
{
public IReadOnlyList<ParameterInstance> Items { get; }
public int Count => Items.Count;
Expand All @@ -18,6 +19,14 @@ public ParameterInstances(IReadOnlyList<ParameterInstance> items)
Items = items;
}

public void Dispose()
{
foreach (var parameterInstance in Items)
{
parameterInstance.Dispose();
}
}

public string FolderInfo => string.Join("_", Items.Select(p => $"{p.Name}-{p.ToDisplayText()}")).AsValidFileName();

public string DisplayInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.ToDisplayText()}")) + "]" : "";
Expand Down
41 changes: 36 additions & 5 deletions src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -97,9 +99,8 @@ public string ToSourceCode()
? $"[{argumentIndex}]" // IEnumerable<object[]>
: string.Empty; // IEnumerable<object>

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

Expand Down Expand Up @@ -129,8 +130,38 @@ public string ToSourceCode()

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

// we just execute (cast)source.ToArray()[index];
return $"{cast}System.Linq.Enumerable.ToArray({instancePrefix}.{source.Name}{callPostfix})[{index}];";
// we so something like enumerable.ElementAt(index);
return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({instancePrefix}.{source.Name}{callPostfix}, {index});";
}
}

public static class ParameterExtractor
{
[EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public so we can call it form the boilerplate code
public static T GetParameter<T>(IEnumerable<T> parameters, int index)
{
int count = 0;

foreach (T parameter in parameters)
{
if (count == index)
{
return parameter;
}

if (parameter is IDisposable disposable)
{
// parameters might contain locking finalizers which might cause the benchmarking process to hung at the end
// to avoid that, we dispose the parameters that were created, but won't be used
// (for every test case we have to enumerate the underlying source enumerator and stop when we reach index of given test case)
// See https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more
disposable.Dispose();
}

count++;
}

throw new InvalidOperationException("We should never get here!");
}
}
}
4 changes: 3 additions & 1 deletion src/BenchmarkDotNet/Running/BenchmarkCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace BenchmarkDotNet.Running
{
public class BenchmarkCase : IComparable<BenchmarkCase>
public class BenchmarkCase : IComparable<BenchmarkCase>, IDisposable
{
public Descriptor Descriptor { get; }
public Job Job { get; }
Expand All @@ -26,6 +26,8 @@ private BenchmarkCase(Descriptor descriptor, Job job, ParameterInstances paramet
Config = config;
}

public void Dispose() => Parameters.Dispose();

public int CompareTo(BenchmarkCase other) => string.Compare(FolderInfo, other.FolderInfo, StringComparison.Ordinal);

public bool HasParameters => Parameters != null && Parameters.Items.Any();
Expand Down
10 changes: 9 additions & 1 deletion src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace BenchmarkDotNet.Running
{
public class BenchmarkRunInfo
public class BenchmarkRunInfo : IDisposable
{
public BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConfig config)
{
Expand All @@ -12,6 +12,14 @@ public BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConf
Config = config;
}

public void Dispose()
{
foreach (var benchmarkCase in BenchmarksCases)
{
benchmarkCase.Dispose();
}
}

public BenchmarkCase[] BenchmarksCases { get; }
public Type Type { get; }
public ImmutableConfig Config { get; }
Expand Down
14 changes: 12 additions & 2 deletions src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos)
}
finally
{
// some benchmarks might be using parameters that have locking finalizers
// so we need to dispose them after we are done running the benchmarks
// see https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more
foreach (var benchmarkInfo in benchmarkRunInfos)
{
benchmarkInfo.Dispose();
}

compositeLogger.WriteLineHeader("// * Artifacts cleanup *");
Cleanup(new HashSet<string>(artifactsToCleanup.Distinct()));
compositeLogger.Flush();
Expand Down Expand Up @@ -501,10 +509,12 @@ private static ExecuteResult RunExecute(ILogger logger, BenchmarkCase benchmarkC
logger.WriteLineError($"Executable {buildResult.ArtifactsPaths.ExecutablePath} not found");
}

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

return executeResult;
Expand Down
16 changes: 5 additions & 11 deletions src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase,
startInfo.SetEnvironmentVariables(benchmarkCase, resolver);

using (var process = new Process { StartInfo = startInfo })
using (new ConsoleExitHandler(process, logger))
using (var consoleExitHandler = new ConsoleExitHandler(process, logger))
{
var loggerWithDiagnoser = new SynchronousProcessOutputLoggerWithDiagnoser(logger, process, diagnoser, benchmarkCase, benchmarkId);

Expand All @@ -86,21 +86,15 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase,
}

loggerWithDiagnoser.ProcessInput();
string standardError = process.StandardError.ReadToEnd();

process.WaitForExit(); // should we add timeout here?

if (process.ExitCode == 0)
if (!process.WaitForExit(milliseconds: 250))
{
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
}
logger.WriteLineInfo("// The benchmarking process did not quit on time, it's going to get force killed now.");

if (!string.IsNullOrEmpty(standardError))
{
logger.WriteError(standardError);
consoleExitHandler.KillProcessTree();
}

return new ExecuteResult(true, process.ExitCode, process.Id, Array.Empty<string>(), Array.Empty<string>());
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/BenchmarkDotNet/Toolchains/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmark
try
{
using (var process = new Process { StartInfo = CreateStartInfo(benchmarkCase, artifactsPaths, args, resolver) })
using (new ConsoleExitHandler(process, logger))
using (var consoleExitHandler = new ConsoleExitHandler(process, logger))
{
var loggerWithDiagnoser = new SynchronousProcessOutputLoggerWithDiagnoser(logger, process, diagnoser, benchmarkCase, benchmarkId);

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

return Execute(process, benchmarkCase, loggerWithDiagnoser, logger);
return Execute(process, benchmarkCase, loggerWithDiagnoser, logger, consoleExitHandler);
}
}
finally
Expand All @@ -55,7 +55,7 @@ private ExecuteResult Execute(BenchmarkCase benchmarkCase, BenchmarkId benchmark
}
}

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

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

loggerWithDiagnoser.ProcessInput();

process.WaitForExit(); // should we add timeout here?

if (process.ExitCode == 0)
if (!process.WaitForExit(milliseconds: 250))
{
return new ExecuteResult(true, process.ExitCode, process.Id, loggerWithDiagnoser.LinesWithResults, loggerWithDiagnoser.LinesWithExtraOutput);
logger.WriteLineInfo("// The benchmarking process did not quit on time, it's going to get force killed now.");

consoleExitHandler.KillProcessTree();
}

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

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

private ProcessStartInfo CreateStartInfo(BenchmarkCase benchmarkCase, ArtifactsPaths artifactsPaths, string args, IResolver resolver)
Expand Down
52 changes: 51 additions & 1 deletion tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,56 @@ public void Test(string first, string second)
}
}

private void CanExecute<T>(IToolchain toolchain) => CanExecute<T>(CreateSimpleConfig(job: Job.Dry.With(toolchain)));
[Fact]
public void UnusedDisposableParamsAreDisposed() => CanExecute<WithDisposableArguments>(Job.Default.GetToolchain());

public class WithDisposableArguments
{
public IEnumerable<Disposable> GetDisposables()
{
yield return new Disposable(0);
yield return new Disposable(1);
}

[ParamsSource(nameof(GetDisposables))]
public Disposable used;

[Benchmark]
public void CheckDisposed()
{
if (used.Id == 0)
{
if (Disposable.Created != 1)
throw new ArgumentException("Only one instance should be created so far!");
if (Disposable.Disposed != 0)
throw new ArgumentException("None should be disposed as only one was created and is still in use");
}
if (used.Id == 1)
{
if (Disposable.Created != 2)
throw new ArgumentException("Two instances should be created so far!");
if (Disposable.Disposed != 1)
throw new ArgumentException("The first one should be disposed as it's not used");
}
}

public class Disposable : IDisposable
{
public static int Created = 0;
public static int Disposed = 0;

public int Id { get; private set; }

public Disposable(int id)
{
Id = id;
++Created;
}

public void Dispose() => ++Disposed;
}
}

private void CanExecute<T>(IToolchain toolchain) => CanExecute<T>(CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)));
}
}