Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 | - | - |
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Sentry.Extensibility;
using Sentry.Internal;

namespace Sentry.Benchmarks;
namespace Sentry.Benchmarks.Internal;

/// <summary>
/// <see cref="BatchProcessor{TItem}"/> (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only.
Expand Down
44 changes: 44 additions & 0 deletions benchmarks/Sentry.Benchmarks/Internal/LazyBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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<char>(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<char>(ValueFactory);

var first = lazy.Value;
var second = lazy.Value;

return (first, second);
}

private static char ValueFactory()
{
return Text[^1];
}
}
2 changes: 1 addition & 1 deletion benchmarks/Sentry.Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*"
```
2 changes: 1 addition & 1 deletion src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string?>(context.TryGetCustomTransactionName, LazyThreadSafetyMode.None);
var customName = new LazyLite<string?>(context.TryGetCustomTransactionName);
var forceCustomName = _options.PreferTransactionNameProvider && customName.Value is not null;
if (transaction.Name == string.Empty || forceCustomName)
{
Expand Down
38 changes: 38 additions & 0 deletions src/Sentry/Internal/LazyLite.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Sentry.Internal;

/// <summary>
/// A <see cref="ValueType"/>-based variant of <see cref="Lazy{T}"/> that is not thread safe.
///
/// We're using this for local variables, where no <see cref="LazyThreadSafetyMode"/> is required,
/// but a value is still cached, without any allocations via the <see langword="class"/>-based <see cref="Lazy{T}"/>.
/// </summary>
/// <remarks>
/// This type is not thread safe.
/// </remarks>
internal struct LazyLite<T>
{
private Func<T>? _factory;
private T? _value;

public LazyLite(Func<T>? 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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

question: exception

If the factory-callback throws, we propagate the Exception, but only once ... I think System.Lazy captures it and re-throws it every time accessing Value. On the other hand, we don't really need this functionality right now.

}

return _value!;
}
}
92 changes: 92 additions & 0 deletions test/Sentry.Tests/Internals/LazyLiteTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#nullable enable

namespace Sentry.Tests.Internals;

public class LazyLiteTests
{
private int _valueFactoryInvoked = 0;

[Fact]
public void Default_HasNoFactory_ValueConsideredCreated()
{
LazyLite<string?> 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<string?> 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<string?> 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<string?> 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<string?> 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";
}
}
Loading