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,27 @@ private ReadOnlyMemory<byte> GetPropertyRawValue(int valueIndex)
: JsonReaderHelper.TranscodeHelper(segment);
}

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

DbRow row = _parsedData.Get(index);

JsonTokenType tokenType = row.TokenType;

if (tokenType == JsonTokenType.Null)
{
return default;
}

CheckExpectedType(expectedType, tokenType);

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 +384,12 @@ internal string GetNameOfPropertyValue(int index)
return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!;
}

internal ReadOnlySpan<byte> GetRawNameOfPropertyValueAsUtf8Span(int index)
{
// The property name is one row before the property value
return GetRawUtf8Span(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
Expand Up @@ -1163,6 +1163,13 @@ internal string GetPropertyName()
return _parent.GetNameOfPropertyValue(_idx);
}

internal ReadOnlySpan<byte> GetRawPropertyNameAsUtf8Span()
{
CheckValidInstance();

return _parent.GetRawNameOfPropertyValueAsUtf8Span(_idx);
}

/// <summary>
/// Gets the original input data backing this value, returning it as a <see cref="string"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -117,7 +118,46 @@ public void WriteTo(Utf8JsonWriter writer)
ThrowHelper.ThrowArgumentNullException(nameof(writer));
}

writer.WritePropertyName(Name);
if (_name is null)
{
ReadOnlySpan<byte> 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<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
{
writer.WritePropertyName(rawName);
}
}
else
{
writer.WritePropertyName(_name);
}

Value.WriteTo(writer);
}

Expand Down