diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs index 4e0f7b36527788..48494efd3f045a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs @@ -86,29 +86,40 @@ private struct MetadataDb : IDisposable internal int Length { get; private set; } private byte[] _data; -#if DEBUG - private readonly bool _isLocked; -#endif + + private bool _convertToAlloc; // Convert the rented data to an alloc when complete. + private bool _isLocked; // Is the array the correct fixed size. + // _isLocked _convertToAlloc truth table: + // false false Standard flow. Size is not known and renting used throughout lifetime. + // true false Used by JsonElement.ParseValue() for primitives and JsonDocument.Clone(). Size is known and no renting. + // false true Used by JsonElement.ParseValue() for arrays and objects. Renting used until size is known. + // true true not valid + + private MetadataDb(byte[] initialDb, bool isLocked, bool convertToAlloc) + { + _data = initialDb; + _isLocked = isLocked; + _convertToAlloc = convertToAlloc; + Length = 0; + } internal MetadataDb(byte[] completeDb) { _data = completeDb; - Length = completeDb.Length; - -#if DEBUG _isLocked = true; -#endif + _convertToAlloc = false; + Length = completeDb.Length; } - internal MetadataDb(int payloadLength) + internal static MetadataDb CreateRented(int payloadLength, bool convertToAlloc) { // Assume that a token happens approximately every 12 bytes. // int estimatedTokens = payloadLength / 12 // now acknowledge that the number of bytes we need per token is 12. // So that's just the payload length. // - // Add one token's worth of data just because. - int initialSize = DbRow.Size + payloadLength; + // Add one row worth of data since we need at least one row for a primitive type. + int initialSize = payloadLength + DbRow.Size; // Stick with ArrayPool's rent/return range if it looks feasible. // If it's wrong, we'll just grow and copy as we would if the tokens @@ -120,30 +131,17 @@ internal MetadataDb(int payloadLength) initialSize = OneMegabyte; } - _data = ArrayPool.Shared.Rent(initialSize); - Length = 0; -#if DEBUG - _isLocked = false; -#endif + byte[] data = ArrayPool.Shared.Rent(initialSize); + return new MetadataDb(data, isLocked: false, convertToAlloc); } - internal MetadataDb(MetadataDb source, bool useArrayPools) + internal static MetadataDb CreateLocked(int payloadLength) { - Length = source.Length; - -#if DEBUG - _isLocked = !useArrayPools; -#endif + // Add one row worth of data since we need at least one row for a primitive type. + int size = payloadLength + DbRow.Size; - if (useArrayPools) - { - _data = ArrayPool.Shared.Rent(Length); - source._data.AsSpan(0, Length).CopyTo(_data); - } - else - { - _data = source._data.AsSpan(0, Length).ToArray(); - } + byte[] data = new byte[size]; + return new MetadataDb(data, isLocked: true, convertToAlloc: false); } public void Dispose() @@ -154,9 +152,7 @@ public void Dispose() return; } -#if DEBUG Debug.Assert(!_isLocked, "Dispose called on a locked database"); -#endif // The data in this rented buffer only conveys the positions and // lengths of tokens in a document, but no content; so it does not @@ -165,28 +161,51 @@ public void Dispose() Length = 0; } - internal void TrimExcess() + /// + /// If using array pools, trim excess if necessary. + /// If not using array pools, release the temporary array pool and alloc. + /// + internal void CompleteAllocations() { - // There's a chance that the size we have is the size we'd get for this - // amount of usage (particularly if Enlarge ever got called); and there's - // the small copy-cost associated with trimming anyways. "Is half-empty" is - // just a rough metric for "is trimming worth it?". - if (Length <= _data.Length / 2) + if (!_isLocked) { - byte[] newRent = ArrayPool.Shared.Rent(Length); - byte[] returnBuf = newRent; - - if (newRent.Length < _data.Length) + if (_convertToAlloc) { - Buffer.BlockCopy(_data, 0, newRent, 0, Length); - returnBuf = _data; - _data = newRent; + Debug.Assert(_data != null); + byte[] returnBuf = _data; + _data = _data.AsSpan(0, Length).ToArray(); + _isLocked = true; + _convertToAlloc = false; + + // The data in this rented buffer only conveys the positions and + // lengths of tokens in a document, but no content; so it does not + // need to be cleared. + ArrayPool.Shared.Return(returnBuf); + } + else + { + // There's a chance that the size we have is the size we'd get for this + // amount of usage (particularly if Enlarge ever got called); and there's + // the small copy-cost associated with trimming anyways. "Is half-empty" is + // just a rough metric for "is trimming worth it?". + if (Length <= _data.Length / 2) + { + byte[] newRent = ArrayPool.Shared.Rent(Length); + byte[] returnBuf = newRent; + + if (newRent.Length < _data.Length) + { + Buffer.BlockCopy(_data, 0, newRent, 0, Length); + returnBuf = _data; + _data = newRent; + } + + // The data in this rented buffer only conveys the positions and + // lengths of tokens in a document, but no content; so it does not + // need to be cleared. + ArrayPool.Shared.Return(returnBuf); + } } - - // The data in this rented buffer only conveys the positions and - // lengths of tokens in a document, but no content; so it does not - // need to be cleared. - ArrayPool.Shared.Return(returnBuf); } } @@ -197,10 +216,6 @@ internal void Append(JsonTokenType tokenType, int startLocation, int length) (tokenType == JsonTokenType.StartArray || tokenType == JsonTokenType.StartObject) == (length == DbRow.UnknownSize)); -#if DEBUG - Debug.Assert(!_isLocked, "Appending to a locked database"); -#endif - if (Length >= _data.Length - DbRow.Size) { Enlarge(); @@ -213,6 +228,8 @@ internal void Append(JsonTokenType tokenType, int startLocation, int length) private void Enlarge() { + Debug.Assert(!_isLocked, "Appending to a locked database"); + byte[] toReturn = _data; _data = ArrayPool.Shared.Rent(toReturn.Length * 2); Buffer.BlockCopy(toReturn, 0, _data, 0, toReturn.Length); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs index 2c6b46ee2240c1..0339ac4e50f9e9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs @@ -12,6 +12,11 @@ namespace System.Text.Json { public sealed partial class JsonDocument { + // Cached unrented documents for literal values. + private static JsonDocument? s_nullLiteral; + private static JsonDocument? s_trueLiteral; + private static JsonDocument? s_falseLiteral; + private const int UnseekableStreamInitialRentSize = 4096; /// @@ -211,7 +216,10 @@ public static JsonDocument Parse(ReadOnlyMemory json, JsonDocumentOptions int actualByteCount = JsonReaderHelper.GetUtf8FromText(jsonChars, utf8Bytes); Debug.Assert(expectedByteCount == actualByteCount); - return Parse(utf8Bytes.AsMemory(0, actualByteCount), options.GetReaderOptions(), utf8Bytes); + return Parse( + utf8Bytes.AsMemory(0, actualByteCount), + options.GetReaderOptions(), + utf8Bytes); } catch { @@ -286,7 +294,7 @@ public static JsonDocument Parse(string json, JsonDocumentOptions options = defa /// public static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] out JsonDocument? document) { - return TryParseValue(ref reader, out document, shouldThrow: false); + return TryParseValue(ref reader, out document, shouldThrow: false, useArrayPools: true); } /// @@ -326,12 +334,17 @@ public static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] /// public static JsonDocument ParseValue(ref Utf8JsonReader reader) { - bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true); + bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: true); + Debug.Assert(ret, "TryParseValue returned false with shouldThrow: true."); return document!; } - private static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] out JsonDocument? document, bool shouldThrow) + internal static bool TryParseValue( + ref Utf8JsonReader reader, + [NotNullWhen(true)] out JsonDocument? document, + bool shouldThrow, + bool useArrayPools) { JsonReaderState state = reader.CurrentState; CheckSupportedOptions(state.Options, nameof(reader)); @@ -367,6 +380,7 @@ private static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] document = null; return false; } + break; } } @@ -414,11 +428,27 @@ private static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] break; } - // Single-token values - case JsonTokenType.Number: - case JsonTokenType.True: case JsonTokenType.False: + case JsonTokenType.True: case JsonTokenType.Null: + if (useArrayPools) + { + if (reader.HasValueSequence) + { + valueSequence = reader.ValueSequence; + } + else + { + valueSpan = reader.ValueSpan; + } + + break; + } + + document = CreateForLiteral(reader.TokenType); + return true; + + case JsonTokenType.Number: { if (reader.HasValueSequence) { @@ -431,6 +461,7 @@ private static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] break; } + // String's ValueSequence/ValueSpan omits the quotes, we need them back. case JsonTokenType.String: { @@ -507,31 +538,74 @@ private static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] } int length = valueSpan.IsEmpty ? checked((int)valueSequence.Length) : valueSpan.Length; - byte[] rented = ArrayPool.Shared.Rent(length); - Span rentedSpan = rented.AsSpan(0, length); + if (useArrayPools) + { + byte[] rented = ArrayPool.Shared.Rent(length); + Span rentedSpan = rented.AsSpan(0, length); - try + try + { + if (valueSpan.IsEmpty) + { + valueSequence.CopyTo(rentedSpan); + } + else + { + valueSpan.CopyTo(rentedSpan); + } + + document = Parse(rented.AsMemory(0, length), state.Options, rented); + } + catch + { + // This really shouldn't happen since the document was already checked + // for consistency by Skip. But if data mutations happened just after + // the calls to Read then the copy may not be valid. + rentedSpan.Clear(); + ArrayPool.Shared.Return(rented); + throw; + } + } + else { + byte[] owned; + if (valueSpan.IsEmpty) { - valueSequence.CopyTo(rentedSpan); + owned = valueSequence.ToArray(); } else { - valueSpan.CopyTo(rentedSpan); + owned = valueSpan.ToArray(); } - document = Parse(rented.AsMemory(0, length), state.Options, rented); - return true; + document = ParseUnrented(owned, state.Options, reader.TokenType); } - catch + + return true; + } + + private static JsonDocument CreateForLiteral(JsonTokenType tokenType) + { + switch (tokenType) { - // This really shouldn't happen since the document was already checked - // for consistency by Skip. But if data mutations happened just after - // the calls to Read then the copy may not be valid. - rentedSpan.Clear(); - ArrayPool.Shared.Return(rented); - throw; + case JsonTokenType.False: + s_falseLiteral ??= Create(JsonConstants.FalseValue.ToArray()); + return s_falseLiteral; + case JsonTokenType.True: + s_trueLiteral ??= Create(JsonConstants.TrueValue.ToArray()); + return s_trueLiteral; + default: + Debug.Assert(tokenType == JsonTokenType.Null); + s_nullLiteral ??= Create(JsonConstants.NullValue.ToArray()); + return s_nullLiteral; + } + + JsonDocument Create(byte[] utf8Json) + { + MetadataDb database = MetadataDb.CreateLocked(utf8Json.Length); + database.Append(tokenType, startLocation: 0, utf8Json.Length); + return new JsonDocument(utf8Json, database, extraRentedBytes: null); } } @@ -541,7 +615,7 @@ private static JsonDocument Parse( byte[]? extraRentedBytes) { ReadOnlySpan utf8JsonSpan = utf8Json.Span; - var database = new MetadataDb(utf8Json.Length); + var database = MetadataDb.CreateRented(utf8Json.Length, convertToAlloc: false); var stack = new StackRowStack(JsonDocumentOptions.DefaultMaxDepth * StackRow.Size); try @@ -561,6 +635,44 @@ private static JsonDocument Parse( return new JsonDocument(utf8Json, database, extraRentedBytes); } + private static JsonDocument ParseUnrented( + ReadOnlyMemory utf8Json, + JsonReaderOptions readerOptions, + JsonTokenType tokenType) + { + // These tokens should already have been processed. + Debug.Assert( + tokenType != JsonTokenType.Null && + tokenType != JsonTokenType.False && + tokenType != JsonTokenType.True); + + ReadOnlySpan utf8JsonSpan = utf8Json.Span; + MetadataDb database; + + if (tokenType == JsonTokenType.String || tokenType == JsonTokenType.Number) + { + // For primitive types, we can avoid renting MetadataDb and creating StackRowStack. + database = MetadataDb.CreateLocked(utf8Json.Length); + StackRowStack stack = default; + Parse(utf8JsonSpan, readerOptions, ref database, ref stack); + } + else + { + database = MetadataDb.CreateRented(utf8Json.Length, convertToAlloc: true); + var stack = new StackRowStack(JsonDocumentOptions.DefaultMaxDepth * StackRow.Size); + try + { + Parse(utf8JsonSpan, readerOptions, ref database, ref stack); + } + finally + { + stack.Dispose(); + } + } + + return new JsonDocument(utf8Json, database, extraRentedBytes: null); + } + private static ArraySegment ReadToEnd(Stream stream) { int written = 0; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.StackRowStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.StackRowStack.cs index 4978148a819f8a..3288e954fad2e3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.StackRowStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.StackRowStack.cs @@ -14,7 +14,7 @@ private struct StackRowStack : IDisposable private byte[] _rentedBuffer; private int _topOfStack; - internal StackRowStack(int initialSize) + public StackRowStack(int initialSize) { _rentedBuffer = ArrayPool.Shared.Rent(initialSize); _topOfStack = _rentedBuffer.Length; @@ -48,7 +48,9 @@ internal void Push(StackRow row) internal StackRow Pop() { - Debug.Assert(_topOfStack <= _rentedBuffer.Length - StackRow.Size); + Debug.Assert(_rentedBuffer != null); + Debug.Assert(_topOfStack <= _rentedBuffer!.Length - StackRow.Size); + StackRow row = MemoryMarshal.Read(_rentedBuffer.AsSpan(_topOfStack)); _topOfStack += StackRow.Size; return row; 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 5a472f5c2607e0..fb120a49ae72d9 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 @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Buffers; using System.Buffers.Text; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; using System.Threading; using System.Diagnostics.CodeAnalysis; @@ -1068,7 +1066,7 @@ private static void Parse( } Debug.Assert(reader.BytesConsumed == utf8JsonSpan.Length); - database.TrimExcess(); + database.CompleteAllocations(); } private void 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 c339aa766c92d1..49215698e7c38a 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 @@ -27,6 +27,17 @@ internal JsonElement(JsonDocument parent, int idx) _idx = idx; } + // Currently used only as an optimization by the serializer, which does not want to + // return elements that are based on the pattern. + internal static JsonElement ParseValue(ref Utf8JsonReader reader) + { + bool ret = JsonDocument.TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: false); + + Debug.Assert(ret != false, "Parse returned false with shouldThrow: true."); + Debug.Assert(document != null, "null document returned with shouldThrow: true."); + return document.RootElement; + } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private JsonTokenType TokenType { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs index a3a0d1ede13728..f371fe920a23c0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs @@ -7,10 +7,7 @@ internal sealed class JsonElementConverter : JsonConverter { public override JsonElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - using (JsonDocument document = JsonDocument.ParseValue(ref reader)) - { - return document.RootElement.Clone(); - } + return JsonElement.ParseValue(ref reader); } public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs index c9e10f9877282a..cf2992344d1803 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs @@ -12,10 +12,7 @@ public ObjectConverter() public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - using (JsonDocument document = JsonDocument.ParseValue(ref reader)) - { - return document.RootElement.Clone(); - } + return JsonElement.ParseValue(ref reader); } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) @@ -48,10 +45,7 @@ private JsonConverter GetRuntimeConverter(Type runtimeType, JsonSerializerOption internal override object ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling) { - using (JsonDocument document = JsonDocument.ParseValue(ref reader)) - { - return document.RootElement.Clone(); - } + return JsonElement.ParseValue(ref reader); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index d5a671dd4794f3..59533e5c542310 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -66,7 +66,7 @@ internal struct WriteStackFrame /// For objects, it is the for the class and current property. /// For collections, it is the for the class and current element. /// - public JsonPropertyInfo? PolymorphicJsonPropertyInfo; + private JsonPropertyInfo? PolymorphicJsonPropertyInfo; // Whether to use custom number handling. public JsonNumberHandling? NumberHandling; @@ -94,16 +94,18 @@ public JsonPropertyInfo GetPolymorphicJsonPropertyInfo() } /// - /// Initializes the state for polymorphic or re-entry cases. + /// Initializes the state for polymorphic cases and returns the appropriate converter. /// - public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName = null) + public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options) { - JsonClassInfo classInfo = options.GetOrAddClass(type); + // For perf, avoid the dictionary lookup in GetOrAddClass() for every element of a collection + // if the current element is the same type as the previous element. + if (PolymorphicJsonPropertyInfo?.RuntimePropertyType != type) + { + JsonClassInfo classInfo = options.GetOrAddClass(type); + PolymorphicJsonPropertyInfo = classInfo.PropertyInfoForClassInfo; + } - // Set for exception handling calculation of JsonPath. - JsonPropertyNameAsString = propertyName; - - PolymorphicJsonPropertyInfo = classInfo.PropertyInfoForClassInfo; return PolymorphicJsonPropertyInfo.ConverterBase; }