diff --git a/benchmarks/Sentry.Benchmarks/SpanIdBenchmarks.cs b/benchmarks/Sentry.Benchmarks/SpanIdBenchmarks.cs new file mode 100644 index 0000000000..41de5686d0 --- /dev/null +++ b/benchmarks/Sentry.Benchmarks/SpanIdBenchmarks.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Attributes; + +namespace Sentry.Benchmarks; + +public class SpanIdBenchmarks +{ + [Benchmark(Description = "Creates a Span ID")] + public void CreateSpanId() + { + SpanId.Create(); + } +} diff --git a/src/Sentry/Internal/RandomValuesFactory.cs b/src/Sentry/Internal/RandomValuesFactory.cs index 5c57429c7a..e2a5f59f92 100644 --- a/src/Sentry/Internal/RandomValuesFactory.cs +++ b/src/Sentry/Internal/RandomValuesFactory.cs @@ -7,6 +7,10 @@ internal abstract class RandomValuesFactory public abstract double NextDouble(); public abstract void NextBytes(byte[] bytes); +#if !(NETSTANDARD2_0 || NET461) + public abstract void NextBytes(Span bytes); +#endif + public bool NextBool(double rate) => rate switch { >= 1 => true, diff --git a/src/Sentry/Internal/SynchronizedRandomValuesFactory.cs b/src/Sentry/Internal/SynchronizedRandomValuesFactory.cs index 75b0988857..b1f591ce12 100644 --- a/src/Sentry/Internal/SynchronizedRandomValuesFactory.cs +++ b/src/Sentry/Internal/SynchronizedRandomValuesFactory.cs @@ -7,6 +7,7 @@ internal class SynchronizedRandomValuesFactory : RandomValuesFactory public override int NextInt(int minValue, int maxValue) => Random.Shared.Next(minValue, maxValue); public override double NextDouble() => Random.Shared.NextDouble(); public override void NextBytes(byte[] bytes) => Random.Shared.NextBytes(bytes); + public override void NextBytes(Span bytes) => Random.Shared.NextBytes(bytes); #else private static readonly AsyncLocal LocalRandom = new(); private static Random Random => LocalRandom.Value ??= new Random(); @@ -15,5 +16,10 @@ internal class SynchronizedRandomValuesFactory : RandomValuesFactory public override int NextInt(int minValue, int maxValue) => Random.Next(minValue, maxValue); public override double NextDouble() => Random.NextDouble(); public override void NextBytes(byte[] bytes) => Random.NextBytes(bytes); + +#if !(NETSTANDARD2_0 || NET461) + public override void NextBytes(Span bytes) => Random.NextBytes(bytes); +#endif + #endif -} \ No newline at end of file +} diff --git a/src/Sentry/SpanId.cs b/src/Sentry/SpanId.cs index 85aa9405e9..5ede9357f2 100644 --- a/src/Sentry/SpanId.cs +++ b/src/Sentry/SpanId.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry; @@ -7,58 +8,81 @@ namespace Sentry; /// public readonly struct SpanId : IEquatable, IJsonSerializable { - private readonly string _value; + private static readonly char[] HexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static readonly RandomValuesFactory Random = new SynchronizedRandomValuesFactory(); - private const string EmptyValue = "0000000000000000"; + private readonly long _value; + + private long GetValue() => _value; /// /// An empty Sentry span ID. /// - public static readonly SpanId Empty = new(EmptyValue); + public static readonly SpanId Empty = new(0); /// /// Creates a new instance of a Sentry span Id. /// - public SpanId(string value) => _value = value; - - // This method is used to return a string with all zeroes in case - // the `_value` is equal to null. - // It can be equal to null because this is a struct and always has - // a parameterless constructor that evaluates to an instance with - // all fields initialized to default values. - // Effectively, using this method instead of just referencing `_value` - // makes the behavior more consistent, for example: - // default(SpanId).ToString() -> "0000000000000000" - // default(SpanId) == SpanId.Empty -> true - private string GetNormalizedValue() => !string.IsNullOrWhiteSpace(_value) - ? _value - : EmptyValue; + public SpanId(string value) => long.TryParse(value, NumberStyles.HexNumber, null, out _value); + + /// + /// Creates a new instance of a Sentry span Id. + /// + /// + public SpanId(long value) => _value = value; /// - public bool Equals(SpanId other) => StringComparer.Ordinal.Equals( - GetNormalizedValue(), - other.GetNormalizedValue()); + public bool Equals(SpanId other) => GetValue().Equals(other.GetValue()); /// public override bool Equals(object? obj) => obj is SpanId other && Equals(other); /// - public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(GetNormalizedValue()); + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(_value); /// - public override string ToString() => GetNormalizedValue(); + public override string ToString() => _value.ToString("x8"); - // Note: spans are sentry IDs with only 16 characters, rest being truncated. - // This is obviously a bad idea as it invalidates GUID's uniqueness properties - // (https://devblogs.microsoft.com/oldnewthing/20080627-00/?p=21823) - // but all other SDKs do it this way, so we have no choice but to comply. /// /// Generates a new Sentry ID. /// - public static SpanId Create() => new(Guid.NewGuid().ToString("n")[..16]); + public static SpanId Create() + { +#if NETSTANDARD2_0 || NET461 + byte[] buf = new byte[8]; +#else + Span buf = stackalloc byte[8]; +#endif + + Random.NextBytes(buf); + + var random = BitConverter.ToInt64(buf +#if NETSTANDARD2_0 || NET461 + , 0); +#else + ); +#endif + + return new SpanId(random); + } /// - public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? _) => writer.WriteStringValue(GetNormalizedValue()); + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? _) + { + Span convertedBytes = stackalloc byte[sizeof(long)]; + Unsafe.As(ref convertedBytes[0]) = _value; + + // Going backwards through the array to preserve the order of the output hex string (i.e. `4e76` -> `76e4`) + Span output = stackalloc char[16]; + for (var i = convertedBytes.Length - 1; i >= 0; i--) + { + var value = convertedBytes[i]; + output[(convertedBytes.Length - 1 - i) * 2] = HexChars[value >> 4]; + output[(convertedBytes.Length - 1 - i) * 2 + 1] = HexChars[value & 0xF]; + } + + writer.WriteStringValue(output); + } /// /// Parses from string. @@ -91,20 +115,4 @@ public static SpanId FromJson(JsonElement json) /// The from the . /// public static implicit operator string(SpanId id) => id.ToString(); - - // Note: no implicit conversion from `string` to `SpanId` as that leads to serious bugs. - // For example, given a method: - // transaction.StartChild(SpanId parentSpanId, string operation) - // And an *extension* method: - // transaction.StartChild(string operation, string description) - // The following code: - // transaction.StartChild("foo", "bar") - // Will resolve to the first method and not the second, which is incorrect. - - /* - /// - /// A from a . - /// - public static implicit operator SpanId(string value) => new(value); - */ } diff --git a/test/Sentry.Testing/IsolatedRandomValuesFactory.cs b/test/Sentry.Testing/IsolatedRandomValuesFactory.cs index 9da87fbd0a..b8d7123108 100644 --- a/test/Sentry.Testing/IsolatedRandomValuesFactory.cs +++ b/test/Sentry.Testing/IsolatedRandomValuesFactory.cs @@ -11,4 +11,9 @@ internal class IsolatedRandomValuesFactory : RandomValuesFactory public override double NextDouble() => _random.NextDouble(); public override void NextBytes(byte[] bytes) => _random.NextBytes(bytes); + +#if !(NETSTANDARD2_0 || NET48) + public override void NextBytes(Span bytes) => _random.NextBytes(bytes); +#endif + } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt index b09c5062dd..ce1ac35d50 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt @@ -934,6 +934,7 @@ namespace Sentry public readonly struct SpanId : Sentry.IJsonSerializable, System.IEquatable { public static readonly Sentry.SpanId Empty; + public SpanId(long value) { } public SpanId(string value) { } public bool Equals(Sentry.SpanId other) { } public override bool Equals(object? obj) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index e87375aed5..f76f4a5738 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -935,6 +935,7 @@ namespace Sentry public readonly struct SpanId : Sentry.IJsonSerializable, System.IEquatable { public static readonly Sentry.SpanId Empty; + public SpanId(long value) { } public SpanId(string value) { } public bool Equals(Sentry.SpanId other) { } public override bool Equals(object? obj) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index e87375aed5..f76f4a5738 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -935,6 +935,7 @@ namespace Sentry public readonly struct SpanId : Sentry.IJsonSerializable, System.IEquatable { public static readonly Sentry.SpanId Empty; + public SpanId(long value) { } public SpanId(string value) { } public bool Equals(Sentry.SpanId other) { } public override bool Equals(object? obj) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index a3174ce82f..f65bd4dd48 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -933,6 +933,7 @@ namespace Sentry public readonly struct SpanId : Sentry.IJsonSerializable, System.IEquatable { public static readonly Sentry.SpanId Empty; + public SpanId(long value) { } public SpanId(string value) { } public bool Equals(Sentry.SpanId other) { } public override bool Equals(object? obj) { }