Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid string allocation and improve performance of JsonProperty.WriteTo #90074

Merged
merged 13 commits into from
Sep 22, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,22 @@ private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
: JsonReaderHelper.TranscodeHelper(segment);
}

internal ReadOnlySpan<byte> GetRawSpan(int index, JsonTokenType expectedType)
{
CheckNotDisposed();

DbRow row = _parsedData.Get(index);

JsonTokenType tokenType = row.TokenType;

Debug.Assert(tokenType == expectedType);

ReadOnlySpan<byte> data = _utf8Json.Span;
ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);

return segment;
}

internal bool TextEquals(int index, ReadOnlySpan<char> otherText, bool isPropertyName)
{
CheckNotDisposed();
Expand Down Expand Up @@ -363,6 +379,12 @@ internal string GetNameOfPropertyValue(int index)
return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!;
}

internal ReadOnlySpan<byte> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -1163,6 +1164,18 @@ internal string GetPropertyName()
return _parent.GetNameOfPropertyValue(_idx);
}

/// <summary>
/// Gets the property name exactly as it is in the underlying <see cref="JsonDocument"/>.
/// </summary>
internal ReadOnlySpan<byte> GetRawPropertyName()
{
// TODO: Related to issue #77666 "Add JsonElement.ValueSpan" (https://github.com/dotnet/runtime/issues/77666)

CheckValidInstance();

return _parent.GetRawNameOfPropertyValue(_idx);
}

/// <summary>
/// Gets the original input data backing this value, returning it as a <see cref="string"/>.
/// </summary>
Expand Down Expand Up @@ -1316,6 +1329,55 @@ public void WriteTo(Utf8JsonWriter writer)
_parent.WriteElementTo(_idx, writer);
}

/// <summary>
/// Write the property name of this <see cref="JsonElement"/> to a <see cref="Utf8JsonWriter"/>
/// in an allocation-less way, if the name is shorter than <see cref="JsonConstants.StackallocByteThreshold"/>
/// or it doesn't require unescaping, and the underlying buffer is long enough.
/// </summary>
/// <param name="writer">Utf8JsonWriter to write</param>
internal void WritePropertyNameTo(Utf8JsonWriter writer)
{
ReadOnlySpan<byte> 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<byte> utf8Unescaped = length <= JsonConstants.StackallocByteThreshold ?
stackalloc byte[JsonConstants.StackallocByteThreshold] :
(pooledName = ArrayPool<byte>.Shared.Rent(length));

JsonReaderHelper.Unescape(rawName, utf8Unescaped, firstBackSlashIndex, out int written);
Debug.Assert(written > 0);

ReadOnlySpan<byte> propertyName = utf8Unescaped.Slice(0, written);
Debug.Assert(!propertyName.IsEmpty);

writer.WritePropertyName(propertyName);

if (pooledName != null)
{
new Span<byte>(pooledName, 0, written).Clear();
ArrayPool<byte>.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);
}
}

/// <summary>
/// Get an enumerator to enumerate the values in the JSON array represented by this JsonElement.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,15 @@ public void WriteTo(Utf8JsonWriter writer)
ThrowHelper.ThrowArgumentNullException(nameof(writer));
}

writer.WritePropertyName(Name);
if (_name is null)
{
Value.WritePropertyNameTo(writer);
}
else
{
writer.WritePropertyName(_name);
}

Value.WriteTo(writer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ public static void WriteSimpleObject()
}
}

[Fact]
public static void WriteEscapedNames()
{
var buffer = new ArrayBufferWriter<byte>(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<byte> buffer)
{
Assert.Equal(
Expand Down