diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 7b221e1435..93833dc108 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -348,5 +348,10 @@ public static partial class Constants // TODO: return a list of built in types so we can use that to prevent deletion in the uI } + + public static class Udi + { + public const string Prefix = "umb://"; + } } } diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index e8280bceb6..7f95299fd8 100644 --- a/src/Umbraco.Core/GuidUdi.cs +++ b/src/Umbraco.Core/GuidUdi.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using Umbraco.Extensions; namespace Umbraco.Cms.Core; @@ -14,7 +15,7 @@ public class GuidUdi : Udi /// The entity type part of the udi. /// The guid part of the udi. public GuidUdi(string entityType, Guid guid) - : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) => + : base(entityType, CreateStringValue(entityType, guid)) => Guid = guid; /// @@ -57,4 +58,19 @@ public class GuidUdi : Udi EnsureNotRoot(); return this; } + + private static string CreateStringValue(ReadOnlySpan entityType, Guid guid) + { + var startUdiLength = Constants.Conventions.Udi.Prefix.Length; + var outputSize = entityType.Length + startUdiLength + 32 + 1; //Based on the format umb://entityType/guid (32 = Guid N format, 1 = / between entityType and guid) + Span output = stackalloc char[outputSize]; + + //Add all the values of the format to the output + Constants.Conventions.Udi.Prefix.CopyTo(output[..startUdiLength]); + entityType.CopyTo(output.Slice(startUdiLength, entityType.Length)); + output[startUdiLength + entityType.Length] = '/'; + guid.TryFormat(output.Slice(outputSize - 32, 32), out _, "N"); + + return new string(output); + } } diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index 2b1229be77..32e3677e2f 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -14,7 +14,7 @@ public class StringUdi : Udi /// The entity type part of the udi. /// The string id part of the udi. public StringUdi(string entityType, string id) - : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) => + : base(entityType, Constants.Conventions.Udi.Prefix + entityType + "/" + EscapeUriString(id)) => Id = id; /// diff --git a/tests/Umbraco.Tests.Benchmarks/GuidUdiBenchmarks.cs b/tests/Umbraco.Tests.Benchmarks/GuidUdiBenchmarks.cs new file mode 100644 index 0000000000..f9946aba75 --- /dev/null +++ b/tests/Umbraco.Tests.Benchmarks/GuidUdiBenchmarks.cs @@ -0,0 +1,88 @@ +using System; +using BenchmarkDotNet.Attributes; +using Umbraco.Cms.Core; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunWithMemoryDiagnoserConfig] + public class GuidUdiBenchmarks + { + private readonly Guid _guid = Guid.NewGuid(); + private readonly string _entityType = Constants.UdiEntityType.DocumentType; + + [Benchmark(Baseline = true)] + public Udi CurrentInit() + { + return new OldGuidUdi(_entityType, _guid); + } + + [Benchmark()] + public Udi StringPolationInit() + { + return new StringPolationGuidUdi(_entityType, _guid); + } + + [Benchmark] + public Udi NewInit() + { + return new NewGuidUdi(_entityType, _guid); + } + + public class OldGuidUdi : Udi + { + public Guid Guid { get; } + + public override bool IsRoot => throw new NotImplementedException(); + + public OldGuidUdi(string entityType, Guid guid) + : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) => + Guid = guid; + } + + public class StringPolationGuidUdi : Udi + { + public Guid Guid { get; } + + public override bool IsRoot => throw new NotImplementedException(); + + public StringPolationGuidUdi(string entityType, Guid guid) + : base(entityType, $"umb://{entityType}/{guid:N}") => + Guid = guid; + } + + public class NewGuidUdi : Udi + { + public Guid Guid { get; } + + public override bool IsRoot => throw new NotImplementedException(); + + public NewGuidUdi(string entityType, Guid guid) : base(entityType, GetStringValue(entityType, guid)) + { + Guid = guid; + } + + public static string GetStringValue(ReadOnlySpan entityType, Guid guid) + { + var startUdiLength = Constants.Conventions.Udi.Prefix.Length; + var outputSize = entityType.Length + startUdiLength + 32 + 1; + Span output = stackalloc char[outputSize]; + + Constants.Conventions.Udi.Prefix.CopyTo(output[..startUdiLength]); + entityType.CopyTo(output.Slice(startUdiLength, entityType.Length)); + output[startUdiLength + entityType.Length] = '/'; + guid.TryFormat(output.Slice(outputSize - 32, 32), out _, "N"); + + return new string(output); + } + } + + // I think we are currently bottlenecked by the 'new Udi(string)' not accepting a span. If it did, then we wouldn't have to create a string mid creation and we would be able to cut back on the allocated data + // + //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated | + //|------------------- |---------:|----------:|---------:|------:|--------:|-------:|----------:| + //| CurrentInit | 562.2 ns | 242.84 ns | 13.31 ns | 1.00 | 0.00 | 0.1113 | 352 B | + //| StringPolationInit | 589.3 ns | 530.08 ns | 29.06 ns | 1.05 | 0.07 | 0.0811 | 264 B | + //| NewInit | 533.6 ns | 40.55 ns | 2.22 ns | 0.95 | 0.03 | 0.0808 | 264 B | + } +}