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

Improve enum parsing in config binder generator #88873

Closed
layomia opened this issue Jul 13, 2023 · 3 comments · Fixed by #89823
Closed

Improve enum parsing in config binder generator #88873

layomia opened this issue Jul 13, 2023 · 3 comments · Fixed by #89823
Assignees
Labels
area-Extensions-Configuration help wanted [up-for-grabs] Good issue for external contributors source-generator Indicates an issue with a source generator feature
Milestone

Comments

@layomia
Copy link
Contributor

layomia commented Jul 13, 2023

from @eerhardt in #88067:

Looking at your current generated code:

    public static ConsoleLoggerFormat ParseConsoleLoggerFormat(string value, Func<string?> getPath)
    {
        try
        {
            return (ConsoleLoggerFormat)Enum.Parse(typeof(ConsoleLoggerFormat), value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }

There are a couple comments here:

  1. Why do we need a new method for every enum type in the graph? Can't we write 1 general "ParseEnum" method, like this one?
  2. We really should use Enum.Parse<T> when possible. I did a quick benchmark:
using BenchmarkDotNet.Attributes;

namespace Benchmark;

public enum ConsoleLoggerFormat
{
    Default,
    Systemd,
}

[MemoryDiagnoser]
public class EnumParseBenchmark
{
    [Benchmark]
    public ConsoleLoggerFormat ParseConsoleLoggerFormatGeneric() => ParseConsoleLoggerFormatGeneric("Systemd", () => "");
    [Benchmark]
    public ConsoleLoggerFormat ParseConsoleLoggerFormat() => ParseConsoleLoggerFormat("Systemd", () => "");

    public static ConsoleLoggerFormat ParseConsoleLoggerFormatGeneric(string value, Func<string?> getPath)
    {
        try
        {
            return Enum.Parse<ConsoleLoggerFormat>(value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }

    public static ConsoleLoggerFormat ParseConsoleLoggerFormat(string value, Func<string?> getPath)
    {
        try
        {
            return (ConsoleLoggerFormat)Enum.Parse(typeof(ConsoleLoggerFormat), value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }
}

on my machine I see:

Method Mean Error StdDev Gen 0 Allocated
ParseConsoleLoggerFormatGeneric 27.92 ns 0.855 ns 0.985 ns - -
ParseConsoleLoggerFormat 72.91 ns 0.270 ns 0.226 ns 0.0036 24 B

The generic version is faster and doesn't allocate.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jul 13, 2023
@layomia layomia self-assigned this Jul 13, 2023
@ghost
Copy link

ghost commented Jul 13, 2023

Tagging subscribers to this area: @dotnet/area-extensions-logging
See info in area-owners.md if you want to be subscribed.

Issue Details

from @eerhardt in #88067:

Looking at your current generated code:

    public static ConsoleLoggerFormat ParseConsoleLoggerFormat(string value, Func<string?> getPath)
    {
        try
        {
            return (ConsoleLoggerFormat)Enum.Parse(typeof(ConsoleLoggerFormat), value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }

There are a couple comments here:

  1. Why do we need a new method for every enum type in the graph? Can't we write 1 general "ParseEnum" method, like this one?
  2. We really should use Enum.Parse<T> when possible. I did a quick benchmark:
using BenchmarkDotNet.Attributes;

namespace Benchmark;

public enum ConsoleLoggerFormat
{
    Default,
    Systemd,
}

[MemoryDiagnoser]
public class EnumParseBenchmark
{
    [Benchmark]
    public ConsoleLoggerFormat ParseConsoleLoggerFormatGeneric() => ParseConsoleLoggerFormatGeneric("Systemd", () => "");
    [Benchmark]
    public ConsoleLoggerFormat ParseConsoleLoggerFormat() => ParseConsoleLoggerFormat("Systemd", () => "");

    public static ConsoleLoggerFormat ParseConsoleLoggerFormatGeneric(string value, Func<string?> getPath)
    {
        try
        {
            return Enum.Parse<ConsoleLoggerFormat>(value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }

    public static ConsoleLoggerFormat ParseConsoleLoggerFormat(string value, Func<string?> getPath)
    {
        try
        {
            return (ConsoleLoggerFormat)Enum.Parse(typeof(ConsoleLoggerFormat), value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }
}

on my machine I see:

Method Mean Error StdDev Gen 0 Allocated
ParseConsoleLoggerFormatGeneric 27.92 ns 0.855 ns 0.985 ns - -
ParseConsoleLoggerFormat 72.91 ns 0.270 ns 0.226 ns 0.0036 24 B

The generic version is faster and doesn't allocate.

Author: layomia
Assignees: -
Labels:

area-Extensions-Logging

Milestone: -

@ghost
Copy link

ghost commented Jul 13, 2023

Tagging subscribers to this area: @dotnet/area-extensions-configuration
See info in area-owners.md if you want to be subscribed.

Issue Details

from @eerhardt in #88067:

Looking at your current generated code:

    public static ConsoleLoggerFormat ParseConsoleLoggerFormat(string value, Func<string?> getPath)
    {
        try
        {
            return (ConsoleLoggerFormat)Enum.Parse(typeof(ConsoleLoggerFormat), value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }

There are a couple comments here:

  1. Why do we need a new method for every enum type in the graph? Can't we write 1 general "ParseEnum" method, like this one?
  2. We really should use Enum.Parse<T> when possible. I did a quick benchmark:
using BenchmarkDotNet.Attributes;

namespace Benchmark;

public enum ConsoleLoggerFormat
{
    Default,
    Systemd,
}

[MemoryDiagnoser]
public class EnumParseBenchmark
{
    [Benchmark]
    public ConsoleLoggerFormat ParseConsoleLoggerFormatGeneric() => ParseConsoleLoggerFormatGeneric("Systemd", () => "");
    [Benchmark]
    public ConsoleLoggerFormat ParseConsoleLoggerFormat() => ParseConsoleLoggerFormat("Systemd", () => "");

    public static ConsoleLoggerFormat ParseConsoleLoggerFormatGeneric(string value, Func<string?> getPath)
    {
        try
        {
            return Enum.Parse<ConsoleLoggerFormat>(value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }

    public static ConsoleLoggerFormat ParseConsoleLoggerFormat(string value, Func<string?> getPath)
    {
        try
        {
            return (ConsoleLoggerFormat)Enum.Parse(typeof(ConsoleLoggerFormat), value, ignoreCase: true);
        }
        catch (Exception exception)
        {
            throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ConsoleLoggerFormat)}'.", exception);
        }
    }
}

on my machine I see:

Method Mean Error StdDev Gen 0 Allocated
ParseConsoleLoggerFormatGeneric 27.92 ns 0.855 ns 0.985 ns - -
ParseConsoleLoggerFormat 72.91 ns 0.270 ns 0.226 ns 0.0036 24 B

The generic version is faster and doesn't allocate.

Author: layomia
Assignees: layomia
Labels:

untriaged, area-Extensions-Configuration

Milestone: -

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jul 13, 2023
@layomia layomia added the source-generator Indicates an issue with a source generator feature label Jul 13, 2023
@layomia
Copy link
Contributor Author

layomia commented Jul 21, 2023

Will keep this assigned to me; but happy to take a community contribution here.

@layomia layomia added the help wanted [up-for-grabs] Good issue for external contributors label Jul 21, 2023
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Aug 1, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Aug 2, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Sep 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Extensions-Configuration help wanted [up-for-grabs] Good issue for external contributors source-generator Indicates an issue with a source generator feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant