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

adding validation errors when the benchmarks are unsupported #2148

Conversation

emanuel-v-r
Copy link
Contributor

@emanuel-v-r emanuel-v-r commented Oct 13, 2022

Aims to fix #989, added a similar test to the one that is described in the issue.
After this PR, the summary will include validation errors and the output will look like this:

  Standard Output: 
// Benchmark BenchmarkAllCases.InvokeOnceVoid: Dry(Platform=X86, Toolchain=InProcessToolchain, InvocationCount=16, IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=16, WarmupCount=1)
// cannot be run in-process. Validation errors:
//    * Job Dry, EnvironmentMode.Platform was run as X64 (X86 expected). Fix your test runner options.

// Benchmark BenchmarkAllCases.InvokeOnceTaskAsync: Dry(Platform=X86, Toolchain=InProcessToolchain, InvocationCount=16, IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=16, WarmupCount=1)
// cannot be run in-process. Validation errors:
//    * Job Dry, EnvironmentMode.Platform was run as X64 (X86 expected). Fix your test runner options.

// Benchmark BenchmarkAllCases.InvokeOnceRefType: Dry(Platform=X86, Toolchain=InProcessToolchain, InvocationCount=16, IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=16, WarmupCount=1)
// cannot be run in-process. Validation errors:
//    * Job Dry, EnvironmentMode.Platform was run as X64 (X86 expected). Fix your test runner options.

// Benchmark BenchmarkAllCases.InvokeOnceValueType: Dry(Platform=X86, Toolchain=InProcessToolchain, InvocationCount=16, IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=16, WarmupCount=1)
// cannot be run in-process. Validation errors:
//    * Job Dry, EnvironmentMode.Platform was run as X64 (X86 expected). Fix your test runner options.

// Benchmark BenchmarkAllCases.InvokeOnceTaskOfTAsync: Dry(Platform=X86, Toolchain=InProcessToolchain, InvocationCount=16, IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=16, WarmupCount=1)
// cannot be run in-process. Validation errors:
//    * Job Dry, EnvironmentMode.Platform was run as X64 (X86 expected). Fix your test runner options.

// Benchmark BenchmarkAllCases.InvokeOnceValueTaskOfT: Dry(Platform=X86, Toolchain=InProcessToolchain, InvocationCount=16, IterationCount=1, LaunchCount=1, RunStrategy=ColdStart, UnrollFactor=16, WarmupCount=1)
// cannot be run in-process. Validation errors:
//    * Job Dry, EnvironmentMode.Platform was run as X64 (X86 expected). Fix your test runner options.

No suitable benchmarks were found

Although I believe it fits the purpose of what is described in the original issue, the error is not that specific, if we want to have something more specific we will have to introduce a new method in the IToolChain interface as the existing one returns only a bool https://github.com/dotnet/BenchmarkDotNet/blob/master/src/BenchmarkDotNet/Toolchains/IToolchain.cs#L16

@dnfadmin
Copy link

dnfadmin commented Oct 13, 2022

CLA assistant check
All CLA requirements met.

@YegorStepanov
Copy link
Contributor

Yep, we need a specific error message in summary.ValidationErrors.

The problem is that the benchmarks are filtered by Toolchain.IsSupported before the validation executes.

var supportedBenchmarks = GetSupportedBenchmarks(benchmarkRunInfos, compositeLogger, resolver); //filtering by Toolchain.IsSupported
if (!supportedBenchmarks.Any(benchmarks => benchmarks.BenchmarksCases.Any()))
    return new[] { Summary.NothingToRun(title, resultsFolderPath, logFilePath) };

var validationErrors = Validate(supportedBenchmarks, compositeLogger);
if (validationErrors.Any(validationError => validationError.IsCritical))
    return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors) };

if we want to have something more specific we will have to introduce a new method in the IToolChain interface as the existing one returns only a bool https://github.com/dotnet/BenchmarkDotNet/blob/master/src/BenchmarkDotNet/Toolchains/IToolchain.cs#L16

If we replace ILogger logger with List<ValidationError> validationErrors, we can do smth like this:

var supportedBenchmarks = GetSupportedBenchmarks(benchmarkRunInfos, resolver, out List<ValidationErrors> validationErrors);
validationErrors.AddRange(Validate(supportedBenchmarks, compositeLogger));

if (validationErrors.Any(validationError => validationError.IsCritical))
    return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors) };

if (!supportedBenchmarks.Any(benchmarks => benchmarks.BenchmarksCases.Any()))  // looks redundant, validators should check it
    return new[] { Summary.NothingToRun(title, resultsFolderPath, logFilePath) };

For the maintainers:
Validators are pretty bad at the moment, they need to be improved in some places. Someday I will write a "plan" issue. But the task is quite big.

@emanuel-v-r
Copy link
Contributor Author

emanuel-v-r commented Oct 14, 2022

Yep, we need a specific error message in summary.ValidationErrors.

The problem is that the benchmarks are filtered by Toolchain.IsSupported before the validation executes.

var supportedBenchmarks = GetSupportedBenchmarks(benchmarkRunInfos, compositeLogger, resolver); //filtering by Toolchain.IsSupported
if (!supportedBenchmarks.Any(benchmarks => benchmarks.BenchmarksCases.Any()))
    return new[] { Summary.NothingToRun(title, resultsFolderPath, logFilePath) };

var validationErrors = Validate(supportedBenchmarks, compositeLogger);
if (validationErrors.Any(validationError => validationError.IsCritical))
    return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors) };

if we want to have something more specific we will have to introduce a new method in the IToolChain interface as the existing one returns only a bool https://github.com/dotnet/BenchmarkDotNet/blob/master/src/BenchmarkDotNet/Toolchains/IToolchain.cs#L16

If we replace ILogger logger with List<ValidationError> validationErrors, we can do smth like this:

var supportedBenchmarks = GetSupportedBenchmarks(benchmarkRunInfos, resolver, out List<ValidationErrors> validationErrors);
validationErrors.AddRange(Validate(supportedBenchmarks, compositeLogger));

if (validationErrors.Any(validationError => validationError.IsCritical))
    return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors) };

if (!supportedBenchmarks.Any(benchmarks => benchmarks.BenchmarksCases.Any()))  // looks redundant, validators should check it
    return new[] { Summary.NothingToRun(title, resultsFolderPath, logFilePath) };

For the maintainers: Validators are pretty bad at the moment, they need to be improved in some places. Someday I will write a "plan" issue. But the task is quite big.

Yep, we need a specific error message in summary.ValidationErrors.

The problem is that the benchmarks are filtered by Toolchain.IsSupported before the validation executes.

var supportedBenchmarks = GetSupportedBenchmarks(benchmarkRunInfos, compositeLogger, resolver); //filtering by Toolchain.IsSupported
if (!supportedBenchmarks.Any(benchmarks => benchmarks.BenchmarksCases.Any()))
    return new[] { Summary.NothingToRun(title, resultsFolderPath, logFilePath) };

var validationErrors = Validate(supportedBenchmarks, compositeLogger);
if (validationErrors.Any(validationError => validationError.IsCritical))
    return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors) };

if we want to have something more specific we will have to introduce a new method in the IToolChain interface as the existing one returns only a bool https://github.com/dotnet/BenchmarkDotNet/blob/master/src/BenchmarkDotNet/Toolchains/IToolchain.cs#L16

If we replace ILogger logger with List<ValidationError> validationErrors, we can do smth like this:

var supportedBenchmarks = GetSupportedBenchmarks(benchmarkRunInfos, resolver, out List<ValidationErrors> validationErrors);
validationErrors.AddRange(Validate(supportedBenchmarks, compositeLogger));

if (validationErrors.Any(validationError => validationError.IsCritical))
    return new[] { Summary.ValidationFailed(title, resultsFolderPath, logFilePath, validationErrors) };

if (!supportedBenchmarks.Any(benchmarks => benchmarks.BenchmarksCases.Any()))  // looks redundant, validators should check it
    return new[] { Summary.NothingToRun(title, resultsFolderPath, logFilePath) };

For the maintainers: Validators are pretty bad at the moment, they need to be improved in some places. Someday I will write a "plan" issue. But the task is quite big.

Thanks for the feedback, changed accordingly here 524dbf9.
Notes:

Please let me know if that works for you

@emanuel-v-r emanuel-v-r force-pushed the validation-errors-for-unsupported-benchmarks branch 3 times, most recently from b08a45b to 3863545 Compare October 14, 2022 13:06
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emanuel-v-r big thanks for your contribution! PTAL at my comments

src/BenchmarkDotNet/Loggers/LoggerExtensions.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Reports/Summary.cs Outdated Show resolved Hide resolved
tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/IToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/IToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs Outdated Show resolved Hide resolved
@YegorStepanov
Copy link
Contributor

I guess this test won't contain validationError:

[Fact]
public void BenchmarkDifferentPlatformReturnsValidationError()
{
    var сonfig = new ManualConfig()
        .With(Job.Dry.With(InProcessToolchain.Instance).With(Platform.X86))
        .With(Job.Dry.With(InProcessToolchain.Instance).With(Platform.X64))
        .With(new OutputLogger(Output))
        .With(DefaultColumnProviders.Instance);

    var runInfo = BenchmarkConverter.TypeToBenchmarks(typeof(BenchmarkAllCases), сonfig);
    var summary = BenchmarkRunner.Run(runInfo);

    Assert.NotEmpty(summary.ValidationErrors);
}

Also, we need to add a specific reason to the validation errors.

To fix it we need change interface to it

public interface IToolchain                                                            
{                                                                                     
    [PublicAPI] string Name { get; }                                                   
    IGenerator Generator { get; }                                                      
    IBuilder Builder { get; }                                                          
    IExecutor Executor { get; }                                                        
    bool IsInProcess { get; }                                                          
                                                                                      
-   bool IsSupported(BenchmarkCase benchmarkCase, ILogger logger, IResolver resolver);
+   bool IsSupported(BenchmarkCase benchmarkCase, IResolver resolver, List<ValidationError> validationErrors);
}                                                                                     

and do it for each toolchain:

- logger.WriteLineError("The Roslyn toolchain is only supported on .NET Framework");
+ validationErrors.Add(new ValidationError(false, "The Roslyn toolchain is only supported on .NET Framework"));

But:

  1. We need approval from the maintainers to do it.
  2. Refactoring is desirable here, but it requires an understanding of the project's code
    2.1) For example: change List<ValidationError> to the add-only collection ValidationErrors in the whole project.

@emanuel-v-r emanuel-v-r force-pushed the validation-errors-for-unsupported-benchmarks branch from 1e54eb9 to bbc9544 Compare October 14, 2022 14:03
@emanuel-v-r
Copy link
Contributor Author

emanuel-v-r commented Oct 14, 2022

I guess this test won't contain validationError:

[Fact]
public void BenchmarkDifferentPlatformReturnsValidationError()
{
    var сonfig = new ManualConfig()
        .With(Job.Dry.With(InProcessToolchain.Instance).With(Platform.X86))
        .With(Job.Dry.With(InProcessToolchain.Instance).With(Platform.X64))
        .With(new OutputLogger(Output))
        .With(DefaultColumnProviders.Instance);

    var runInfo = BenchmarkConverter.TypeToBenchmarks(typeof(BenchmarkAllCases), сonfig);
    var summary = BenchmarkRunner.Run(runInfo);

    Assert.NotEmpty(summary.ValidationErrors);
}

Also, we need to add a specific reason to the validation errors.

To fix it we need change interface to it

public interface IToolchain                                                            
{                                                                                     
    [PublicAPI] string Name { get; }                                                   
    IGenerator Generator { get; }                                                      
    IBuilder Builder { get; }                                                          
    IExecutor Executor { get; }                                                        
    bool IsInProcess { get; }                                                          
                                                                                      
-   bool IsSupported(BenchmarkCase benchmarkCase, ILogger logger, IResolver resolver);
+   bool IsSupported(BenchmarkCase benchmarkCase, IResolver resolver, List<ValidationError> validationErrors);
}                                                                                     

and do it for each toolchain:

- logger.WriteLineError("The Roslyn toolchain is only supported on .NET Framework");
+ validationErrors.Add(new ValidationError(false, "The Roslyn toolchain is only supported on .NET Framework"));

But:

  1. We need approval from the maintainers to do it.
  2. Refactoring is desirable here, but it requires an understanding of the project's code
    2.1) For example: change List<ValidationError> to the add-only collection ValidationErrors in the whole project.

I was not able to replicate the issue using your test but I do understand what the issue is, nice catch thank you!
This commit should do the trick c45077d.
Also @adamsitnik thanks for your feedback, I addressed most of your comments, but I kept open some of them that I still have some doubts.
More specifically this one #2148 (comment).
Seems that we all agree that this validate method should not log/print anything, and therefore the logger should be removed.
The problem is that inside the toolchain implementations, we do have some logging/printing, and there's no existing generic approach for such error handling.
My suggestion is that we create a separated issue for handling this, as it could take some effort.
Anyway the changes that I have done in this PR will not affect such behavior.
I can take it myself as the next issue, as I already have some context.

EDIT:
I ended up removing logger dependency from toolchain.
Please @YegorStepanov @adamsitnik take a look.

src/BenchmarkDotNet/Loggers/LoggerExtensions.cs Outdated Show resolved Hide resolved
}

if (InvalidCliPath(customDotNetCliPath: null, benchmarkCase, logger))
return false;
{
var validationError = new ValidationError(true, $"InvalidCliPath, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", benchmarkCase);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use fast exit, collect all errors and return them.

It's better to display as much as possible errors.
If the user is not on windows AND the cli path is invalid, it displays only the first message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we want to check all the errors, or just fail fast here, anyway I kept the existing behavior, not sure if we want to change that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can open the validators folder, all of them may display multiple errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can open the validators folder, all of them may display multiple errors.

I mean toolchains errors, currently it only prints one error and exits as you can see in the previous code for "IsSupported" method
I am ok to change it as well, but I am trying to be objective here, and change only the necessary for what is described in the issue, and try not have side effects
Maybe @adamsitnik can provide his opinion here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of returning all errors instead the first one. 👍

Currently if there are two or more errors:

  1. The users tries to run the benchmarks, gets a single error and fixes it.
  2. The users tries to run the benchmarks, gets another error and fixes it.
  3. The user actually runs the benchmarks.

If we provide all errors at once we can reduce the number of steps needed and hence improve the UX.

src/BenchmarkDotNet/Validators/ValidationError.cs Outdated Show resolved Hide resolved
@emanuel-v-r emanuel-v-r force-pushed the validation-errors-for-unsupported-benchmarks branch from 4d594fc to 86e07f5 Compare October 15, 2022 12:53
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, it looks very good, but it would be great if you could change the validator behavior to return all known errors when possible. I know that the original issue did not mention it, but since you are improving the error handling, why not make it even better when we can?

Thank you @emanuel-v-r !

src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/Mono/MonoAotToolchain.cs Outdated Show resolved Hide resolved
src/BenchmarkDotNet/Toolchains/CoreRun/CoreRunToolchain.cs Outdated Show resolved Hide resolved
}

if (InvalidCliPath(customDotNetCliPath: null, benchmarkCase, logger))
return false;
{
var validationError = new ValidationError(true, $"InvalidCliPath, benchmark '{benchmarkCase.DisplayInfo}' will not be executed", benchmarkCase);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of returning all errors instead the first one. 👍

Currently if there are two or more errors:

  1. The users tries to run the benchmarks, gets a single error and fixes it.
  2. The users tries to run the benchmarks, gets another error and fixes it.
  3. The user actually runs the benchmarks.

If we provide all errors at once we can reduce the number of steps needed and hence improve the UX.

@emanuel-v-r emanuel-v-r force-pushed the validation-errors-for-unsupported-benchmarks branch 2 times, most recently from ffa1224 to 5953396 Compare October 17, 2022 16:47
* Replace IsSupported method by Validate which returns the errors instead of only a bool
* Extract printing logic from tool chains
* Remove logger dependency from IToolChain

Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
@emanuel-v-r emanuel-v-r force-pushed the validation-errors-for-unsupported-benchmarks branch from 5953396 to e3ed1e1 Compare October 17, 2022 17:31
@emanuel-v-r
Copy link
Contributor Author

emanuel-v-r commented Oct 17, 2022

Overall, it looks very good, but it would be great if you could change the validator behavior to return all known errors when possible. I know that the original issue did not mention it, but since you are improving the error handling, why not make it even better when we can?

Thank you @emanuel-v-r !

Thank you @adamsitnik , applied your suggestions, and also changed the validate method in the other toolchains so that it returns multiple erros.

@@ -136,7 +136,7 @@ private static Summary RunWithExceptionHandling(Func<Summary> run)
catch (InvalidBenchmarkDeclarationException e)
{
ConsoleLogger.Default.WriteLineError(e.Message);
return Summary.NothingToRun(e.Message, string.Empty, string.Empty);
return Summary.ValidationFailed(e.Message, string.Empty, string.Empty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a little weird here. Maybe find a better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestion? Maybe just Summary.Failed?

Co-authored-by: Yegor Stepanov <yegor.stepanov@outlook.com>
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you very much for your contribution @emanuel-v-r !


namespace BenchmarkDotNet.Toolchains.MonoWasm
{
[PublicAPI]
public class WasmToolChain : Toolchain
public class WasmToolchain : Toolchain
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming public types is considered to be a breaking change and we avoid doing that if we can. However, this particular type is most likely not used directly by anyone, so it's OK-ish ;)

@@ -50,6 +51,7 @@ public override int GetHashCode()
}
}


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: redundant empty lone

@adamsitnik adamsitnik added this to the v0.13.3 milestone Oct 17, 2022
@adamsitnik adamsitnik merged commit 28bf214 into dotnet:master Oct 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Suggestion] add API for detecting benchmark run failures.
5 participants