diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Internal.BatchProcessorBenchmarks-report-github.md similarity index 100% rename from benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md rename to benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Internal.BatchProcessorBenchmarks-report-github.md diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Internal.LazyBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Internal.LazyBenchmarks-report-github.md new file mode 100644 index 0000000000..e5636e5ea0 --- /dev/null +++ b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Internal.LazyBenchmarks-report-github.md @@ -0,0 +1,14 @@ +``` + +BenchmarkDotNet v0.15.8, macOS Tahoe 26.5.1 (25F80) [Darwin 25.5.0] +Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores +.NET SDK 10.0.301 + [Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), Arm64 RyuJIT armv8.0-a + DefaultJob : .NET 10.0.9 (10.0.9, 10.0.926.27113), Arm64 RyuJIT armv8.0-a + + +``` +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------- |---------:|----------:|----------:|-------:|----------:| +| System_Lazy | 6.909 ns | 0.0807 ns | 0.0715 ns | 0.0048 | 40 B | +| Sentry_Internal_LazyLite | 1.006 ns | 0.0102 ns | 0.0085 ns | - | - | diff --git a/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs b/benchmarks/Sentry.Benchmarks/Internal/BatchProcessorBenchmarks.cs similarity index 98% rename from benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs rename to benchmarks/Sentry.Benchmarks/Internal/BatchProcessorBenchmarks.cs index 51e0394ee1..77be6c1671 100644 --- a/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs +++ b/benchmarks/Sentry.Benchmarks/Internal/BatchProcessorBenchmarks.cs @@ -2,7 +2,7 @@ using Sentry.Extensibility; using Sentry.Internal; -namespace Sentry.Benchmarks; +namespace Sentry.Benchmarks.Internal; /// /// (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only. diff --git a/benchmarks/Sentry.Benchmarks/Internal/LazyBenchmarks.cs b/benchmarks/Sentry.Benchmarks/Internal/LazyBenchmarks.cs new file mode 100644 index 0000000000..760ce3771d --- /dev/null +++ b/benchmarks/Sentry.Benchmarks/Internal/LazyBenchmarks.cs @@ -0,0 +1,44 @@ +#nullable enable + +using BenchmarkDotNet.Attributes; +using Sentry.Internal; + +namespace Sentry.Benchmarks.Internal; + +public class LazyBenchmarks +{ + private static string Text = null!; + + [GlobalSetup] + public void Setup() + { + Text = "Factory"; + } + + [Benchmark] + public (char, char) System_Lazy() + { + var lazy = new Lazy(ValueFactory, LazyThreadSafetyMode.None); + + var first = lazy.Value; + var second = lazy.Value; + + return (first, second); + } + + [Benchmark] + public (char, char) Sentry_Internal_LazyLite() + { + var lazy = new LazyLite(ValueFactory); + + var first = lazy.Value; + var second = lazy.Value; + + return (first, second); + } + + private static char ValueFactory() + { + return Text[^1]; + } +} diff --git a/benchmarks/Sentry.Benchmarks/README.md b/benchmarks/Sentry.Benchmarks/README.md index 73da241446..b50dd7598f 100644 --- a/benchmarks/Sentry.Benchmarks/README.md +++ b/benchmarks/Sentry.Benchmarks/README.md @@ -3,5 +3,5 @@ To run benchmarks in a single class: ```shell-script -dotnet run -c Release -- --filter *StackFrameBenchmarks* +dotnet run -c Release -- --filter "*StackFrameBenchmarks*" ``` diff --git a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs index 63cbd5f0d1..82dd1fc247 100644 --- a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs @@ -179,7 +179,7 @@ public async Task InvokeAsync(HttpContext context) // If no Name was found for Transaction, then we don't have the route. // Also, run this block if the caller has opted into always calling the TransactionNameProvider. - var customName = new Lazy(context.TryGetCustomTransactionName, LazyThreadSafetyMode.None); + var customName = new LazyLite(context.TryGetCustomTransactionName); var forceCustomName = _options.PreferTransactionNameProvider && customName.Value is not null; if (transaction.Name == string.Empty || forceCustomName) { diff --git a/src/Sentry/Internal/LazyLite.cs b/src/Sentry/Internal/LazyLite.cs new file mode 100644 index 0000000000..ae46a0ecf0 --- /dev/null +++ b/src/Sentry/Internal/LazyLite.cs @@ -0,0 +1,38 @@ +namespace Sentry.Internal; + +/// +/// A -based variant of that is not thread safe. +/// +/// We're using this for local variables, where no is required, +/// but a value is still cached, without any allocations via the -based . +/// +/// +/// This type is not thread safe. +/// +internal struct LazyLite +{ + private Func? _factory; + private T? _value; + + public LazyLite(Func? valueFactory) + { + _factory = valueFactory; + _value = default; + } + + public readonly bool IsValueCreated => _factory is null; + + public T Value => _factory is null ? _value! : CreateValue(); + + private T CreateValue() + { + var factory = _factory; + if (factory is not null) + { + _factory = null; + _value = factory(); + } + + return _value!; + } +} diff --git a/test/Sentry.Tests/Internals/LazyLiteTests.cs b/test/Sentry.Tests/Internals/LazyLiteTests.cs new file mode 100644 index 0000000000..cd4cf3aa4e --- /dev/null +++ b/test/Sentry.Tests/Internals/LazyLiteTests.cs @@ -0,0 +1,92 @@ +#nullable enable + +namespace Sentry.Tests.Internals; + +public class LazyLiteTests +{ + private int _valueFactoryInvoked = 0; + + [Fact] + public void Default_HasNoFactory_ValueConsideredCreated() + { + LazyLite lazy = default; + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(0); + + lazy.Value.Should().BeNull(); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(0); + } + + [Fact] + public void Null_HasNoFactory_ValueConsideredCreated() + { + LazyLite lazy = new(null); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(0); + + lazy.Value.Should().BeNull(); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(0); + } + + [Fact] + public void NullFactory_HasFactory_CreatesNullValue() + { + LazyLite lazy = new(NullFactory); + + lazy.IsValueCreated.Should().BeFalse(); + _valueFactoryInvoked.Should().Be(0); + + lazy.Value.Should().BeNull(); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(1); + } + + [Fact] + public void ValueFactory_HasFactory_CreatesValue() + { + LazyLite lazy = new(ValueFactory); + + lazy.IsValueCreated.Should().BeFalse(); + _valueFactoryInvoked.Should().Be(0); + + lazy.Value.Should().Be("Created"); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(1); + } + + [Fact] + public void ValueFactory_HasFactory_ReuseCachedValue() + { + LazyLite lazy = new(ValueFactory); + + lazy.Value.Should().Be("Created"); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(1); + + lazy.Value.Should().Be("Created"); + + lazy.IsValueCreated.Should().BeTrue(); + _valueFactoryInvoked.Should().Be(1); + } + + private string? NullFactory() + { + _valueFactoryInvoked++; + return null; + } + + private string ValueFactory() + { + _valueFactoryInvoked++; + return "Created"; + } +}