From ad30f5439b0e286c20136b8e7aece32b3b55f22e Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Sat, 5 Aug 2023 23:15:10 +0800 Subject: [PATCH 01/13] Avoid string allocation in WriteTo when possible. System.Text.Json.JsonProperty.WriteTo uses get_Name, calling JsonElement.GetPropertyName() which would allocate a string. Use ReadOnlySpan from the underlying UTF8 json, when possible, by adding helper methods into JsonDocument & JsonElement. Fix #88767 --- .../System/Text/Json/Document/JsonDocument.cs | 29 +++++++++++++++++++ .../System/Text/Json/Document/JsonElement.cs | 7 +++++ .../System/Text/Json/Document/JsonProperty.cs | 10 ++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 93f7143a55844b..06205796157673 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -283,6 +283,29 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) : JsonReaderHelper.TranscodeHelper(segment); } + internal ReadOnlySpan GetUtf8Span(int index, JsonTokenType expectedType) + { + CheckNotDisposed(); + + DbRow row = _parsedData.Get(index); + + JsonTokenType tokenType = row.TokenType; + + if (tokenType == JsonTokenType.Null) + { + return default; + } + + CheckExpectedType(expectedType, tokenType); + + ReadOnlySpan data = _utf8Json.Span; + ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); + + return row.HasComplexChildren + ? JsonReaderHelper.GetUnescapedSpan(segment) + : segment; + } + internal bool TextEquals(int index, ReadOnlySpan otherText, bool isPropertyName) { CheckNotDisposed(); @@ -363,6 +386,12 @@ internal string GetNameOfPropertyValue(int index) return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!; } + internal ReadOnlySpan GetNameOfPropertyValueAsUtf8Span(int index) + { + // The property name is one row before the property value + return GetUtf8Span(index - DbRow.Size, JsonTokenType.PropertyName)!; + } + internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value) { CheckNotDisposed(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index 77732b69548d6e..f7dad1edb17b1f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1163,6 +1163,13 @@ internal string GetPropertyName() return _parent.GetNameOfPropertyValue(_idx); } + internal ReadOnlySpan GetPropertyNameAsUtf8Span() + { + CheckValidInstance(); + + return _parent.GetNameOfPropertyValueAsUtf8Span(_idx); + } + /// /// Gets the original input data backing this value, returning it as a . /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 61a66b689ebcbf..71945d656227c2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -117,7 +117,15 @@ public void WriteTo(Utf8JsonWriter writer) ThrowHelper.ThrowArgumentNullException(nameof(writer)); } - writer.WritePropertyName(Name); + if (_name is null) + { + writer.WritePropertyName(Value.GetPropertyNameAsUtf8Span()); + } + else + { + writer.WritePropertyName(_name); + } + Value.WriteTo(writer); } From 41f5c3744b86227e28ebe7966ec611483acf8cd3 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Sat, 5 Aug 2023 23:49:07 +0800 Subject: [PATCH 02/13] Avoid alloc in unescaping & escaping. Current code unescapes & escapes property names and uses ToArray. Avoid alloc by adding internal GetRaw/WriteRaw methods. --- .../src/System/Text/Json/Document/JsonDocument.cs | 10 ++++------ .../src/System/Text/Json/Document/JsonElement.cs | 4 ++-- .../src/System/Text/Json/Document/JsonProperty.cs | 2 +- .../Writer/Utf8JsonWriter.WriteProperties.String.cs | 11 +++++++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 06205796157673..911537f3acf517 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -283,7 +283,7 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) : JsonReaderHelper.TranscodeHelper(segment); } - internal ReadOnlySpan GetUtf8Span(int index, JsonTokenType expectedType) + internal ReadOnlySpan GetRawUtf8Span(int index, JsonTokenType expectedType) { CheckNotDisposed(); @@ -301,9 +301,7 @@ internal ReadOnlySpan GetUtf8Span(int index, JsonTokenType expectedType) ReadOnlySpan data = _utf8Json.Span; ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); - return row.HasComplexChildren - ? JsonReaderHelper.GetUnescapedSpan(segment) - : segment; + return segment; } internal bool TextEquals(int index, ReadOnlySpan otherText, bool isPropertyName) @@ -386,10 +384,10 @@ internal string GetNameOfPropertyValue(int index) return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!; } - internal ReadOnlySpan GetNameOfPropertyValueAsUtf8Span(int index) + internal ReadOnlySpan GetRawNameOfPropertyValueAsUtf8Span(int index) { // The property name is one row before the property value - return GetUtf8Span(index - DbRow.Size, JsonTokenType.PropertyName)!; + return GetRawUtf8Span(index - DbRow.Size, JsonTokenType.PropertyName)!; } internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index f7dad1edb17b1f..fa2c86a8e7639f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1163,11 +1163,11 @@ internal string GetPropertyName() return _parent.GetNameOfPropertyValue(_idx); } - internal ReadOnlySpan GetPropertyNameAsUtf8Span() + internal ReadOnlySpan GetRawPropertyNameAsUtf8Span() { CheckValidInstance(); - return _parent.GetNameOfPropertyValueAsUtf8Span(_idx); + return _parent.GetRawNameOfPropertyValueAsUtf8Span(_idx); } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 71945d656227c2..16d690096cb768 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -119,7 +119,7 @@ public void WriteTo(Utf8JsonWriter writer) if (_name is null) { - writer.WritePropertyName(Value.GetPropertyNameAsUtf8Span()); + writer.WriteRawPropertyName(Value.GetRawPropertyNameAsUtf8Span()); } else { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs index 7db29516ddbe0f..c32b3d369df6a3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs @@ -259,6 +259,17 @@ public void WritePropertyName(ReadOnlySpan utf8PropertyName) _commentAfterNoneOrPropertyName = false; } + internal void WriteRawPropertyName(ReadOnlySpan utf8PropertyName) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + WriteStringByOptionsPropertyName(utf8PropertyName); + + _currentDepth &= JsonConstants.RemoveFlagsBitMask; + _tokenType = JsonTokenType.PropertyName; + _commentAfterNoneOrPropertyName = false; + } + private void WritePropertyNameUnescaped(ReadOnlySpan utf8PropertyName) { JsonWriterHelper.ValidateProperty(utf8PropertyName); From d4d84b1d3e646d30707e24a4df96811e9057ff9c Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Sun, 6 Aug 2023 15:58:56 +0800 Subject: [PATCH 03/13] Fix bugs on escaped property names Original code doesn't handle GetRaw/WriteRaw on escaped property names correctly. --- .../src/System/Text/Json/Document/JsonProperty.cs | 8 +++++++- .../Writer/Utf8JsonWriter.WriteProperties.String.cs | 11 ----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 16d690096cb768..209f3861195201 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -119,7 +119,13 @@ public void WriteTo(Utf8JsonWriter writer) if (_name is null) { - writer.WriteRawPropertyName(Value.GetRawPropertyNameAsUtf8Span()); + ReadOnlySpan rawName = Value.GetRawPropertyNameAsUtf8Span(); + if (rawName.IndexOf(JsonConstants.BackSlash) >= 0) + { + rawName = JsonReaderHelper.GetUnescapedSpan(rawName); + } + + writer.WritePropertyName(rawName); } else { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs index c32b3d369df6a3..7db29516ddbe0f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs @@ -259,17 +259,6 @@ public void WritePropertyName(ReadOnlySpan utf8PropertyName) _commentAfterNoneOrPropertyName = false; } - internal void WriteRawPropertyName(ReadOnlySpan utf8PropertyName) - { - JsonWriterHelper.ValidateProperty(utf8PropertyName); - - WriteStringByOptionsPropertyName(utf8PropertyName); - - _currentDepth &= JsonConstants.RemoveFlagsBitMask; - _tokenType = JsonTokenType.PropertyName; - _commentAfterNoneOrPropertyName = false; - } - private void WritePropertyNameUnescaped(ReadOnlySpan utf8PropertyName) { JsonWriterHelper.ValidateProperty(utf8PropertyName); From c368aa36224acb3a64cc3b599b19ed3f41b23e60 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Sun, 6 Aug 2023 16:17:59 +0800 Subject: [PATCH 04/13] Change IndexOf to Contains if possible. --- .../src/System/Text/Json/Document/JsonProperty.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 209f3861195201..89c4ee10382fcd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -120,7 +120,11 @@ public void WriteTo(Utf8JsonWriter writer) if (_name is null) { ReadOnlySpan rawName = Value.GetRawPropertyNameAsUtf8Span(); +#if NET462_OR_GREATER || NETSTANDARD2_0 if (rawName.IndexOf(JsonConstants.BackSlash) >= 0) +#else + if (rawName.Contains(JsonConstants.BackSlash)) +#endif { rawName = JsonReaderHelper.GetUnescapedSpan(rawName); } From c5c559346fd5ce668cde475daeb79383fca16051 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Sun, 6 Aug 2023 17:46:47 +0800 Subject: [PATCH 05/13] Further avoid alloc by inlining GetUnescapedSpan Allocations are further avoided when the property name is shorter than JsonConstants.StackallocByteThreshold, by inlining JsonReaderHelper.GetUnescapedSpan. --- .../System/Text/Json/Document/JsonProperty.cs | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 89c4ee10382fcd..5e76cbb83b549e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Diagnostics; namespace System.Text.Json @@ -120,16 +121,37 @@ public void WriteTo(Utf8JsonWriter writer) if (_name is null) { ReadOnlySpan rawName = Value.GetRawPropertyNameAsUtf8Span(); -#if NET462_OR_GREATER || NETSTANDARD2_0 - if (rawName.IndexOf(JsonConstants.BackSlash) >= 0) -#else - if (rawName.Contains(JsonConstants.BackSlash)) -#endif + int firstBackSlashIndex = rawName.IndexOf(JsonConstants.BackSlash); + if (firstBackSlashIndex >= 0) { - rawName = JsonReaderHelper.GetUnescapedSpan(rawName); - } + // Code is adapted from JsonReaderHelper.GetUnescapedSpan to avoid allocations further. + // writer.WritePropertyName(JsonReaderHelper.GetUnescapedSpan(rawName)); + + int length = rawName.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ? + stackalloc byte[JsonConstants.StackallocByteThreshold] : + (pooledName = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out int written); + Debug.Assert(written > 0); + + ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written); + Debug.Assert(!propertyName.IsEmpty); - writer.WritePropertyName(rawName); + writer.WritePropertyName(propertyName); + + if (pooledName != null) + { + new Span(pooledName, 0, written).Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + else + { + writer.WritePropertyName(rawName); + } } else { From 1528bf3366ce09b727e3ca7aa54d22425040cfc5 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Tue, 8 Aug 2023 02:34:10 +0800 Subject: [PATCH 06/13] Move writing logic to JsonElement; Shorten names of new methods; Add a test of writing out special names. --- .../System/Text/Json/Document/JsonDocument.cs | 13 ++-- .../System/Text/Json/Document/JsonElement.cs | 59 ++++++++++++++++++- .../System/Text/Json/Document/JsonProperty.cs | 34 +---------- .../JsonPropertyTests.cs | 19 ++++++ 4 files changed, 81 insertions(+), 44 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 911537f3acf517..67bb4596be493e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -283,7 +283,7 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) : JsonReaderHelper.TranscodeHelper(segment); } - internal ReadOnlySpan GetRawUtf8Span(int index, JsonTokenType expectedType) + internal ReadOnlySpan GetRawSpan(int index, JsonTokenType expectedType) { CheckNotDisposed(); @@ -291,12 +291,7 @@ internal ReadOnlySpan GetRawUtf8Span(int index, JsonTokenType expectedType JsonTokenType tokenType = row.TokenType; - if (tokenType == JsonTokenType.Null) - { - return default; - } - - CheckExpectedType(expectedType, tokenType); + Debug.Assert(tokenType == expectedType); ReadOnlySpan data = _utf8Json.Span; ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); @@ -384,10 +379,10 @@ internal string GetNameOfPropertyValue(int index) return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!; } - internal ReadOnlySpan GetRawNameOfPropertyValueAsUtf8Span(int index) + internal ReadOnlySpan GetRawNameOfPropertyValue(int index) { // The property name is one row before the property value - return GetRawUtf8Span(index - DbRow.Size, JsonTokenType.PropertyName)!; + return GetRawSpan(index - DbRow.Size, JsonTokenType.PropertyName)!; } internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index fa2c86a8e7639f..f084595b342cc3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -1163,11 +1164,16 @@ internal string GetPropertyName() return _parent.GetNameOfPropertyValue(_idx); } - internal ReadOnlySpan GetRawPropertyNameAsUtf8Span() + /// + /// Gets the property name exactly as it is in the underlying . + /// + internal ReadOnlySpan GetRawPropertyName() { + // TODO: Related to issue #77666 "Add JsonElement.ValueSpan" (https://github.com/dotnet/runtime/issues/77666) + CheckValidInstance(); - return _parent.GetRawNameOfPropertyValueAsUtf8Span(_idx); + return _parent.GetRawNameOfPropertyValue(_idx); } /// @@ -1323,6 +1329,55 @@ public void WriteTo(Utf8JsonWriter writer) _parent.WriteElementTo(_idx, writer); } + /// + /// Write the property name of this to a + /// in an allocation-less way, if the name is shorter than + /// or it doesn't require unescaping, and the underlying buffer is long enough. + /// + /// Utf8JsonWriter to write + internal void WritePropertyNameTo(Utf8JsonWriter writer) + { + ReadOnlySpan rawName = GetRawPropertyName(); + int firstBackSlashIndex = rawName.IndexOf(JsonConstants.BackSlash); + + if (firstBackSlashIndex >= 0) + { + // If the name needs unescaping + + // Equivalent to writer.WritePropertyName(JsonReaderHelper.GetUnescapedSpan(rawName)) + // Method is inlined here to avoid .ToArray() allocation for short names. + + int length = rawName.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ? + stackalloc byte[JsonConstants.StackallocByteThreshold] : + (pooledName = ArrayPool.Shared.Rent(length)); + + JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out int written); + Debug.Assert(written > 0); + + ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written); + Debug.Assert(!propertyName.IsEmpty); + + writer.WritePropertyName(propertyName); + + if (pooledName != null) + { + new Span(pooledName, 0, written).Clear(); + ArrayPool.Shared.Return(pooledName); + } + } + else + { + // Note we cannot just write it out by WriteStringByOptionsPropertyName. + // Because there might be plain Unicode chars in the original JsonDocument, + // which should be escaped if it is written out. + + writer.WritePropertyName(rawName); + } + } + /// /// Get an enumerator to enumerate the values in the JSON array represented by this JsonElement. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 5e76cbb83b549e..0d4ad75cbcadf7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Buffers; using System.Diagnostics; namespace System.Text.Json @@ -120,38 +119,7 @@ public void WriteTo(Utf8JsonWriter writer) if (_name is null) { - ReadOnlySpan rawName = Value.GetRawPropertyNameAsUtf8Span(); - int firstBackSlashIndex = rawName.IndexOf(JsonConstants.BackSlash); - if (firstBackSlashIndex >= 0) - { - // Code is adapted from JsonReaderHelper.GetUnescapedSpan to avoid allocations further. - // writer.WritePropertyName(JsonReaderHelper.GetUnescapedSpan(rawName)); - - int length = rawName.Length; - byte[]? pooledName = null; - - Span utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ? - stackalloc byte[JsonConstants.StackallocByteThreshold] : - (pooledName = ArrayPool.Shared.Rent(length)); - - JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out int written); - Debug.Assert(written > 0); - - ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written); - Debug.Assert(!propertyName.IsEmpty); - - writer.WritePropertyName(propertyName); - - if (pooledName != null) - { - new Span(pooledName, 0, written).Clear(); - ArrayPool.Shared.Return(pooledName); - } - } - else - { - writer.WritePropertyName(rawName); - } + Value.WritePropertyNameTo(writer); } else { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs index 8193c928342f71..e356e55e0f5554 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs @@ -79,6 +79,25 @@ public static void WriteSimpleObject() } } + [Fact] + public static void WriteEscapedNames() + { + var buffer = new ArrayBufferWriter(1024); + const string json = """{"q\t\\mm\t":1,"":2}"""; + using (JsonDocument doc = JsonDocument.Parse(json)) + { + using var writer = new Utf8JsonWriter(buffer); + writer.WriteStartObject(); + foreach (JsonProperty prop in doc.RootElement.EnumerateObject()) + { + prop.WriteTo(writer); + } + writer.WriteEndObject(); + writer.Flush(); + + AssertContents(json, buffer); + } + } private static void AssertContents(string expectedValue, ArrayBufferWriter buffer) { Assert.Equal( From 569048755ab1ed5b05afba0829120fe0e26acca2 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Fri, 11 Aug 2023 12:25:59 +0800 Subject: [PATCH 07/13] Move logic into JsonDocument --- .../System/Text/Json/Document/JsonDocument.cs | 70 +++++++++++++------ .../System/Text/Json/Document/JsonElement.cs | 52 +------------- 2 files changed, 49 insertions(+), 73 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 67bb4596be493e..453019bc944453 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -283,22 +283,6 @@ private ReadOnlyMemory GetPropertyRawValue(int valueIndex) : JsonReaderHelper.TranscodeHelper(segment); } - internal ReadOnlySpan GetRawSpan(int index, JsonTokenType expectedType) - { - CheckNotDisposed(); - - DbRow row = _parsedData.Get(index); - - JsonTokenType tokenType = row.TokenType; - - Debug.Assert(tokenType == expectedType); - - ReadOnlySpan data = _utf8Json.Span; - ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); - - return segment; - } - internal bool TextEquals(int index, ReadOnlySpan otherText, bool isPropertyName) { CheckNotDisposed(); @@ -379,12 +363,6 @@ internal string GetNameOfPropertyValue(int index) return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!; } - internal ReadOnlySpan GetRawNameOfPropertyValue(int index) - { - // The property name is one row before the property value - return GetRawSpan(index - DbRow.Size, JsonTokenType.PropertyName)!; - } - internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value) { CheckNotDisposed(); @@ -896,6 +874,53 @@ private static void ClearAndReturn(ArraySegment rented) } } + internal void WritePropertyName(int index, Utf8JsonWriter writer) + { + CheckNotDisposed(); + + WritePropertyName(_parsedData.Get(index - DbRow.Size), writer); + } + private void WritePropertyNameNew(in DbRow row, Utf8JsonWriter writer) + { + // To be determined. + // This method is ~10% faster than the original WritePropertyName + // when the property name contains escaped/Unicode characters. + + Debug.Assert(row.TokenType == JsonTokenType.PropertyName); + int loc = row.Location; + int length = row.SizeOrLength; + ReadOnlySpan rawName = _utf8Json.Slice(loc, length).Span; + + int firstBackSlashIndex = rawName.IndexOf(JsonConstants.BackSlash); + + if (firstBackSlashIndex < 0) + { + writer.WritePropertyName(rawName); + return; + } + + // If the name needs unescaping + + ArraySegment rented = default; + + Span utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ? + stackalloc byte[JsonConstants.StackallocByteThreshold] : + (rented = new ArraySegment(ArrayPool.Shared.Rent(length), 0, length)); + + try + { + int written = 0; + + JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out written); + ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written); + + writer.WritePropertyName(propertyName); + } + finally + { + ClearAndReturn(rented); + } + } private void WritePropertyName(in DbRow row, Utf8JsonWriter writer) { ArraySegment rented = default; @@ -909,7 +934,6 @@ private void WritePropertyName(in DbRow row, Utf8JsonWriter writer) ClearAndReturn(rented); } } - private void WriteString(in DbRow row, Utf8JsonWriter writer) { ArraySegment rented = default; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index f084595b342cc3..bbb26de132123c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1164,18 +1164,6 @@ internal string GetPropertyName() return _parent.GetNameOfPropertyValue(_idx); } - /// - /// Gets the property name exactly as it is in the underlying . - /// - internal ReadOnlySpan GetRawPropertyName() - { - // TODO: Related to issue #77666 "Add JsonElement.ValueSpan" (https://github.com/dotnet/runtime/issues/77666) - - CheckValidInstance(); - - return _parent.GetRawNameOfPropertyValue(_idx); - } - /// /// Gets the original input data backing this value, returning it as a . /// @@ -1337,45 +1325,9 @@ public void WriteTo(Utf8JsonWriter writer) /// Utf8JsonWriter to write internal void WritePropertyNameTo(Utf8JsonWriter writer) { - ReadOnlySpan rawName = GetRawPropertyName(); - int firstBackSlashIndex = rawName.IndexOf(JsonConstants.BackSlash); - - if (firstBackSlashIndex >= 0) - { - // If the name needs unescaping - - // Equivalent to writer.WritePropertyName(JsonReaderHelper.GetUnescapedSpan(rawName)) - // Method is inlined here to avoid .ToArray() allocation for short names. - - int length = rawName.Length; - byte[]? pooledName = null; - - Span utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ? - stackalloc byte[JsonConstants.StackallocByteThreshold] : - (pooledName = ArrayPool.Shared.Rent(length)); - - JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out int written); - Debug.Assert(written > 0); - - ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written); - Debug.Assert(!propertyName.IsEmpty); - - writer.WritePropertyName(propertyName); - - if (pooledName != null) - { - new Span(pooledName, 0, written).Clear(); - ArrayPool.Shared.Return(pooledName); - } - } - else - { - // Note we cannot just write it out by WriteStringByOptionsPropertyName. - // Because there might be plain Unicode chars in the original JsonDocument, - // which should be escaped if it is written out. + CheckValidInstance(); - writer.WritePropertyName(rawName); - } + _parent.WritePropertyName(_idx, writer); } /// From b6c860694008762e77fc0fde05bc6343a032bbaf Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Fri, 11 Aug 2023 12:27:31 +0800 Subject: [PATCH 08/13] fix format --- .../src/System/Text/Json/Document/JsonDocument.cs | 2 ++ .../src/System/Text/Json/Document/JsonElement.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 453019bc944453..acff5f98b65dfa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -921,6 +921,7 @@ private void WritePropertyNameNew(in DbRow row, Utf8JsonWriter writer) ClearAndReturn(rented); } } + private void WritePropertyName(in DbRow row, Utf8JsonWriter writer) { ArraySegment rented = default; @@ -934,6 +935,7 @@ private void WritePropertyName(in DbRow row, Utf8JsonWriter writer) ClearAndReturn(rented); } } + private void WriteString(in DbRow row, Utf8JsonWriter writer) { ArraySegment rented = default; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index bbb26de132123c..37b124a8a18bb1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; From 9644915daae3bed0f0afb1f2e287e70c65734411 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Fri, 11 Aug 2023 12:29:37 +0800 Subject: [PATCH 09/13] fix format 2 --- .../src/System/Text/Json/Document/JsonDocument.cs | 1 + .../src/System/Text/Json/Document/JsonElement.cs | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index acff5f98b65dfa..3d95399a30a281 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -880,6 +880,7 @@ internal void WritePropertyName(int index, Utf8JsonWriter writer) WritePropertyName(_parsedData.Get(index - DbRow.Size), writer); } + private void WritePropertyNameNew(in DbRow row, Utf8JsonWriter writer) { // To be determined. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index 37b124a8a18bb1..c5d687892335aa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1316,12 +1316,6 @@ public void WriteTo(Utf8JsonWriter writer) _parent.WriteElementTo(_idx, writer); } - /// - /// Write the property name of this to a - /// in an allocation-less way, if the name is shorter than - /// or it doesn't require unescaping, and the underlying buffer is long enough. - /// - /// Utf8JsonWriter to write internal void WritePropertyNameTo(Utf8JsonWriter writer) { CheckValidInstance(); From 63da811e7e5e328d4efd853adad8152e5b314492 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Fri, 11 Aug 2023 12:31:39 +0800 Subject: [PATCH 10/13] improve comment --- .../src/System/Text/Json/Document/JsonDocument.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 3d95399a30a281..9e4c9b94aecc74 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -883,9 +883,9 @@ internal void WritePropertyName(int index, Utf8JsonWriter writer) private void WritePropertyNameNew(in DbRow row, Utf8JsonWriter writer) { - // To be determined. - // This method is ~10% faster than the original WritePropertyName - // when the property name contains escaped/Unicode characters. + // To be determined whether to use this or the original WritePropertyName. + // This method is ~10% faster than the original one + // when the property name contains escaped characters. Debug.Assert(row.TokenType == JsonTokenType.PropertyName); int loc = row.Location; From df8e6faee327ab2a0fb453e02050f918380bc2cd Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Thu, 21 Sep 2023 21:35:49 +0800 Subject: [PATCH 11/13] removed unused stub --- .../System/Text/Json/Document/JsonDocument.cs | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 9e4c9b94aecc74..776889de9a7ea2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -881,48 +881,6 @@ internal void WritePropertyName(int index, Utf8JsonWriter writer) WritePropertyName(_parsedData.Get(index - DbRow.Size), writer); } - private void WritePropertyNameNew(in DbRow row, Utf8JsonWriter writer) - { - // To be determined whether to use this or the original WritePropertyName. - // This method is ~10% faster than the original one - // when the property name contains escaped characters. - - Debug.Assert(row.TokenType == JsonTokenType.PropertyName); - int loc = row.Location; - int length = row.SizeOrLength; - ReadOnlySpan rawName = _utf8Json.Slice(loc, length).Span; - - int firstBackSlashIndex = rawName.IndexOf(JsonConstants.BackSlash); - - if (firstBackSlashIndex < 0) - { - writer.WritePropertyName(rawName); - return; - } - - // If the name needs unescaping - - ArraySegment rented = default; - - Span utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ? - stackalloc byte[JsonConstants.StackallocByteThreshold] : - (rented = new ArraySegment(ArrayPool.Shared.Rent(length), 0, length)); - - try - { - int written = 0; - - JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out written); - ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written); - - writer.WritePropertyName(propertyName); - } - finally - { - ClearAndReturn(rented); - } - } - private void WritePropertyName(in DbRow row, Utf8JsonWriter writer) { ArraySegment rented = default; From d2fd37ca67473650e14d6ff663c49d28dce106b5 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Fri, 22 Sep 2023 00:17:01 +0800 Subject: [PATCH 12/13] added assertion --- .../src/System/Text/Json/Document/JsonDocument.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 776889de9a7ea2..17e189cf9ff689 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -878,7 +878,9 @@ internal void WritePropertyName(int index, Utf8JsonWriter writer) { CheckNotDisposed(); - WritePropertyName(_parsedData.Get(index - DbRow.Size), writer); + DbRow row = _parsedData.Get(index - DbRow.Size); + Debug.Assert(row.TokenType == JsonTokenType.PropertyName); + WritePropertyName(row, writer); } private void WritePropertyName(in DbRow row, Utf8JsonWriter writer) From ecca233f8c9a038cedfa1cfa9bf5e9e577d95222 Mon Sep 17 00:00:00 2001 From: Gan Keyu Date: Fri, 22 Sep 2023 00:19:51 +0800 Subject: [PATCH 13/13] removed unnecessary test --- .../JsonPropertyTests.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs index e356e55e0f5554..8193c928342f71 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonPropertyTests.cs @@ -79,25 +79,6 @@ public static void WriteSimpleObject() } } - [Fact] - public static void WriteEscapedNames() - { - var buffer = new ArrayBufferWriter(1024); - const string json = """{"q\t\\mm\t":1,"":2}"""; - using (JsonDocument doc = JsonDocument.Parse(json)) - { - using var writer = new Utf8JsonWriter(buffer); - writer.WriteStartObject(); - foreach (JsonProperty prop in doc.RootElement.EnumerateObject()) - { - prop.WriteTo(writer); - } - writer.WriteEndObject(); - writer.Flush(); - - AssertContents(json, buffer); - } - } private static void AssertContents(string expectedValue, ArrayBufferWriter buffer) { Assert.Equal(