From d01fa96cfcb419eecaa611c620754fa4a80f4319 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Jul 2024 16:51:46 +0100 Subject: [PATCH 1/4] Optimise TraceContextPropagator.Extract --- .../Propagation/TraceContextPropagator.cs | 217 ++++++++++++------ .../TraceContextPropagatorBenchmarks.cs | 72 ++++++ .../Propagation/TraceContextPropagatorTest.cs | 14 +- 3 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index 3ef627a5bdf..a9b8037435d 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -1,9 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Text; using OpenTelemetry.Internal; namespace OpenTelemetry.Context.Propagation; @@ -76,7 +76,7 @@ public override PropagationContext Extract(PropagationContext context, T carr var tracestateCollection = getter(carrier, TraceState); if (tracestateCollection?.Any() ?? false) { - TryExtractTracestate(tracestateCollection.ToArray(), out tracestate); + TryExtractTracestate(tracestateCollection, out tracestate); } return new PropagationContext( @@ -220,91 +220,180 @@ internal static bool TryExtractTraceparent(string traceparent, out ActivityTrace return true; } - internal static bool TryExtractTracestate(string[] tracestateCollection, out string tracestateResult) + internal static bool TryExtractTracestate(IEnumerable tracestateCollection, out string tracestateResult) { tracestateResult = string.Empty; - if (tracestateCollection != null) + char[] array = null; + Span buffer = stackalloc char[256]; + Span keyLookupBuffer = stackalloc char[96]; // 3 x 32 keys + int keys = 0; + int charsWritten = 0; + + foreach (var tracestateItem in tracestateCollection) { - var keySet = new HashSet(); - var result = new StringBuilder(); - for (int i = 0; i < tracestateCollection.Length; ++i) + var tracestate = tracestateItem.AsSpan(); + int position = 0; + + while (position < tracestate.Length) { - var tracestate = tracestateCollection[i].AsSpan(); - int begin = 0; - while (begin < tracestate.Length) + int length = tracestate.Slice(position).IndexOf(','); + ReadOnlySpan listMember; + + if (length != -1) { - int length = tracestate.Slice(begin).IndexOf(','); - ReadOnlySpan listMember; - if (length != -1) - { - listMember = tracestate.Slice(begin, length).Trim(); - begin += length + 1; - } - else - { - listMember = tracestate.Slice(begin).Trim(); - begin = tracestate.Length; - } + listMember = tracestate.Slice(position, length).Trim(); + position += length + 1; + } + else + { + listMember = tracestate.Slice(position).Trim(); + position = tracestate.Length; + } - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values - if (listMember.IsEmpty) - { - // Empty and whitespace - only list members are allowed. - // Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. - continue; - } + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values + if (listMember.IsEmpty) + { + // Empty and whitespace - only list members are allowed. + // Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. + continue; + } - if (keySet.Count >= 32) - { - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list - // test_tracestate_member_count_limit - return false; - } + if (keys >= 32) + { + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list + // test_tracestate_member_count_limit + return Return(false, ref array); + } - int keyLength = listMember.IndexOf('='); - if (keyLength == listMember.Length || keyLength == -1) - { - // Missing key or value in tracestate - return false; - } + int keyLength = listMember.IndexOf('='); + if (keyLength == listMember.Length || keyLength == -1) + { + // Missing key or value in tracestate + return Return(false, ref array); + } - var key = listMember.Slice(0, keyLength); - if (!ValidateKey(key)) - { - // test_tracestate_key_illegal_characters in https://github.com/w3c/trace-context/blob/master/test/test.py - // test_tracestate_key_length_limit - // test_tracestate_key_illegal_vendor_format - return false; - } + var key = listMember.Slice(0, keyLength); + if (!ValidateKey(key)) + { + // test_tracestate_key_illegal_characters in https://github.com/w3c/trace-context/blob/master/test/test.py + // test_tracestate_key_length_limit + // test_tracestate_key_illegal_vendor_format + return Return(false, ref array); + } - var value = listMember.Slice(keyLength + 1); - if (!ValidateValue(value)) - { - // test_tracestate_value_illegal_characters - return false; - } + var value = listMember.Slice(keyLength + 1); + if (!ValidateValue(value)) + { + // test_tracestate_value_illegal_characters + return Return(false, ref array); + } + + // ValidateKey() call above has ensured the key does not contain upper case letters. - // ValidateKey() call above has ensured the key does not contain upper case letters. - if (!keySet.Add(key.ToString())) + var duplicationCheckLength = Math.Min(key.Length, 3); + + if (keys > 0) + { + // Fast path check of first three chars for potential duplicated keys + var potentialMatchingKeyPosition = 1; + var found = false; + for (int i = 0; i < keys * 3; i += 3) { - // test_tracestate_duplicated_keys - return false; + if (keyLookupBuffer.Slice(i, duplicationCheckLength).SequenceEqual(key.Slice(0, duplicationCheckLength))) + { + found = true; + break; + } + + potentialMatchingKeyPosition++; } - if (result.Length > 0) + // If the fast check has found a possible duplicate, we need to do a full check + if (found) { - result.Append(','); + var bufferToCompare = buffer.Slice(0, charsWritten); + + // We know which key is the first posible duplicate, so skip to that key + // by slicing to the position after the appropriate comma. + for (int i = 1; i < potentialMatchingKeyPosition; i++) + { + var commaIndex = bufferToCompare.IndexOf(','); + + if (commaIndex > -1) + { + bufferToCompare.Slice(commaIndex); + } + } + + int existingIndex = -1; + while ((existingIndex = bufferToCompare.IndexOf(key)) > -1) + { + if ((existingIndex > 0 && bufferToCompare[existingIndex - 1] != ',') || bufferToCompare[existingIndex + key.Length] != '=') + { + continue; // this is not a key + } + + return Return(false, ref array); // test_tracestate_duplicated_keys + } } + } + + // Store up to the first three characters of the key for use in the duplicate lookup fast path + var startKeyLookupIndex = keys > 0 ? keys * 3 : 0; + key.Slice(0, duplicationCheckLength).CopyTo(keyLookupBuffer.Slice(startKeyLookupIndex)); + + // Check we have capacity to write the key and value + var requiredCapacity = charsWritten > 0 ? listMember.Length + 1 : listMember.Length; - result.Append(listMember.ToString()); + while (charsWritten + requiredCapacity > buffer.Length) + { + GrowBuffer(ref array, ref buffer); + } + + if (charsWritten > 0) + { + buffer[charsWritten++] = ','; } + + listMember.CopyTo(buffer.Slice(charsWritten)); + charsWritten += listMember.Length; + + keys++; } + } - tracestateResult = result.ToString(); + tracestateResult = buffer.Slice(0, charsWritten).ToString(); + + return Return(true, ref array); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool Return(bool returnValue, ref char[] array) + { + if (array is not null) + { + ArrayPool.Shared.Return(array); + array = null; + } + + return returnValue; } - return true; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void GrowBuffer(ref char[] array, ref Span buffer) + { + var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); + + buffer.CopyTo(newBuffer.AsSpan()); + + if (array is not null) + { + ArrayPool.Shared.Return(array); + } + + array = newBuffer; + buffer = array.AsSpan(); + } } private static byte HexCharToByte(char c) diff --git a/test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs b/test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs new file mode 100644 index 00000000000..2f9d26f9ab0 --- /dev/null +++ b/test/Benchmarks/Context/Propagation/TraceContextPropagatorBenchmarks.cs @@ -0,0 +1,72 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using OpenTelemetry.Context.Propagation; + +namespace Benchmarks.Context.Propagation; + +public class TraceContextPropagatorBenchmarks +{ + private const string TraceParent = "traceparent"; + private const string TraceState = "tracestate"; + private const string TraceId = "0af7651916cd43dd8448eb211c80319c"; + private const string SpanId = "b9c7c989f97918e1"; + + private static readonly Random Random = new(455946); + private static readonly TraceContextPropagator TraceContextPropagator = new(); + + private static readonly Func, string, IEnumerable> Getter = (headers, name) => + { + if (headers.TryGetValue(name, out var value)) + { + return [value]; + } + + return []; + }; + + private Dictionary headers; + + [Params(true, false)] + public bool LongListMember { get; set; } + + [Params(0, 4, 32)] + public int MembersCount { get; set; } + + public Dictionary Headers => this.headers; + + [GlobalSetup] + public void Setup() + { + var length = this.LongListMember ? 256 : 20; + + var value = new string('a', length); + + Span keyBuffer = stackalloc char[length - 2]; + + string traceState = string.Empty; + for (var i = 0; i < this.MembersCount; i++) + { + // We want a unique key for each member + for (var j = 0; j < length - 2; j++) + { + keyBuffer[j] = (char)('a' + Random.Next(0, 26)); + } + + var key = keyBuffer.ToString(); + + var listMember = $"{key}{i:00}={value}"; + traceState += i < this.MembersCount - 1 ? $"{listMember}," : listMember; + } + + this.headers = new Dictionary + { + { TraceParent, $"00-{TraceId}-{SpanId}-01" }, + { TraceState, traceState }, + }; + } + + [Benchmark(Baseline = true)] + public void Extract() => _ = TraceContextPropagator!.Extract(default, this.headers, Getter); +} diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs index 728d3884e45..cfd3c7833e5 100644 --- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs @@ -183,8 +183,18 @@ public void DuplicateKeys() // test_tracestate_duplicated_keys Assert.Empty(CallTraceContextPropagator("foo=1,foo=1")); Assert.Empty(CallTraceContextPropagator("foo=1,foo=2")); - Assert.Empty(CallTraceContextPropagator(new[] { "foo=1", "foo=1" })); - Assert.Empty(CallTraceContextPropagator(new[] { "foo=1", "foo=2" })); + Assert.Empty(CallTraceContextPropagator(["foo=1", "foo=1"])); + Assert.Empty(CallTraceContextPropagator(["foo=1", "foo=2"])); + Assert.Empty(CallTraceContextPropagator("foo=1,bar=2,baz=3,foo=4")); + } + + [Fact] + public void NoDuplicateKeys() + { + Assert.Equal("foo=1,bar=foo,baz=2", CallTraceContextPropagator("foo=1,bar=foo,baz=2")); + Assert.Equal("foo=1,bar=2,baz=foo", CallTraceContextPropagator("foo=1,bar=2,baz=foo")); + Assert.Equal("foo=1,foo@tenant=2", CallTraceContextPropagator("foo=1,foo@tenant=2")); + Assert.Equal("foo=1,tenant@foo=2", CallTraceContextPropagator("foo=1,tenant@foo=2")); } [Fact] From 538592e7de2f8d50de1556417d4f73bdc4b1e2c9 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Jul 2024 16:56:13 +0100 Subject: [PATCH 2/4] Update changelog --- src/OpenTelemetry.Api/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index f39d81b108e..22c3981f18c 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Optimize performance of `TraceContextPropagator.Extract`. + ([#5749](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5749)) + ## 1.9.0 Released 2024-Jun-14 From a24b5a994beff3c9dbf07af671fa07672abb104d Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 16 Jul 2024 08:14:22 +0100 Subject: [PATCH 3/4] Reduce initial stackalloc buffer size and prefer try/finally --- .../Propagation/TraceContextPropagator.cs | 225 +++++++++--------- .../Propagation/TraceContextPropagatorTest.cs | 6 +- 2 files changed, 115 insertions(+), 116 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index a9b8037435d..5a092acf8df 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -224,159 +224,158 @@ internal static bool TryExtractTracestate(IEnumerable tracestateCollecti { tracestateResult = string.Empty; - char[] array = null; - Span buffer = stackalloc char[256]; - Span keyLookupBuffer = stackalloc char[96]; // 3 x 32 keys + char[] rentedArray = null; + Span traceStateBuffer = stackalloc char[128]; // 256B + Span keyLookupBuffer = stackalloc char[96]; // 192B (3x32 keys) int keys = 0; int charsWritten = 0; - foreach (var tracestateItem in tracestateCollection) + try { - var tracestate = tracestateItem.AsSpan(); - int position = 0; - - while (position < tracestate.Length) + foreach (var tracestateItem in tracestateCollection) { - int length = tracestate.Slice(position).IndexOf(','); - ReadOnlySpan listMember; - - if (length != -1) - { - listMember = tracestate.Slice(position, length).Trim(); - position += length + 1; - } - else - { - listMember = tracestate.Slice(position).Trim(); - position = tracestate.Length; - } - - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values - if (listMember.IsEmpty) - { - // Empty and whitespace - only list members are allowed. - // Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. - continue; - } - - if (keys >= 32) - { - // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list - // test_tracestate_member_count_limit - return Return(false, ref array); - } + var tracestate = tracestateItem.AsSpan(); + int position = 0; - int keyLength = listMember.IndexOf('='); - if (keyLength == listMember.Length || keyLength == -1) + while (position < tracestate.Length) { - // Missing key or value in tracestate - return Return(false, ref array); - } + int length = tracestate.Slice(position).IndexOf(','); + ReadOnlySpan listMember; - var key = listMember.Slice(0, keyLength); - if (!ValidateKey(key)) - { - // test_tracestate_key_illegal_characters in https://github.com/w3c/trace-context/blob/master/test/test.py - // test_tracestate_key_length_limit - // test_tracestate_key_illegal_vendor_format - return Return(false, ref array); - } - - var value = listMember.Slice(keyLength + 1); - if (!ValidateValue(value)) - { - // test_tracestate_value_illegal_characters - return Return(false, ref array); - } + if (length != -1) + { + listMember = tracestate.Slice(position, length).Trim(); + position += length + 1; + } + else + { + listMember = tracestate.Slice(position).Trim(); + position = tracestate.Length; + } - // ValidateKey() call above has ensured the key does not contain upper case letters. + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values + if (listMember.IsEmpty) + { + // Empty and whitespace - only list members are allowed. + // Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. + continue; + } - var duplicationCheckLength = Math.Min(key.Length, 3); + if (keys >= 32) + { + // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list + // test_tracestate_member_count_limit + return false; + } - if (keys > 0) - { - // Fast path check of first three chars for potential duplicated keys - var potentialMatchingKeyPosition = 1; - var found = false; - for (int i = 0; i < keys * 3; i += 3) + int keyLength = listMember.IndexOf('='); + if (keyLength == listMember.Length || keyLength == -1) { - if (keyLookupBuffer.Slice(i, duplicationCheckLength).SequenceEqual(key.Slice(0, duplicationCheckLength))) - { - found = true; - break; - } + // Missing key or value in tracestate + return false; + } - potentialMatchingKeyPosition++; + var key = listMember.Slice(0, keyLength); + if (!ValidateKey(key)) + { + // test_tracestate_key_illegal_characters in https://github.com/w3c/trace-context/blob/master/test/test.py + // test_tracestate_key_length_limit + // test_tracestate_key_illegal_vendor_format + return false; } - // If the fast check has found a possible duplicate, we need to do a full check - if (found) + var value = listMember.Slice(keyLength + 1); + if (!ValidateValue(value)) { - var bufferToCompare = buffer.Slice(0, charsWritten); + // test_tracestate_value_illegal_characters + return false; + } - // We know which key is the first posible duplicate, so skip to that key - // by slicing to the position after the appropriate comma. - for (int i = 1; i < potentialMatchingKeyPosition; i++) - { - var commaIndex = bufferToCompare.IndexOf(','); + // ValidateKey() call above has ensured the key does not contain upper case letters. + + var duplicationCheckLength = Math.Min(key.Length, 3); - if (commaIndex > -1) + if (keys > 0) + { + // Fast path check of first three chars for potential duplicated keys + var potentialMatchingKeyPosition = 1; + var found = false; + for (int i = 0; i < keys * 3; i += 3) + { + if (keyLookupBuffer.Slice(i, duplicationCheckLength).SequenceEqual(key.Slice(0, duplicationCheckLength))) { - bufferToCompare.Slice(commaIndex); + found = true; + break; } + + potentialMatchingKeyPosition++; } - int existingIndex = -1; - while ((existingIndex = bufferToCompare.IndexOf(key)) > -1) + // If the fast check has found a possible duplicate, we need to do a full check + if (found) { - if ((existingIndex > 0 && bufferToCompare[existingIndex - 1] != ',') || bufferToCompare[existingIndex + key.Length] != '=') + var bufferToCompare = traceStateBuffer.Slice(0, charsWritten); + + // We know which key is the first posible duplicate, so skip to that key + // by slicing to the position after the appropriate comma. + for (int i = 1; i < potentialMatchingKeyPosition; i++) { - continue; // this is not a key + var commaIndex = bufferToCompare.IndexOf(','); + + if (commaIndex > -1) + { + bufferToCompare.Slice(commaIndex); + } } - return Return(false, ref array); // test_tracestate_duplicated_keys + int existingIndex = -1; + while ((existingIndex = bufferToCompare.IndexOf(key)) > -1) + { + if ((existingIndex > 0 && bufferToCompare[existingIndex - 1] != ',') || bufferToCompare[existingIndex + key.Length] != '=') + { + continue; // this is not a key + } + + return false; // test_tracestate_duplicated_keys + } } } - } - // Store up to the first three characters of the key for use in the duplicate lookup fast path - var startKeyLookupIndex = keys > 0 ? keys * 3 : 0; - key.Slice(0, duplicationCheckLength).CopyTo(keyLookupBuffer.Slice(startKeyLookupIndex)); + // Store up to the first three characters of the key for use in the duplicate lookup fast path + var startKeyLookupIndex = keys > 0 ? keys * 3 : 0; + key.Slice(0, duplicationCheckLength).CopyTo(keyLookupBuffer.Slice(startKeyLookupIndex)); - // Check we have capacity to write the key and value - var requiredCapacity = charsWritten > 0 ? listMember.Length + 1 : listMember.Length; + // Check we have capacity to write the key and value + var requiredCapacity = charsWritten > 0 ? listMember.Length + 1 : listMember.Length; - while (charsWritten + requiredCapacity > buffer.Length) - { - GrowBuffer(ref array, ref buffer); - } + while (charsWritten + requiredCapacity > traceStateBuffer.Length) + { + GrowBuffer(ref rentedArray, ref traceStateBuffer); + } - if (charsWritten > 0) - { - buffer[charsWritten++] = ','; - } + if (charsWritten > 0) + { + traceStateBuffer[charsWritten++] = ','; + } - listMember.CopyTo(buffer.Slice(charsWritten)); - charsWritten += listMember.Length; + listMember.CopyTo(traceStateBuffer.Slice(charsWritten)); + charsWritten += listMember.Length; - keys++; + keys++; + } } - } - tracestateResult = buffer.Slice(0, charsWritten).ToString(); + tracestateResult = traceStateBuffer.Slice(0, charsWritten).ToString(); - return Return(true, ref array); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool Return(bool returnValue, ref char[] array) + return true; + } + finally { - if (array is not null) + if (rentedArray is not null) { - ArrayPool.Shared.Return(array); - array = null; + ArrayPool.Shared.Return(rentedArray); + rentedArray = null; } - - return returnValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs index cfd3c7833e5..8fbd20f11de 100644 --- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs @@ -13,12 +13,12 @@ public class TraceContextPropagatorTest private const string TraceId = "0af7651916cd43dd8448eb211c80319c"; private const string SpanId = "b9c7c989f97918e1"; - private static readonly string[] Empty = Array.Empty(); + private static readonly string[] Empty = []; private static readonly Func, string, IEnumerable> Getter = (headers, name) => { if (headers.TryGetValue(name, out var value)) { - return new[] { value }; + return [value]; } return Empty; @@ -31,7 +31,7 @@ public class TraceContextPropagatorTest return value; } - return Array.Empty(); + return []; }; private static readonly Action, string, string> Setter = (carrier, name, value) => From 35af52d6445cb8f12770ddd9e297b43ca38ed1b1 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 8 Aug 2024 10:48:21 +0100 Subject: [PATCH 4/4] Fix typo Co-authored-by: Vishwesh Bankwar --- .../Context/Propagation/TraceContextPropagator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index 5a092acf8df..b37fb93cc08 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -316,7 +316,7 @@ internal static bool TryExtractTracestate(IEnumerable tracestateCollecti { var bufferToCompare = traceStateBuffer.Slice(0, charsWritten); - // We know which key is the first posible duplicate, so skip to that key + // We know which key is the first possible duplicate, so skip to that key // by slicing to the position after the appropriate comma. for (int i = 1; i < potentialMatchingKeyPosition; i++) {