From 93b659b29e9fcde9a1044d6e4309fb08f126d051 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 21 Nov 2022 17:36:43 +0000 Subject: [PATCH 1/5] Implement a heuristic for using fast-path serialization in streaming JsonSerializer methods. --- .../src/System.Text.Json.csproj | 2 +- .../Text/Json/Serialization/JsonConverter.cs | 5 - .../Serialization/JsonConverterFactory.cs | 11 - .../JsonConverterOfT.WriteCore.cs | 31 -- .../Serialization/JsonSerializer.Helpers.cs | 17 + .../JsonSerializer.Write.ByteArray.cs | 4 +- .../JsonSerializer.Write.Document.cs | 4 +- .../JsonSerializer.Write.Element.cs | 4 +- .../JsonSerializer.Write.Helpers.cs | 127 ------- .../JsonSerializer.Write.Node.cs | 4 +- .../JsonSerializer.Write.Stream.cs | 140 +------- .../JsonSerializer.Write.String.cs | 4 +- .../JsonSerializer.Write.Utf8JsonWriter.cs | 8 +- .../JsonSerializerOptions.Caching.cs | 15 + .../Serialization/Metadata/JsonTypeInfo.cs | 7 + .../Metadata/JsonTypeInfoOfT.WriteHelpers.cs | 334 ++++++++++++++++++ .../Serialization/Metadata/JsonTypeInfoOfT.cs | 2 +- .../Text/Json/Serialization/WriteStack.cs | 15 +- .../Serialization/Stream.WriteTests.cs | 118 ++++++- 19 files changed, 532 insertions(+), 320 deletions(-) delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index b35802fc1187f..7f2d0cfa37463 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -130,6 +130,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -233,7 +234,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index dfd247a58f180..f56357b317472 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -149,11 +149,6 @@ internal static bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) internal abstract bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); - /// - /// Loosely-typed WriteCore() that forwards to strongly-typed WriteCore(). - /// - internal abstract bool WriteCoreAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); - /// /// Loosely-typed WriteToPropertyName() that forwards to strongly-typed WriteToPropertyName(). /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index fc008aed9bfb3..1b7debe0bf1d6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -106,17 +106,6 @@ internal sealed override bool TryWriteAsObject( internal sealed override Type TypeToConvert => null!; - internal sealed override bool WriteCoreAsObject( - Utf8JsonWriter writer, - object? value, - JsonSerializerOptions options, - ref WriteStack state) - { - Debug.Fail("We should never get here."); - - throw new InvalidOperationException(); - } - internal sealed override void WriteAsPropertyNameCoreAsObject( Utf8JsonWriter writer, object value, JsonSerializerOptions options, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs index 7b003325d4dae..3abbd6564067c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs @@ -5,37 +5,6 @@ namespace System.Text.Json.Serialization { public partial class JsonConverter { - internal sealed override bool WriteCoreAsObject( - Utf8JsonWriter writer, - object? value, - JsonSerializerOptions options, - ref WriteStack state) - { - if ( -#if NETCOREAPP - // Treated as a constant by recent versions of the JIT. - typeof(T).IsValueType) -#else - IsValueType) -#endif - { - // Value types can never have a null except for Nullable. - if (default(T) is not null && value is null) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); - } - - // Root object is a boxed value type, we need to push it to the reference stack before it gets unboxed here. - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && value != null) - { - state.ReferenceResolver.PushReferenceForCycleDetection(value); - } - } - - T actualValue = (T)value!; - return WriteCore(writer, actualValue, options, ref state); - } - internal bool WriteCore( Utf8JsonWriter writer, in T value, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs index 0e5c16b05bb65..5713cf079581d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs @@ -54,6 +54,23 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type inpu return info; } + private static void ValidateInputType(object? value, Type inputType) + { + if (inputType is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(inputType)); + } + + if (value is not null) + { + Type runtimeType = value.GetType(); + if (!inputType.IsAssignableFrom(runtimeType)) + { + ThrowHelper.ThrowArgumentException_DeserializeWrongType(inputType, value); + } + } + } + internal static bool IsValidNumberHandlingValue(JsonNumberHandling handling) => JsonHelpers.IsInRangeInclusive((int)handling, 0, (int)( diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs index 74a98ccd0a8b0..5d336a4879fd7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs @@ -124,7 +124,7 @@ private static byte[] WriteBytes(in TValue value, JsonTypeInfo j try { - WriteCore(writer, value, jsonTypeInfo); + jsonTypeInfo.Serialize(writer, value); return output.WrittenMemory.ToArray(); } finally @@ -141,7 +141,7 @@ private static byte[] WriteBytesAsObject(object? value, JsonTypeInfo jsonTypeInf try { - WriteCoreAsObject(writer, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(writer, value); return output.WrittenMemory.ToArray(); } finally diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Document.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Document.cs index 19f02d7ff74a7..644bd0d432500 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Document.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Document.cs @@ -121,7 +121,7 @@ private static JsonDocument WriteDocument(in TValue value, JsonTypeInfo< try { - WriteCore(writer, value, jsonTypeInfo); + jsonTypeInfo.Serialize(writer, value); return JsonDocument.ParseRented(output, options.GetDocumentOptions()); } finally @@ -142,7 +142,7 @@ private static JsonDocument WriteDocumentAsObject(object? value, JsonTypeInfo js try { - WriteCoreAsObject(writer, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(writer, value); return JsonDocument.ParseRented(output, options.GetDocumentOptions()); } finally diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Element.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Element.cs index b04a132189c8b..1fdad1abf6e7c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Element.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Element.cs @@ -119,7 +119,7 @@ private static JsonElement WriteElement(in TValue value, JsonTypeInfo - /// Sync, strongly typed root value serialization helper. - /// - private static void WriteCore( - Utf8JsonWriter writer, - in TValue value, - JsonTypeInfo jsonTypeInfo) - { - if (jsonTypeInfo.CanUseSerializeHandler) - { - // Short-circuit calls into SerializeHandler, if supported. - // Even though this is already handled by JsonMetadataServicesConverter, - // we avoid instantiating a WriteStack and a couple of additional virtual calls. - - Debug.Assert(jsonTypeInfo.SerializeHandler != null); - Debug.Assert(jsonTypeInfo.Options.SerializerContext?.CanUseSerializationLogic == true); - Debug.Assert(jsonTypeInfo.Converter is JsonMetadataServicesConverter); - - jsonTypeInfo.SerializeHandler(writer, value); - } - else - { - WriteStack state = default; - JsonTypeInfo polymorphicTypeInfo = ResolvePolymorphicTypeInfo(value, jsonTypeInfo, out state.IsPolymorphicRootValue); - state.Initialize(polymorphicTypeInfo); - - bool success = - state.IsPolymorphicRootValue - ? polymorphicTypeInfo.Converter.WriteCoreAsObject(writer, value, jsonTypeInfo.Options, ref state) - : jsonTypeInfo.EffectiveConverter.WriteCore(writer, value, jsonTypeInfo.Options, ref state); - - Debug.Assert(success); - } - - writer.Flush(); - } - - /// - /// Sync, untyped root value serialization helper. - /// - private static void WriteCoreAsObject( - Utf8JsonWriter writer, - object? value, - JsonTypeInfo jsonTypeInfo) - { - WriteStack state = default; - JsonTypeInfo polymorphicTypeInfo = ResolvePolymorphicTypeInfo(value, jsonTypeInfo, out state.IsPolymorphicRootValue); - state.Initialize(polymorphicTypeInfo); - - bool success = polymorphicTypeInfo.Converter.WriteCoreAsObject(writer, value, jsonTypeInfo.Options, ref state); - Debug.Assert(success); - writer.Flush(); - } - - /// - /// Streaming root-level serialization helper. - /// - private static bool WriteCore(Utf8JsonWriter writer, in TValue value, JsonTypeInfo jsonTypeInfo, ref WriteStack state) - { - Debug.Assert(state.SupportContinuation); - - bool isFinalBlock; - if (jsonTypeInfo is JsonTypeInfo typedInfo) - { - isFinalBlock = typedInfo.EffectiveConverter.WriteCore(writer, value, jsonTypeInfo.Options, ref state); - } - else - { - // The non-generic API was called. - isFinalBlock = jsonTypeInfo.Converter.WriteCoreAsObject(writer, value, jsonTypeInfo.Options, ref state); - } - - writer.Flush(); - return isFinalBlock; - } - - private static JsonTypeInfo ResolvePolymorphicTypeInfo(in TValue value, JsonTypeInfo jsonTypeInfo, out bool isPolymorphicType) - { - if ( -#if NETCOREAPP - !typeof(TValue).IsValueType && -#endif - jsonTypeInfo.Converter.CanBePolymorphic && value is not null) - { - Debug.Assert(typeof(TValue) == typeof(object)); - - Type runtimeType = value.GetType(); - if (runtimeType != jsonTypeInfo.Type) - { - isPolymorphicType = true; - return jsonTypeInfo.Options.GetTypeInfoForRootType(runtimeType); - } - } - - isPolymorphicType = false; - return jsonTypeInfo; - } - - private static void ValidateInputType(object? value, Type inputType) - { - if (inputType is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(inputType)); - } - - if (value is not null) - { - Type runtimeType = value.GetType(); - if (!inputType.IsAssignableFrom(runtimeType)) - { - ThrowHelper.ThrowArgumentException_DeserializeWrongType(inputType, value); - } - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Node.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Node.cs index e3ef9dcf80ad3..ebb87b26c7c37 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Node.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Node.cs @@ -120,7 +120,7 @@ public static partial class JsonSerializer try { - WriteCore(writer, value, jsonTypeInfo); + jsonTypeInfo.Serialize(writer, value); return JsonNode.Parse(output.WrittenMemory.Span, options.GetNodeOptions(), options.GetDocumentOptions()); } finally @@ -138,7 +138,7 @@ public static partial class JsonSerializer try { - WriteCoreAsObject(writer, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(writer, value); return JsonNode.Parse(output.WrittenMemory.Span, options.GetNodeOptions(), options.GetDocumentOptions()); } finally diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs index 8b2afc9b4675e..107578964faac 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json.Serialization; @@ -19,7 +18,7 @@ public static partial class JsonSerializer // We check for flush after each JSON property and element is written to the buffer. // Once the buffer is expanded to contain the largest single element\property, a 90% threshold // means the buffer may be expanded a maximum of 4 times: 1-(1/(2^4))==.9375. - private const float FlushThreshold = .90f; + internal const float FlushThreshold = .90f; /// /// Converts the provided value to UTF-8 encoded JSON text and write it to the . @@ -50,8 +49,8 @@ public static Task SerializeAsync( ThrowHelper.ThrowArgumentNullException(nameof(utf8Json)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return WriteStreamAsync(utf8Json, value, jsonTypeInfo, cancellationToken); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return jsonTypeInfo.SerializeAsync(utf8Json, value, cancellationToken); } /// @@ -80,8 +79,8 @@ public static void Serialize( ThrowHelper.ThrowArgumentNullException(nameof(utf8Json)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - WriteStream(utf8Json, value, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + jsonTypeInfo.Serialize(utf8Json, value); } /// @@ -119,7 +118,7 @@ public static Task SerializeAsync( ValidateInputType(value, inputType); JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType); - return WriteStreamAsync(utf8Json, value, jsonTypeInfo, cancellationToken); + return jsonTypeInfo.SerializeAsObjectAsync(utf8Json, value, cancellationToken); } /// @@ -154,7 +153,7 @@ public static void Serialize( ValidateInputType(value, inputType); JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType); - WriteStream(utf8Json, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(utf8Json, value); } /// @@ -188,7 +187,8 @@ public static Task SerializeAsync( ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo)); } - return WriteStreamAsync(utf8Json, value, jsonTypeInfo, cancellationToken); + jsonTypeInfo.EnsureConfigured(); + return jsonTypeInfo.SerializeAsync(utf8Json, value, cancellationToken); } /// @@ -219,7 +219,8 @@ public static void Serialize( ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo)); } - WriteStream(utf8Json, value, jsonTypeInfo); + jsonTypeInfo.EnsureConfigured(); + jsonTypeInfo.Serialize(utf8Json, value); } /// @@ -258,11 +259,8 @@ public static Task SerializeAsync( } ValidateInputType(value, inputType); - return WriteStreamAsync( - utf8Json, - value, - GetTypeInfo(context, inputType), - cancellationToken); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, inputType); + return jsonTypeInfo.SerializeAsObjectAsync(utf8Json, value, cancellationToken); } /// @@ -298,116 +296,8 @@ public static void Serialize( } ValidateInputType(value, inputType); - WriteStream(utf8Json, value, GetTypeInfo(context, inputType)); - } - - private static async Task WriteStreamAsync( - Stream utf8Json, - TValue value, - JsonTypeInfo jsonTypeInfo, - CancellationToken cancellationToken) - { - jsonTypeInfo.EnsureConfigured(); - JsonSerializerOptions options = jsonTypeInfo.Options; - JsonWriterOptions writerOptions = options.GetWriterOptions(); - - using (var bufferWriter = new PooledByteBufferWriter(options.DefaultBufferSize)) - using (var writer = new Utf8JsonWriter(bufferWriter, writerOptions)) - { - WriteStack state = default; - jsonTypeInfo = ResolvePolymorphicTypeInfo(value, jsonTypeInfo, out state.IsPolymorphicRootValue); - state.Initialize(jsonTypeInfo, supportAsync: true, supportContinuation: true); - state.CancellationToken = cancellationToken; - - bool isFinalBlock; - - try - { - do - { - state.FlushThreshold = (int)(bufferWriter.Capacity * FlushThreshold); - - try - { - isFinalBlock = WriteCore(writer, value, jsonTypeInfo, ref state); - - if (state.SuppressFlush) - { - Debug.Assert(!isFinalBlock); - Debug.Assert(state.PendingTask is not null); - state.SuppressFlush = false; - } - else - { - await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); - bufferWriter.Clear(); - } - } - finally - { - // Await any pending resumable converter tasks (currently these can only be IAsyncEnumerator.MoveNextAsync() tasks). - // Note that pending tasks are always awaited, even if an exception has been thrown or the cancellation token has fired. - if (state.PendingTask is not null) - { - try - { - await state.PendingTask.ConfigureAwait(false); - } - catch - { - // Exceptions should only be propagated by the resuming converter - // TODO https://github.com/dotnet/runtime/issues/22144 - } - } - - // Dispose any pending async disposables (currently these can only be completed IAsyncEnumerators). - if (state.CompletedAsyncDisposables?.Count > 0) - { - await state.DisposeCompletedAsyncDisposables().ConfigureAwait(false); - } - } - - } while (!isFinalBlock); - } - catch - { - // On exception, walk the WriteStack for any orphaned disposables and try to dispose them. - await state.DisposePendingDisposablesOnExceptionAsync().ConfigureAwait(false); - throw; - } - } - } - - private static void WriteStream( - Stream utf8Json, - in TValue value, - JsonTypeInfo jsonTypeInfo) - { - jsonTypeInfo.EnsureConfigured(); - JsonSerializerOptions options = jsonTypeInfo.Options; - JsonWriterOptions writerOptions = options.GetWriterOptions(); - - using (var bufferWriter = new PooledByteBufferWriter(options.DefaultBufferSize)) - using (var writer = new Utf8JsonWriter(bufferWriter, writerOptions)) - { - WriteStack state = default; - jsonTypeInfo = ResolvePolymorphicTypeInfo(value, jsonTypeInfo, out state.IsPolymorphicRootValue); - state.Initialize(jsonTypeInfo, supportContinuation: true, supportAsync: false); - - bool isFinalBlock; - - do - { - state.FlushThreshold = (int)(bufferWriter.Capacity * FlushThreshold); - - isFinalBlock = WriteCore(writer, value, jsonTypeInfo, ref state); - - bufferWriter.WriteToStream(utf8Json); - bufferWriter.Clear(); - - Debug.Assert(state.PendingTask == null); - } while (!isFinalBlock); - } + JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, inputType); + jsonTypeInfo.SerializeAsObject(utf8Json, value); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs index b83f413f02752..5076cda2594da 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs @@ -137,7 +137,7 @@ private static string WriteString(in TValue value, JsonTypeInfo try { - WriteCore(writer, value, jsonTypeInfo); + jsonTypeInfo.Serialize(writer, value); return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); } finally @@ -154,7 +154,7 @@ private static string WriteStringAsObject(object? value, JsonTypeInfo jsonTypeIn try { - WriteCoreAsObject(writer, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(writer, value); return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); } finally diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs index 20ffe8d767529..4ccd26096fd42 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs @@ -37,7 +37,7 @@ public static void Serialize( } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); - WriteCore(writer, value, jsonTypeInfo); + jsonTypeInfo.Serialize(writer, value); } /// @@ -72,7 +72,7 @@ public static void Serialize( ValidateInputType(value, inputType); JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, inputType); - WriteCoreAsObject(writer, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(writer, value); } /// @@ -101,7 +101,7 @@ public static void Serialize(Utf8JsonWriter writer, TValue value, JsonTy } jsonTypeInfo.EnsureConfigured(); - WriteCore(writer, value, jsonTypeInfo); + jsonTypeInfo.Serialize(writer, value); } /// @@ -138,7 +138,7 @@ public static void Serialize(Utf8JsonWriter writer, object? value, Type inputTyp ValidateInputType(value, inputType); JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, inputType); - WriteCoreAsObject(writer, value, jsonTypeInfo); + jsonTypeInfo.SerializeAsObject(writer, value); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index af1c610e3ee57..b7461d1bacb9a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -106,6 +106,21 @@ internal JsonTypeInfo GetTypeInfoForRootType(Type type) return jsonTypeInfo; } + internal bool TryGetPolymorphicTypeInfoForRootType(object rootValue, [NotNullWhen(true)] out JsonTypeInfo? polymorphicTypeInfo) + { + Debug.Assert(rootValue != null); + + Type runtimeType = rootValue.GetType(); + if (runtimeType != JsonTypeInfo.ObjectType) + { + polymorphicTypeInfo = GetTypeInfoForRootType(runtimeType); + return true; + } + + polymorphicTypeInfo = null; + return false; + } + // Caches the resolved JsonTypeInfo for faster access during root-level object type serialization. internal JsonTypeInfo ObjectTypeInfo { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 48a32b2a4a2dc..3e26508659252 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -4,10 +4,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text.Json.Reflection; using System.Threading; +using System.Threading.Tasks; namespace System.Text.Json.Serialization.Metadata { @@ -763,6 +765,11 @@ private void PopulatePropertyList() internal abstract JsonParameterInfoValues[] GetParameterInfoValues(); + // Untyped, root-level serialization methods + internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue, bool isInvokedByPolymorphicConverter = false); + internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken, bool isInvokedByPolymorphicConverter = false); + internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue, bool isInvokedByPolymorphicConverter = false); + internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary propertyCache, ref Dictionary? ignoredMembers) { Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName can be null in custom JsonPropertyInfo instances and should never be passed in this method"); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs new file mode 100644 index 0000000000000..79067ae8ef199 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs @@ -0,0 +1,334 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization.Converters; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization.Metadata +{ + public partial class JsonTypeInfo + { + // This section provides helper methods guiding root-level serialization + // of values corresponding according to the current JsonTypeInfo configuration. + + // Root serialization method for sync, non-streaming serialization + internal void Serialize( + Utf8JsonWriter writer, + in T? rootValue, + object? rootValueBoxed = null, + bool isInvokedByPolymorphicConverter = false) + { + Debug.Assert(IsConfigured); + Debug.Assert(rootValueBoxed is null || rootValueBoxed is T); + + if (CanUseSerializeHandler) + { + // Short-circuit calls into SerializeHandler, if supported. + // Even though this is already handled by JsonMetadataServicesConverter, + // this avoids creating a WriteStack and calling into the converter infrastructure. + + Debug.Assert(SerializeHandler != null); + Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true); + Debug.Assert(Converter is JsonMetadataServicesConverter); + + SerializeHandler(writer, rootValue!); + writer.Flush(); + } + else if ( +#if NETCOREAPP + !typeof(T).IsValueType && +#endif + Converter.CanBePolymorphic && + rootValue is not null && + Options.TryGetPolymorphicTypeInfoForRootType(rootValue, out JsonTypeInfo? derivedTypeInfo)) + { + Debug.Assert(typeof(T) == typeof(object)); + derivedTypeInfo.SerializeAsObject(writer, rootValue, isInvokedByPolymorphicConverter: true); + // NB flushing is handled by the derived type's serialization method. + } + else + { + WriteStack state = default; + state.Initialize(this, rootValueBoxed, isPolymorphicRootValue: isInvokedByPolymorphicConverter); + + bool success = EffectiveConverter.WriteCore(writer, rootValue!, Options, ref state); + Debug.Assert(success); + writer.Flush(); + } + } + + // Root serialization method for async streaming serialization. + internal async Task SerializeAsync( + Stream utf8Json, + T? rootValue, + CancellationToken cancellationToken, + object? rootValueBoxed = null, + bool isInvokedByPolymorphicConverter = false) + { + Debug.Assert(IsConfigured); + Debug.Assert(rootValueBoxed is null || rootValueBoxed is T); + + if (CanUseSerializeHandlerInStreaming) + { + // Short-circuit calls into SerializeHandler, if the `CanUseSerializeHandlerInStreaming` heuristic allows it. + + Debug.Assert(SerializeHandler != null); + Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true); + Debug.Assert(Converter is JsonMetadataServicesConverter); + + using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize); + using var writer = new Utf8JsonWriter(bufferWriter, Options.GetWriterOptions()); + + try + { + SerializeHandler(writer, rootValue!); + writer.Flush(); + await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); + } + finally + { + OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + } + } + else if ( +#if NETCOREAPP + !typeof(T).IsValueType && +#endif + !isInvokedByPolymorphicConverter && + Converter.CanBePolymorphic && + rootValue is not null && + Options.TryGetPolymorphicTypeInfoForRootType(rootValue, out JsonTypeInfo? derivedTypeInfo)) + { + Debug.Assert(typeof(T) == typeof(object)); + await derivedTypeInfo.SerializeAsObjectAsync(utf8Json, rootValue, cancellationToken, isInvokedByPolymorphicConverter: true).ConfigureAwait(false); + } + else + { + bool isFinalBlock; + WriteStack state = default; + state.Initialize(this, + rootValueBoxed, + isInvokedByPolymorphicConverter, + supportContinuation: true, + supportAsync: true); + + using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize); + using var writer = new Utf8JsonWriter(bufferWriter, Options.GetWriterOptions()); + + try + { + do + { + state.FlushThreshold = (int)(bufferWriter.Capacity * JsonSerializer.FlushThreshold); + + try + { + isFinalBlock = EffectiveConverter.WriteCore(writer, rootValue!, Options, ref state); + writer.Flush(); + + if (state.SuppressFlush) + { + Debug.Assert(!isFinalBlock); + Debug.Assert(state.PendingTask is not null); + state.SuppressFlush = false; + } + else + { + await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); + bufferWriter.Clear(); + } + } + finally + { + // Await any pending resumable converter tasks (currently these can only be IAsyncEnumerator.MoveNextAsync() tasks). + // Note that pending tasks are always awaited, even if an exception has been thrown or the cancellation token has fired. + if (state.PendingTask is not null) + { + try + { + await state.PendingTask.ConfigureAwait(false); + } + catch + { + // Exceptions should only be propagated by the resuming converter + // TODO https://github.com/dotnet/runtime/issues/22144 + } + } + + // Dispose any pending async disposables (currently these can only be completed IAsyncEnumerators). + if (state.CompletedAsyncDisposables?.Count > 0) + { + await state.DisposeCompletedAsyncDisposables().ConfigureAwait(false); + } + } + + } while (!isFinalBlock); + } + catch + { + // On exception, walk the WriteStack for any orphaned disposables and try to dispose them. + await state.DisposePendingDisposablesOnExceptionAsync().ConfigureAwait(false); + throw; + } + finally + { + if (CanUseSerializeHandler) + { + OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + } + } + } + } + + // Root serialization method for non-async streaming serialization + internal void Serialize( + Stream utf8Json, + in T? rootValue, + object? rootValueBoxed = null, + bool isInvokedByPolymorphicConverter = false) + { + Debug.Assert(IsConfigured); + Debug.Assert(rootValueBoxed is null || rootValueBoxed is T); + + if (CanUseSerializeHandlerInStreaming) + { + // Short-circuit calls into SerializeHandler, if the `CanUseSerializeHandlerInStreaming` heuristic allows it. + + Debug.Assert(SerializeHandler != null); + Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true); + Debug.Assert(Converter is JsonMetadataServicesConverter); + + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(Options, out PooledByteBufferWriter bufferWriter); + try + { + SerializeHandler(writer, rootValue!); + writer.Flush(); + bufferWriter.WriteToStream(utf8Json); + } + finally + { + OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, bufferWriter); + } + } + else if ( +#if NETCOREAPP + !typeof(T).IsValueType && +#endif + !isInvokedByPolymorphicConverter && + Converter.CanBePolymorphic && + rootValue is not null && + Options.TryGetPolymorphicTypeInfoForRootType(rootValue, out JsonTypeInfo? polymorphicTypeInfo)) + { + Debug.Assert(typeof(T) == typeof(object)); + polymorphicTypeInfo.SerializeAsObject(utf8Json, rootValue, isInvokedByPolymorphicConverter: true); + } + else + { + bool isFinalBlock; + WriteStack state = default; + state.Initialize(this, + rootValueBoxed, + isInvokedByPolymorphicConverter, + supportContinuation: true, + supportAsync: false); + + using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize); + using var writer = new Utf8JsonWriter(bufferWriter, Options.GetWriterOptions()); + + try + { + do + { + state.FlushThreshold = (int)(bufferWriter.Capacity * JsonSerializer.FlushThreshold); + + isFinalBlock = EffectiveConverter.WriteCore(writer, rootValue!, Options, ref state); + writer.Flush(); + + bufferWriter.WriteToStream(utf8Json); + bufferWriter.Clear(); + + Debug.Assert(state.PendingTask == null); + } while (!isFinalBlock); + } + finally + { + if (CanUseSerializeHandler) + { + OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + } + } + } + } + + internal sealed override void SerializeAsObject(Utf8JsonWriter writer, object? rootValue, bool isInvokedByPolymorphicConverter = false) + => Serialize(writer, UnboxValue(rootValue), rootValue, isInvokedByPolymorphicConverter); + + internal sealed override Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken, bool isInvokedByPolymorphicConverter = false) + => SerializeAsync(utf8Json, UnboxValue(rootValue), cancellationToken, rootValue, isInvokedByPolymorphicConverter); + + internal sealed override void SerializeAsObject(Stream utf8Json, object? rootValue, bool isInvokedByPolymorphicConverter = false) + => Serialize(utf8Json, UnboxValue(rootValue), rootValue, isInvokedByPolymorphicConverter); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T? UnboxValue(object? value) + { + if ( +#if NETCOREAPP + // Treated as a constant by recent versions of the JIT. + typeof(T).IsValueType && +#else + Type.IsValueType && +#endif + default(T) is not null && value is null) + { + // Casting null values to a non-nullable struct throws NullReferenceException, replace with JsonException + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type); + } + + return (T?)value; + } + + // Fast-path serialization in source gen has not been designed with streaming in mind. + // Even though it's not used in streaming by default, we can sometimes try to turn it on + // assuming that the current type is known to produce small enough JSON payloads. + // The `CanUseSerializeHandlerInStreaming` flag returns true iff: + // * The type has been used in at least `MinSerializationsSampleSize` streaming serializations AND + // * No serialization size exceeding JsonSerializerOptions.DefaultBufferSize / 2 has been recorded so far. + private bool CanUseSerializeHandlerInStreaming => _canUseSerializeHandlerInStreamingState == 1; + private volatile int _canUseSerializeHandlerInStreamingState; // 0: unspecified, 1: allowed, 2: forbidden + + private const int MinSerializationsSampleSize = 10; + private volatile int _serializationCount; + + // Samples the latest serialization size for the current type to determine + // if the fast-path SerializeHandler is appropriate for streaming serialization. + private void OnRootLevelAsyncSerializationCompleted(long serializationSize) + { + Debug.Assert(CanUseSerializeHandler); + + if (_canUseSerializeHandlerInStreamingState != 2) + { + if ((ulong)serializationSize > (ulong)(Options.DefaultBufferSize / 2)) + { + // We have a serialization that exceeds the buffer size -- + // forbid any use future use of the fast-path handler. + _canUseSerializeHandlerInStreamingState = 2; + } + else if ((uint)_serializationCount < MinSerializationsSampleSize) + { + if (Interlocked.Increment(ref _serializationCount) == MinSerializationsSampleSize) + { + // We have the minimum number of serializations needed to flag the type as safe for fast-path. + // Use CMPXCHG to avoid racing with threads reporting a large serialization. + Interlocked.CompareExchange(ref _canUseSerializeHandlerInStreamingState, 1, 0); + } + } + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index d5f5962f3298e..df47486963d85 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -10,7 +10,7 @@ namespace System.Text.Json.Serialization.Metadata /// Provides JSON serialization-related metadata about a type. /// /// The generic definition of the type. - public abstract class JsonTypeInfo : JsonTypeInfo + public abstract partial class JsonTypeInfo : JsonTypeInfo { private Action? _serialize; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index e92bd71b55173..1369c99d05898 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -139,7 +139,12 @@ private void EnsurePushCapacity() } } - internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = false, bool supportAsync = false) + internal void Initialize( + JsonTypeInfo jsonTypeInfo, + object? rootValueBoxed = null, + bool isPolymorphicRootValue = false, + bool supportContinuation = false, + bool supportAsync = false) { Debug.Assert(!supportAsync || supportContinuation, "supportAsync must imply supportContinuation"); Debug.Assert(!IsContinuation); @@ -148,6 +153,7 @@ internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = f Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; Current.NumberHandling = Current.JsonPropertyInfo.EffectiveNumberHandling; + IsPolymorphicRootValue = isPolymorphicRootValue; SupportContinuation = supportContinuation; SupportAsync = supportAsync; @@ -156,6 +162,13 @@ internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = f { Debug.Assert(options.ReferenceHandler != null); ReferenceResolver = options.ReferenceHandler.CreateResolver(writing: true); + + if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles && + rootValueBoxed is not null && jsonTypeInfo.Type.IsValueType) + { + // Root object is a boxed value type, we need to push it to the reference stack before starting the serializer. + ReferenceResolver.PushReferenceForCycleDetection(rootValueBoxed); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs index 5aada3e23361c..e10d616f71a10 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text.Json.Serialization.Tests.Schemas.OrderPayload; using System.Threading.Tasks; +using System.Text.Json.Serialization.Metadata; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -12,16 +13,16 @@ namespace System.Text.Json.Serialization.Tests public partial class StreamTests { [Fact] - public static async Task WriteNullArgumentFail() + public async Task WriteNullArgumentFail() { await Assert.ThrowsAsync(async () => await JsonSerializer.SerializeAsync((Stream)null, 1)); await Assert.ThrowsAsync(async () => await JsonSerializer.SerializeAsync((Stream)null, 1, typeof(int))); - Assert.Throws(() => JsonSerializer.Serialize((Stream)null)); + Assert.Throws(() => JsonSerializer.Serialize((Stream)null, 1)); Assert.Throws(() => JsonSerializer.Serialize((Stream)null, 1, typeof(int))); } [Fact] - public static async Task VerifyValueFail() + public async Task VerifyValueFail() { MemoryStream stream = new MemoryStream(); await Assert.ThrowsAsync(async () => await JsonSerializer.SerializeAsync(stream, "", (Type)null)); @@ -29,7 +30,7 @@ public static async Task VerifyValueFail() } [Fact] - public static async Task VerifyTypeFail() + public async Task VerifyTypeFail() { MemoryStream stream = new MemoryStream(); await Assert.ThrowsAsync(async () => await JsonSerializer.SerializeAsync(stream, 1, typeof(string))); @@ -407,6 +408,115 @@ public async Task NestedJsonFileCircularDependencyTest(int depthFactor) } } + [Theory] + [InlineData(32)] + [InlineData(128)] + [InlineData(1024)] + [InlineData(1024 * 16)] // the default JsonSerializerOptions.DefaultBufferSize value + [InlineData(1024 * 1024)] + public async Task ShouldUseFastPathOnSmallPayloads(int defaultBufferSize) + { + var instrumentedResolver = new PocoWithInstrumentedFastPath.Context( + new JsonSerializerOptions + { + DefaultBufferSize = defaultBufferSize, + }); + + // The current implementation uses a heuristic + int smallValueThreshold = defaultBufferSize / 2; + PocoWithInstrumentedFastPath smallValue = CreateValueWithSerializationSize(smallValueThreshold); + + var stream = new MemoryStream(); + JsonTypeInfo jsonTypeInfo = (JsonTypeInfo)instrumentedResolver.GetTypeInfo(typeof(PocoWithInstrumentedFastPath)); + + // The first 10 serializations should not call into the fast path + for (int i = 0; i < 10; i++) + { + await Serializer.SerializeWrapper(stream, smallValue, jsonTypeInfo); + stream.Position = 0; + Assert.Equal(0, instrumentedResolver.FastPathInvocationCount); + } + + // Subsequent iterations do call into the fast path + for (int i = 0; i < 10; i++) + { + await Serializer.SerializeWrapper(stream, smallValue, jsonTypeInfo); + stream.Position = 0; + Assert.Equal(i + 1, instrumentedResolver.FastPathInvocationCount); + } + + // Attempt to serialize a value that is deemed large + var largeValue = CreateValueWithSerializationSize(smallValueThreshold + 1); + await Serializer.SerializeWrapper(stream, largeValue, jsonTypeInfo); + stream.Position = 0; + Assert.Equal(11, instrumentedResolver.FastPathInvocationCount); + + // Any subsequent attempts no longer call into the fast path + for (int i = 0; i < 10; i++) + { + await Serializer.SerializeWrapper(stream, smallValue, jsonTypeInfo); + stream.Position = 0; + Assert.Equal(11, instrumentedResolver.FastPathInvocationCount); + } + + static PocoWithInstrumentedFastPath CreateValueWithSerializationSize(int targetSerializationSize) + { + int objectSerializationPaddingSize = """{"Value":""}""".Length; // 12 + return new PocoWithInstrumentedFastPath { Value = new string('a', targetSerializationSize - objectSerializationPaddingSize) }; + } + } + + public class PocoWithInstrumentedFastPath + { + public string? Value { get; set; } + + public class Context : JsonSerializerContext + { + public int FastPathInvocationCount { get; private set; } + + public Context(JsonSerializerOptions options) : base(options) + { } + + protected override JsonSerializerOptions? GeneratedSerializerOptions => Options; + + public override JsonTypeInfo? GetTypeInfo(Type type) + { + if (type == typeof(string)) + { + return JsonMetadataServices.CreateValueInfo(Options, JsonMetadataServices.StringConverter); + } + + if (type == typeof(PocoWithInstrumentedFastPath)) + { + return JsonMetadataServices.CreateObjectInfo(Options, + new JsonObjectInfoValues + { + PropertyMetadataInitializer = _ => new JsonPropertyInfo[1] + { + JsonMetadataServices.CreatePropertyInfo(Options, + new JsonPropertyInfoValues + { + DeclaringType = typeof(PocoWithInstrumentedFastPath), + PropertyName = "Value", + Getter = obj => ((PocoWithInstrumentedFastPath)obj).Value, + }) + }, + + SerializeHandler = (writer, value) => + { + writer.WriteStartObject(); + writer.WriteString("Value", value.Value); + writer.WriteEndObject(); + FastPathInvocationCount++; + } + }); + } + + return null; + } + } + } + [Theory] [InlineData(128)] [InlineData(1024)] From 7ba5175917f5d027aa2fbd6730264cd975b968a0 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 24 Nov 2022 13:46:39 +0000 Subject: [PATCH 2/5] Add testing for polymorphic serialization --- .../Common/StreamingJsonSerializerWrapper.cs | 1 + .../JsonSerializerWrapper.Reflection.cs | 2 + .../Serialization/Stream.WriteTests.cs | 39 +++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/Common/StreamingJsonSerializerWrapper.cs b/src/libraries/System.Text.Json/tests/Common/StreamingJsonSerializerWrapper.cs index 5b8f1c9ac3955..ce57ceeb0e2ed 100644 --- a/src/libraries/System.Text.Json/tests/Common/StreamingJsonSerializerWrapper.cs +++ b/src/libraries/System.Text.Json/tests/Common/StreamingJsonSerializerWrapper.cs @@ -16,6 +16,7 @@ public abstract partial class StreamingJsonSerializerWrapper : JsonSerializerWra /// True if the serializer is streaming data synchronously. /// public abstract bool IsAsyncSerializer { get; } + public virtual bool ForceSmallBufferInOptions { get; } = false; public abstract Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerOptions? options = null); public abstract Task SerializeWrapper(Stream stream, T value, JsonSerializerOptions? options = null); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs index 6296522dd91d3..49d6babb2fd93 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs @@ -124,6 +124,7 @@ private class AsyncStreamSerializerWrapper : StreamingJsonSerializerWrapper private readonly bool _forceBomInsertions; public override bool IsAsyncSerializer => true; + public override bool ForceSmallBufferInOptions => _forceSmallBufferInOptions; public AsyncStreamSerializerWrapper(bool forceSmallBufferInOptions = false, bool forceBomInsertions = false) { @@ -184,6 +185,7 @@ private class SyncStreamSerializerWrapper : StreamingJsonSerializerWrapper private readonly bool _forceBomInsertions; public override bool IsAsyncSerializer => false; + public override bool ForceSmallBufferInOptions => _forceSmallBufferInOptions; public SyncStreamSerializerWrapper(bool forceSmallBufferInOptions = false, bool forceBomInsertions = false) { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs index e10d616f71a10..d1062f12febdf 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.WriteTests.cs @@ -416,6 +416,11 @@ public async Task NestedJsonFileCircularDependencyTest(int depthFactor) [InlineData(1024 * 1024)] public async Task ShouldUseFastPathOnSmallPayloads(int defaultBufferSize) { + if (Serializer.ForceSmallBufferInOptions) + { + return; + } + var instrumentedResolver = new PocoWithInstrumentedFastPath.Context( new JsonSerializerOptions { @@ -427,12 +432,11 @@ public async Task ShouldUseFastPathOnSmallPayloads(int defaultBufferSize) PocoWithInstrumentedFastPath smallValue = CreateValueWithSerializationSize(smallValueThreshold); var stream = new MemoryStream(); - JsonTypeInfo jsonTypeInfo = (JsonTypeInfo)instrumentedResolver.GetTypeInfo(typeof(PocoWithInstrumentedFastPath)); // The first 10 serializations should not call into the fast path for (int i = 0; i < 10; i++) { - await Serializer.SerializeWrapper(stream, smallValue, jsonTypeInfo); + await Serializer.SerializeWrapper(stream, smallValue, instrumentedResolver.Options); stream.Position = 0; Assert.Equal(0, instrumentedResolver.FastPathInvocationCount); } @@ -440,23 +444,28 @@ public async Task ShouldUseFastPathOnSmallPayloads(int defaultBufferSize) // Subsequent iterations do call into the fast path for (int i = 0; i < 10; i++) { - await Serializer.SerializeWrapper(stream, smallValue, jsonTypeInfo); + await Serializer.SerializeWrapper(stream, smallValue, instrumentedResolver.Options); stream.Position = 0; Assert.Equal(i + 1, instrumentedResolver.FastPathInvocationCount); } + // Polymorphic serialization should use the fast path + await Serializer.SerializeWrapper(stream, (object)smallValue, instrumentedResolver.Options); + stream.Position = 0; + Assert.Equal(11, instrumentedResolver.FastPathInvocationCount); + // Attempt to serialize a value that is deemed large var largeValue = CreateValueWithSerializationSize(smallValueThreshold + 1); - await Serializer.SerializeWrapper(stream, largeValue, jsonTypeInfo); + await Serializer.SerializeWrapper(stream, largeValue, instrumentedResolver.Options); stream.Position = 0; - Assert.Equal(11, instrumentedResolver.FastPathInvocationCount); + Assert.Equal(12, instrumentedResolver.FastPathInvocationCount); // Any subsequent attempts no longer call into the fast path for (int i = 0; i < 10; i++) { - await Serializer.SerializeWrapper(stream, smallValue, jsonTypeInfo); + await Serializer.SerializeWrapper(stream, smallValue, instrumentedResolver.Options); stream.Position = 0; - Assert.Equal(11, instrumentedResolver.FastPathInvocationCount); + Assert.Equal(12, instrumentedResolver.FastPathInvocationCount); } static PocoWithInstrumentedFastPath CreateValueWithSerializationSize(int targetSerializationSize) @@ -470,7 +479,7 @@ public class PocoWithInstrumentedFastPath { public string? Value { get; set; } - public class Context : JsonSerializerContext + public class Context : JsonSerializerContext, IJsonTypeInfoResolver { public int FastPathInvocationCount { get; private set; } @@ -478,22 +487,28 @@ public Context(JsonSerializerOptions options) : base(options) { } protected override JsonSerializerOptions? GeneratedSerializerOptions => Options; + public override JsonTypeInfo? GetTypeInfo(Type type) => GetTypeInfo(type, Options); - public override JsonTypeInfo? GetTypeInfo(Type type) + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) { if (type == typeof(string)) { - return JsonMetadataServices.CreateValueInfo(Options, JsonMetadataServices.StringConverter); + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.StringConverter); + } + + if (type == typeof(object)) + { + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.ObjectConverter); } if (type == typeof(PocoWithInstrumentedFastPath)) { - return JsonMetadataServices.CreateObjectInfo(Options, + return JsonMetadataServices.CreateObjectInfo(options, new JsonObjectInfoValues { PropertyMetadataInitializer = _ => new JsonPropertyInfo[1] { - JsonMetadataServices.CreatePropertyInfo(Options, + JsonMetadataServices.CreatePropertyInfo(options, new JsonPropertyInfoValues { DeclaringType = typeof(PocoWithInstrumentedFastPath), From 0d4374a6e9619735f2e9c4ef2285048df5db3112 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 24 Nov 2022 21:47:56 +0000 Subject: [PATCH 3/5] Apply refactorings to root-level deserialization methods similar to serialization for consistency. --- .../src/System.Text.Json.csproj | 2 +- .../Text/Json/Serialization/JsonConverter.cs | 6 - .../Serialization/JsonConverterFactory.cs | 10 - .../JsonConverterOfT.ReadCore.cs | 10 +- .../JsonSerializer.Read.Document.cs | 18 +- .../JsonSerializer.Read.Element.cs | 18 +- .../JsonSerializer.Read.Helpers.cs | 59 ------ .../Serialization/JsonSerializer.Read.Node.cs | 35 +++- .../Serialization/JsonSerializer.Read.Span.cs | 45 ++++- .../JsonSerializer.Read.Stream.cs | 165 ++-------------- .../JsonSerializer.Read.String.cs | 55 ++++-- .../JsonSerializer.Read.Utf8JsonReader.cs | 35 +++- .../Serialization/Metadata/JsonTypeInfo.cs | 5 + .../Metadata/JsonTypeInfoOfT.ReadHelper.cs | 176 ++++++++++++++++++ 14 files changed, 352 insertions(+), 287 deletions(-) delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 7f2d0cfa37463..1ccdc0bee064a 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -130,6 +130,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -227,7 +228,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index f56357b317472..0fc70fd5b9818 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -129,12 +129,6 @@ internal virtual JsonTypeInfo CreateCustomJsonTypeInfo(JsonSerializerOptions opt /// internal bool IsInternalConverterForNumberType { get; init; } - /// - /// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore(). - /// - internal abstract object? ReadCoreAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, scoped ref ReadStack state); - - internal static bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) { // If surpassed flush threshold then return false which will flush stream. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index 1b7debe0bf1d6..b5c70e64bc243 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -61,16 +61,6 @@ internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOp return converter; } - internal sealed override object ReadCoreAsObject( - ref Utf8JsonReader reader, - JsonSerializerOptions options, - scoped ref ReadStack state) - { - Debug.Fail("We should never get here."); - - throw new InvalidOperationException(); - } - internal sealed override bool OnTryReadAsObject( ref Utf8JsonReader reader, JsonSerializerOptions options, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs index 975df8ae20227..40b6bd169fe67 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs @@ -5,18 +5,10 @@ namespace System.Text.Json.Serialization { public partial class JsonConverter { - internal sealed override object? ReadCoreAsObject( - ref Utf8JsonReader reader, - JsonSerializerOptions options, - scoped ref ReadStack state) - { - return ReadCore(ref reader, options, ref state); - } - internal T? ReadCore( ref Utf8JsonReader reader, JsonSerializerOptions options, - scoped ref ReadStack state) + ref ReadStack state) { try { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Document.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Document.cs index 58dd06cf6b0de..74361b7a17294 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Document.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Document.cs @@ -35,8 +35,9 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(document)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadDocument(document, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + ReadOnlySpan utf8Json = document.GetRootRawValue().Span; + return ReadFromSpan(utf8Json, jsonTypeInfo); } /// @@ -70,7 +71,8 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadDocument(document, jsonTypeInfo); + ReadOnlySpan utf8Json = document.GetRootRawValue().Span; + return ReadFromSpanAsObject(utf8Json, jsonTypeInfo); } /// @@ -106,7 +108,8 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadDocument(document, jsonTypeInfo); + ReadOnlySpan utf8Json = document.GetRootRawValue().Span; + return ReadFromSpan(utf8Json, jsonTypeInfo); } /// @@ -161,13 +164,8 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadDocument(document, jsonTypeInfo); - } - - private static TValue? ReadDocument(JsonDocument document, JsonTypeInfo jsonTypeInfo) - { ReadOnlySpan utf8Json = document.GetRootRawValue().Span; - return ReadFromSpan(utf8Json, jsonTypeInfo); + return ReadFromSpanAsObject(utf8Json, jsonTypeInfo); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Element.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Element.cs index debacec0ec8b3..ffa12ec7ac5d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Element.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Element.cs @@ -27,8 +27,9 @@ public static partial class JsonSerializer [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] public static TValue? Deserialize(this JsonElement element, JsonSerializerOptions? options = null) { - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadUsingMetadata(element, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + ReadOnlySpan utf8Json = element.GetRawValue().Span; + return ReadFromSpan(utf8Json, jsonTypeInfo); } /// @@ -58,7 +59,8 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadUsingMetadata(element, jsonTypeInfo); + ReadOnlySpan utf8Json = element.GetRawValue().Span; + return ReadFromSpanAsObject(utf8Json, jsonTypeInfo); } /// @@ -86,7 +88,8 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadUsingMetadata(element, jsonTypeInfo); + ReadOnlySpan utf8Json = element.GetRawValue().Span; + return ReadFromSpan(utf8Json, jsonTypeInfo); } /// @@ -133,13 +136,8 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadUsingMetadata(element, jsonTypeInfo); - } - - private static TValue? ReadUsingMetadata(JsonElement element, JsonTypeInfo jsonTypeInfo) - { ReadOnlySpan utf8Json = element.GetRawValue().Span; - return ReadFromSpan(utf8Json, jsonTypeInfo); + return ReadFromSpanAsObject(utf8Json, jsonTypeInfo); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs deleted file mode 100644 index 318e25da2bc7a..0000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - private static TValue? ReadCore(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo, scoped ref ReadStack state) - { - if (jsonTypeInfo is JsonTypeInfo typedInfo) - { - // Call the strongly-typed ReadCore that will not box structs. - return typedInfo.EffectiveConverter.ReadCore(ref reader, typedInfo.Options, ref state); - } - - // The non-generic API was called. - object? value = jsonTypeInfo.Converter.ReadCoreAsObject(ref reader, jsonTypeInfo.Options, ref state); - Debug.Assert(value is null or TValue); - return (TValue?)value; - } - - private static TValue? ReadFromSpan(ReadOnlySpan utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null) - { - Debug.Assert(jsonTypeInfo.IsConfigured); - - JsonSerializerOptions options = jsonTypeInfo.Options; - - var readerState = new JsonReaderState(options.GetReaderOptions()); - var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState); - - ReadStack state = default; - state.Initialize(jsonTypeInfo); - - TValue? value; - - // For performance, the code below is a lifted ReadCore() above. - if (jsonTypeInfo is JsonTypeInfo typedInfo) - { - // Call the strongly-typed ReadCore that will not box structs. - value = typedInfo.EffectiveConverter.ReadCore(ref reader, options, ref state); - } - else - { - // The non-generic API was called. - object? objValue = jsonTypeInfo.Converter.ReadCoreAsObject(ref reader, options, ref state); - Debug.Assert(objValue is null or TValue); - value = (TValue?)objValue; - } - - // The reader should have thrown if we have remaining bytes. - Debug.Assert(reader.BytesConsumed == (actualByteCount ?? utf8Json.Length)); - return value; - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Node.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Node.cs index 21a367264f54b..562faa69c07f1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Node.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Node.cs @@ -29,8 +29,8 @@ public static partial class JsonSerializer [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] public static TValue? Deserialize(this JsonNode? node, JsonSerializerOptions? options = null) { - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadNode(node, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return ReadFromNode(node, jsonTypeInfo); } /// @@ -57,7 +57,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadNode(node, jsonTypeInfo); + return ReadFromNodeAsObject(node, jsonTypeInfo); } /// @@ -85,7 +85,7 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadNode(node, jsonTypeInfo); + return ReadFromNode(node, jsonTypeInfo); } /// @@ -132,10 +132,10 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadNode(node, jsonTypeInfo); + return ReadFromNodeAsObject(node, jsonTypeInfo); } - private static TValue? ReadNode(JsonNode? node, JsonTypeInfo jsonTypeInfo) + private static TValue? ReadFromNode(JsonNode? node, JsonTypeInfo jsonTypeInfo) { JsonSerializerOptions options = jsonTypeInfo.Options; @@ -153,7 +153,28 @@ public static partial class JsonSerializer } } - return ReadFromSpan(output.WrittenMemory.Span, jsonTypeInfo); + return ReadFromSpan(output.WrittenMemory.Span, jsonTypeInfo); + } + + private static object? ReadFromNodeAsObject(JsonNode? node, JsonTypeInfo jsonTypeInfo) + { + JsonSerializerOptions options = jsonTypeInfo.Options; + + // For performance, share the same buffer across serialization and deserialization. + using var output = new PooledByteBufferWriter(options.DefaultBufferSize); + using (var writer = new Utf8JsonWriter(output, options.GetWriterOptions())) + { + if (node is null) + { + writer.WriteNullValue(); + } + else + { + node.WriteTo(writer, options); + } + } + + return ReadFromSpanAsObject(output.WrittenMemory.Span, jsonTypeInfo); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs index 2e0f50c260957..673913e71c5e3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -29,8 +30,8 @@ public static partial class JsonSerializer [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] public static TValue? Deserialize(ReadOnlySpan utf8Json, JsonSerializerOptions? options = null) { - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadFromSpan(utf8Json, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return ReadFromSpan(utf8Json, jsonTypeInfo); } /// @@ -62,7 +63,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadFromSpan(utf8Json, jsonTypeInfo); + return ReadFromSpanAsObject(utf8Json, jsonTypeInfo); } /// @@ -89,7 +90,7 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadFromSpan(utf8Json, jsonTypeInfo); + return ReadFromSpan(utf8Json, jsonTypeInfo); } /// @@ -126,7 +127,41 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(context)); } - return ReadFromSpan(utf8Json, GetTypeInfo(context, returnType)); + return ReadFromSpanAsObject(utf8Json, GetTypeInfo(context, returnType)); + } + + private static TValue? ReadFromSpan(ReadOnlySpan utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + + var readerState = new JsonReaderState(jsonTypeInfo.Options.GetReaderOptions()); + var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState); + + ReadStack state = default; + state.Initialize(jsonTypeInfo); + + TValue? value = jsonTypeInfo.Deserialize(ref reader, ref state); + + // The reader should have thrown if we have remaining bytes. + Debug.Assert(reader.BytesConsumed == (actualByteCount ?? utf8Json.Length)); + return value; + } + + private static object? ReadFromSpanAsObject(ReadOnlySpan utf8Json, JsonTypeInfo jsonTypeInfo, int? actualByteCount = null) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + + var readerState = new JsonReaderState(jsonTypeInfo.Options.GetReaderOptions()); + var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState); + + ReadStack state = default; + state.Initialize(jsonTypeInfo); + + object? value = jsonTypeInfo.DeserializeAsObject(ref reader, ref state); + + // The reader should have thrown if we have remaining bytes. + Debug.Assert(reader.BytesConsumed == (actualByteCount ?? utf8Json.Length)); + return value; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index 2690a5f4c856a..de0dd0a078570 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -51,8 +51,8 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(utf8Json)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadFromStreamAsync(utf8Json, jsonTypeInfo, cancellationToken); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return jsonTypeInfo.DeserializeAsync(utf8Json, cancellationToken); } /// @@ -86,8 +86,8 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(utf8Json)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadFromStream(utf8Json, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return jsonTypeInfo.Deserialize(utf8Json); } /// @@ -131,7 +131,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadFromStreamAsync(utf8Json, jsonTypeInfo, cancellationToken); + return jsonTypeInfo.DeserializeAsObjectAsync(utf8Json, cancellationToken); } /// @@ -171,7 +171,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadFromStream(utf8Json, jsonTypeInfo); + return jsonTypeInfo.DeserializeAsObject(utf8Json); } /// @@ -212,7 +212,7 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadFromStreamAsync(utf8Json, jsonTypeInfo, cancellationToken); + return jsonTypeInfo.DeserializeAsync(utf8Json, cancellationToken); } /// @@ -249,7 +249,7 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadFromStream(utf8Json, jsonTypeInfo); + return jsonTypeInfo.Deserialize(utf8Json); } /// @@ -299,7 +299,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadFromStreamAsync(utf8Json, jsonTypeInfo, cancellationToken); + return jsonTypeInfo.DeserializeAsObjectAsync(utf8Json, cancellationToken); } /// @@ -345,7 +345,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadFromStream(utf8Json, jsonTypeInfo); + return jsonTypeInfo.DeserializeAsObject(utf8Json); } /// @@ -373,8 +373,8 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(utf8Json)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return CreateAsyncEnumerableDeserializer(utf8Json, CreateQueueTypeInfo(jsonTypeInfo), cancellationToken); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return jsonTypeInfo.DeserializeAsyncEnumerable(utf8Json, cancellationToken); } /// @@ -405,145 +405,8 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(jsonTypeInfo)); } - return CreateAsyncEnumerableDeserializer(utf8Json, CreateQueueTypeInfo(jsonTypeInfo), cancellationToken); - } - - private static JsonTypeInfo> CreateQueueTypeInfo(JsonTypeInfo jsonTypeInfo) - { - JsonTypeInfo> queueTypeInfo = JsonMetadataServices.CreateQueueInfo, TValue>( - options: jsonTypeInfo.Options, - collectionInfo: new() - { - ObjectCreator = static () => new Queue(), - ElementInfo = jsonTypeInfo, - NumberHandling = jsonTypeInfo.Options.NumberHandling - }); - - queueTypeInfo.EnsureConfigured(); - return queueTypeInfo; - } - - private static async IAsyncEnumerable CreateAsyncEnumerableDeserializer( - Stream utf8Json, - JsonTypeInfo> queueTypeInfo, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - Debug.Assert(queueTypeInfo.IsConfigured); - JsonSerializerOptions options = queueTypeInfo.Options; - var bufferState = new ReadBufferState(options.DefaultBufferSize); - ReadStack readStack = default; - readStack.Initialize(queueTypeInfo, supportContinuation: true); - - var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); - - try - { - do - { - bufferState = await bufferState.ReadFromStreamAsync(utf8Json, cancellationToken, fillBuffer: false).ConfigureAwait(false); - ContinueDeserialize>( - ref bufferState, - ref jsonReaderState, - ref readStack, - queueTypeInfo); - - if (readStack.Current.ReturnValue is Queue queue) - { - while (queue.Count > 0) - { - yield return queue.Dequeue(); - } - } - } - while (!bufferState.IsFinalBlock); - } - finally - { - bufferState.Dispose(); - } - } - - internal static async ValueTask ReadFromStreamAsync( - Stream utf8Json, - JsonTypeInfo jsonTypeInfo, - CancellationToken cancellationToken) - { - Debug.Assert(jsonTypeInfo.IsConfigured); - JsonSerializerOptions options = jsonTypeInfo.Options; - var bufferState = new ReadBufferState(options.DefaultBufferSize); - ReadStack readStack = default; - readStack.Initialize(jsonTypeInfo, supportContinuation: true); - var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); - - try - { - while (true) - { - bufferState = await bufferState.ReadFromStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); - TValue? value = ContinueDeserialize(ref bufferState, ref jsonReaderState, ref readStack, jsonTypeInfo); - - if (bufferState.IsFinalBlock) - { - return value; - } - } - } - finally - { - bufferState.Dispose(); - } - } - - internal static TValue? ReadFromStream( - Stream utf8Json, - JsonTypeInfo jsonTypeInfo) - { - Debug.Assert(jsonTypeInfo.IsConfigured); - JsonSerializerOptions options = jsonTypeInfo.Options; - var bufferState = new ReadBufferState(options.DefaultBufferSize); - ReadStack readStack = default; - readStack.Initialize(jsonTypeInfo, supportContinuation: true); - var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); - - try - { - while (true) - { - bufferState.ReadFromStream(utf8Json); - TValue? value = ContinueDeserialize(ref bufferState, ref jsonReaderState, ref readStack, jsonTypeInfo); - - if (bufferState.IsFinalBlock) - { - return value; - } - } - } - finally - { - bufferState.Dispose(); - } - } - - internal static TValue? ContinueDeserialize( - ref ReadBufferState bufferState, - ref JsonReaderState jsonReaderState, - ref ReadStack readStack, - JsonTypeInfo jsonTypeInfo) - { - var reader = new Utf8JsonReader(bufferState.Bytes, bufferState.IsFinalBlock, jsonReaderState); - - // If we haven't read in the entire stream's payload we'll need to signify that we want - // to enable read ahead behaviors to ensure we have complete json objects and arrays - // ({}, []) when needed. (Notably to successfully parse JsonElement via JsonDocument - // to assign to object and JsonElement properties in the constructed .NET object.) - readStack.ReadAhead = !bufferState.IsFinalBlock; - readStack.BytesConsumed = 0; - - TValue? value = ReadCore(ref reader, jsonTypeInfo, ref readStack); - Debug.Assert(readStack.BytesConsumed <= bufferState.Bytes.Length); - bufferState.AdvanceBuffer((int)readStack.BytesConsumed); - jsonReaderState = reader.CurrentState; - return value; + jsonTypeInfo.EnsureConfigured(); + return jsonTypeInfo.DeserializeAsyncEnumerable(utf8Json, cancellationToken); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs index 1a307f31936c0..b8772db69421e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs @@ -51,8 +51,8 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(json)); } - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadFromSpan(json.AsSpan(), jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return ReadFromSpan(json.AsSpan(), jsonTypeInfo); } /// @@ -83,10 +83,8 @@ public static partial class JsonSerializer [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] public static TValue? Deserialize([StringSyntax(StringSyntaxAttribute.Json)] ReadOnlySpan json, JsonSerializerOptions? options = null) { - // default/null span is treated as empty - - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); - return ReadFromSpan(json, jsonTypeInfo); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + return ReadFromSpan(json, jsonTypeInfo); } /// @@ -130,7 +128,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadFromSpan(json.AsSpan(), jsonTypeInfo)!; + return ReadFromSpanAsObject(json.AsSpan(), jsonTypeInfo); } /// @@ -172,7 +170,7 @@ public static partial class JsonSerializer // default/null span is treated as empty JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return ReadFromSpan(json, jsonTypeInfo)!; + return ReadFromSpanAsObject(json, jsonTypeInfo); } /// @@ -218,7 +216,7 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadFromSpan(json.AsSpan(), jsonTypeInfo); + return ReadFromSpan(json.AsSpan(), jsonTypeInfo); } /// @@ -260,7 +258,7 @@ public static partial class JsonSerializer } jsonTypeInfo.EnsureConfigured(); - return ReadFromSpan(json, jsonTypeInfo); + return ReadFromSpan(json, jsonTypeInfo); } /// @@ -314,7 +312,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadFromSpan(json.AsSpan(), jsonTypeInfo); + return ReadFromSpanAsObject(json.AsSpan(), jsonTypeInfo); } /// @@ -364,10 +362,39 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, returnType); - return ReadFromSpan(json, jsonTypeInfo); + return ReadFromSpanAsObject(json, jsonTypeInfo); + } + + private static TValue? ReadFromSpan(ReadOnlySpan json, JsonTypeInfo jsonTypeInfo) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + byte[]? tempArray = null; + + // For performance, avoid obtaining actual byte count unless memory usage is higher than the threshold. + Span utf8 = json.Length <= (JsonConstants.ArrayPoolMaxSizeBeforeUsingNormalAlloc / JsonConstants.MaxExpansionFactorWhileTranscoding) ? + // Use a pooled alloc. + tempArray = ArrayPool.Shared.Rent(json.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) : + // Use a normal alloc since the pool would create a normal alloc anyway based on the threshold (per current implementation) + // and by using a normal alloc we can avoid the Clear(). + new byte[JsonReaderHelper.GetUtf8ByteCount(json)]; + + try + { + int actualByteCount = JsonReaderHelper.GetUtf8FromText(json, utf8); + utf8 = utf8.Slice(0, actualByteCount); + return ReadFromSpan(utf8, jsonTypeInfo, actualByteCount); + } + finally + { + if (tempArray != null) + { + utf8.Clear(); + ArrayPool.Shared.Return(tempArray); + } + } } - private static TValue? ReadFromSpan(ReadOnlySpan json, JsonTypeInfo jsonTypeInfo) + private static object? ReadFromSpanAsObject(ReadOnlySpan json, JsonTypeInfo jsonTypeInfo) { Debug.Assert(jsonTypeInfo.IsConfigured); byte[]? tempArray = null; @@ -384,7 +411,7 @@ public static partial class JsonSerializer { int actualByteCount = JsonReaderHelper.GetUtf8FromText(json, utf8); utf8 = utf8.Slice(0, actualByteCount); - return ReadFromSpan(utf8, jsonTypeInfo, actualByteCount); + return ReadFromSpanAsObject(utf8, jsonTypeInfo, actualByteCount); } finally { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs index fe0a696109f31..4416cc2448474 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs @@ -58,7 +58,7 @@ public static partial class JsonSerializer [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] public static TValue? Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions? options = null) { - JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue)); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); return Read(ref reader, jsonTypeInfo); } @@ -117,7 +117,7 @@ public static partial class JsonSerializer } JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType); - return Read(ref reader, jsonTypeInfo); + return ReadAsObject(ref reader, jsonTypeInfo); } /// @@ -234,10 +234,10 @@ public static partial class JsonSerializer ThrowHelper.ThrowArgumentNullException(nameof(context)); } - return Read(ref reader, GetTypeInfo(context, returnType)); + return ReadAsObject(ref reader, GetTypeInfo(context, returnType)); } - private static TValue? Read(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo) + private static TValue? Read(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo) { Debug.Assert(jsonTypeInfo.IsConfigured); @@ -253,7 +253,32 @@ public static partial class JsonSerializer try { Utf8JsonReader scopedReader = GetReaderScopedToNextValue(ref reader, ref state); - return ReadCore(ref scopedReader, jsonTypeInfo, ref state); + return jsonTypeInfo.Deserialize(ref scopedReader, ref state); + } + catch (JsonException) + { + reader = restore; + throw; + } + } + + private static object? ReadAsObject(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + + if (reader.CurrentState.Options.CommentHandling == JsonCommentHandling.Allow) + { + ThrowHelper.ThrowArgumentException_SerializerDoesNotSupportComments(nameof(reader)); + } + + ReadStack state = default; + state.Initialize(jsonTypeInfo); + Utf8JsonReader restore = reader; + + try + { + Utf8JsonReader scopedReader = GetReaderScopedToNextValue(ref reader, ref state); + return jsonTypeInfo.DeserializeAsObject(ref scopedReader, ref state); } catch (JsonException) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 3e26508659252..1127853c320a8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -770,6 +770,11 @@ private void PopulatePropertyList() internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken, bool isInvokedByPolymorphicConverter = false); internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue, bool isInvokedByPolymorphicConverter = false); + // Untyped, root-level deserialization methods + internal abstract object? DeserializeAsObject(ref Utf8JsonReader reader, ref ReadStack state); + internal abstract ValueTask DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken); + internal abstract object? DeserializeAsObject(Stream utf8Json); + internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary propertyCache, ref Dictionary? ignoredMembers) { Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName can be null in custom JsonPropertyInfo instances and should never be passed in this method"); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs new file mode 100644 index 0000000000000..8b5128f62bbbe --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Text.Json.Serialization.Metadata +{ + public partial class JsonTypeInfo + { + // This section provides helper methods guiding root-level deserialization + // of values corresponding according to the current JsonTypeInfo configuration. + + internal T? Deserialize(ref Utf8JsonReader reader, ref ReadStack state) + { + Debug.Assert(IsConfigured); + return EffectiveConverter.ReadCore(ref reader, Options, ref state); + } + + internal async ValueTask DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken) + { + Debug.Assert(IsConfigured); + JsonSerializerOptions options = Options; + var bufferState = new ReadBufferState(options.DefaultBufferSize); + ReadStack readStack = default; + readStack.Initialize(this, supportContinuation: true); + var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); + + try + { + while (true) + { + bufferState = await bufferState.ReadFromStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); + T? value = ContinueDeserialize(ref bufferState, ref jsonReaderState, ref readStack); + + if (bufferState.IsFinalBlock) + { + return value; + } + } + } + finally + { + bufferState.Dispose(); + } + } + + internal T? Deserialize(Stream utf8Json) + { + Debug.Assert(IsConfigured); + JsonSerializerOptions options = Options; + var bufferState = new ReadBufferState(options.DefaultBufferSize); + ReadStack readStack = default; + readStack.Initialize(this, supportContinuation: true); + var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); + + try + { + while (true) + { + bufferState.ReadFromStream(utf8Json); + T? value = ContinueDeserialize(ref bufferState, ref jsonReaderState, ref readStack); + + if (bufferState.IsFinalBlock) + { + return value; + } + } + } + finally + { + bufferState.Dispose(); + } + } + + private JsonTypeInfo>? _asuncEnumerableQueueTypeInfo; + internal IAsyncEnumerable DeserializeAsyncEnumerable(Stream utf8Json, CancellationToken cancellationToken) + { + Debug.Assert(IsConfigured); + + JsonTypeInfo>? queueTypeInfo = _asuncEnumerableQueueTypeInfo; + if (queueTypeInfo is null) + { + queueTypeInfo = JsonMetadataServices.CreateQueueInfo, T>( + options: Options, + collectionInfo: new() + { + ObjectCreator = static () => new Queue(), + ElementInfo = this, + NumberHandling = Options.NumberHandling + }); + + queueTypeInfo.EnsureConfigured(); + _asuncEnumerableQueueTypeInfo = queueTypeInfo; + } + + return CreateAsyncEnumerableDeserializer(utf8Json, queueTypeInfo, cancellationToken); + + static async IAsyncEnumerable CreateAsyncEnumerableDeserializer( + Stream utf8Json, + JsonTypeInfo> queueTypeInfo, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + Debug.Assert(queueTypeInfo.IsConfigured); + JsonSerializerOptions options = queueTypeInfo.Options; + var bufferState = new ReadBufferState(options.DefaultBufferSize); + ReadStack readStack = default; + readStack.Initialize(queueTypeInfo, supportContinuation: true); + + var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); + + try + { + do + { + bufferState = await bufferState.ReadFromStreamAsync(utf8Json, cancellationToken, fillBuffer: false).ConfigureAwait(false); + queueTypeInfo.ContinueDeserialize( + ref bufferState, + ref jsonReaderState, + ref readStack); + + if (readStack.Current.ReturnValue is Queue queue) + { + while (queue.Count > 0) + { + yield return queue.Dequeue(); + } + } + } + while (!bufferState.IsFinalBlock); + } + finally + { + bufferState.Dispose(); + } + } + } + + internal sealed override object? DeserializeAsObject(ref Utf8JsonReader reader, ref ReadStack state) + => Deserialize(ref reader, ref state); + + internal sealed override async ValueTask DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken) + { + T? result = await DeserializeAsync(utf8Json, cancellationToken).ConfigureAwait(false); + return result; + } + + internal sealed override object? DeserializeAsObject(Stream utf8Json) + => Deserialize(utf8Json); + + private T? ContinueDeserialize( + ref ReadBufferState bufferState, + ref JsonReaderState jsonReaderState, + ref ReadStack readStack) + { + var reader = new Utf8JsonReader(bufferState.Bytes, bufferState.IsFinalBlock, jsonReaderState); + + // If we haven't read in the entire stream's payload we'll need to signify that we want + // to enable read ahead behaviors to ensure we have complete json objects and arrays + // ({}, []) when needed. (Notably to successfully parse JsonElement via JsonDocument + // to assign to object and JsonElement properties in the constructed .NET object.) + readStack.ReadAhead = !bufferState.IsFinalBlock; + readStack.BytesConsumed = 0; + + T? value = EffectiveConverter.ReadCore(ref reader, Options, ref readStack); + Debug.Assert(readStack.BytesConsumed <= bufferState.Bytes.Length); + bufferState.AdvanceBuffer((int)readStack.BytesConsumed); + jsonReaderState = reader.CurrentState; + return value; + } + } +} From 75467824c4dd11c7133ae6b5491b3a03166b3d22 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 1 Dec 2022 15:28:54 +0000 Subject: [PATCH 4/5] Use pooled Utf8JsonWriter in fast-path async serialization --- .../Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs index 79067ae8ef199..33f75cb0647d4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs @@ -81,18 +81,20 @@ internal async Task SerializeAsync( Debug.Assert(Converter is JsonMetadataServicesConverter); using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize); - using var writer = new Utf8JsonWriter(bufferWriter, Options.GetWriterOptions()); + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriter(Options, bufferWriter); try { SerializeHandler(writer, rootValue!); writer.Flush(); - await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); } finally { OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + Utf8JsonWriterCache.ReturnWriter(writer); } + + await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); } else if ( #if NETCOREAPP From 2e5cda65524270de6bba5e3107b5ba8a21de523c Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 1 Dec 2022 18:06:52 +0000 Subject: [PATCH 5/5] Only record streaming serialization sizes on successful operations --- .../Metadata/JsonTypeInfoOfT.WriteHelpers.cs | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs index 33f75cb0647d4..75b0b3f3e4380 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs @@ -90,7 +90,10 @@ internal async Task SerializeAsync( } finally { + // Record the serialization size in both successful and failed operations, + // since we want to immediately opt out of the fast path if it exceeds the threshold. OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + Utf8JsonWriterCache.ReturnWriter(writer); } @@ -176,12 +179,14 @@ rootValue is not null && await state.DisposePendingDisposablesOnExceptionAsync().ConfigureAwait(false); throw; } - finally + + if (CanUseSerializeHandler) { - if (CanUseSerializeHandler) - { - OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); - } + // On sucessful serialization, record the serialization size + // to determine potential suitability of the type for + // fast-path serialization in streaming methods. + Debug.Assert(writer.BytesPending == 0); + OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted); } } } @@ -213,7 +218,10 @@ internal void Serialize( } finally { + // Record the serialization size in both successful and failed operations, + // since we want to immediately opt out of the fast path if it exceeds the threshold. OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, bufferWriter); } } @@ -242,27 +250,26 @@ rootValue is not null && using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize); using var writer = new Utf8JsonWriter(bufferWriter, Options.GetWriterOptions()); - try + do { - do - { - state.FlushThreshold = (int)(bufferWriter.Capacity * JsonSerializer.FlushThreshold); + state.FlushThreshold = (int)(bufferWriter.Capacity * JsonSerializer.FlushThreshold); - isFinalBlock = EffectiveConverter.WriteCore(writer, rootValue!, Options, ref state); - writer.Flush(); + isFinalBlock = EffectiveConverter.WriteCore(writer, rootValue!, Options, ref state); + writer.Flush(); - bufferWriter.WriteToStream(utf8Json); - bufferWriter.Clear(); + bufferWriter.WriteToStream(utf8Json); + bufferWriter.Clear(); - Debug.Assert(state.PendingTask == null); - } while (!isFinalBlock); - } - finally + Debug.Assert(state.PendingTask == null); + } while (!isFinalBlock); + + if (CanUseSerializeHandler) { - if (CanUseSerializeHandler) - { - OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted + writer.BytesPending); - } + // On sucessful serialization, record the serialization size + // to determine potential suitability of the type for + // fast-path serialization in streaming methods. + Debug.Assert(writer.BytesPending == 0); + OnRootLevelAsyncSerializationCompleted(writer.BytesCommitted); } } }