diff --git a/src/libraries/Common/src/System/Text/Json/PooledByteBufferWriter.cs b/src/libraries/Common/src/System/Text/Json/PooledByteBufferWriter.cs index 8694a785ad3a5..187d45b74e216 100644 --- a/src/libraries/Common/src/System/Text/Json/PooledByteBufferWriter.cs +++ b/src/libraries/Common/src/System/Text/Json/PooledByteBufferWriter.cs @@ -13,11 +13,19 @@ namespace System.Text.Json { internal sealed class PooledByteBufferWriter : IBufferWriter, IDisposable { - private byte[] _rentedBuffer; + // This class allows two possible configurations: if rentedBuffer is not null then + // it can be used as an IBufferWriter and holds a buffer that should eventually be + // returned to the shared pool. If rentedBuffer is null, then the instance is in a + // cleared/disposed state and it must re-rent a buffer before it can be used again. + private byte[]? _rentedBuffer; private int _index; private const int MinimumBufferSize = 256; + private PooledByteBufferWriter() + { + } + public PooledByteBufferWriter(int initialCapacity) { Debug.Assert(initialCapacity > 0); @@ -68,6 +76,16 @@ public void Clear() ClearHelper(); } + public void ClearAndReturnBuffers() + { + Debug.Assert(_rentedBuffer != null); + + ClearHelper(); + byte[] toReturn = _rentedBuffer; + _rentedBuffer = null; + ArrayPool.Shared.Return(toReturn); + } + private void ClearHelper() { Debug.Assert(_rentedBuffer != null); @@ -87,10 +105,21 @@ public void Dispose() ClearHelper(); byte[] toReturn = _rentedBuffer; - _rentedBuffer = null!; + _rentedBuffer = null; ArrayPool.Shared.Return(toReturn); } + public void InitializeEmptyInstance(int initialCapacity) + { + Debug.Assert(initialCapacity > 0); + Debug.Assert(_rentedBuffer is null); + + _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); + _index = 0; + } + + public static PooledByteBufferWriter CreateEmptyInstanceForCaching() => new PooledByteBufferWriter(); + public void Advance(int count) { Debug.Assert(_rentedBuffer != null); @@ -125,11 +154,13 @@ internal void WriteToStream(Stream destination) #else internal Task WriteToStreamAsync(Stream destination, CancellationToken cancellationToken) { + Debug.Assert(_rentedBuffer != null); return destination.WriteAsync(_rentedBuffer, 0, _index, cancellationToken); } internal void WriteToStream(Stream destination) { + Debug.Assert(_rentedBuffer != null); destination.Write(_rentedBuffer, 0, _index); } #endif 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 7d111541bb1de..e75edb5c8c2ef 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 + 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 49aa791c8dde9..74a98ccd0a8b0 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 @@ -120,25 +120,34 @@ private static byte[] WriteBytes(in TValue value, JsonTypeInfo j { Debug.Assert(jsonTypeInfo.IsConfigured); - JsonSerializerOptions options = jsonTypeInfo.Options; + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(jsonTypeInfo.Options, out PooledByteBufferWriter output); - using var output = new PooledByteBufferWriter(options.DefaultBufferSize); - using var writer = new Utf8JsonWriter(output, options.GetWriterOptions()); - - WriteCore(writer, value, jsonTypeInfo); - return output.WrittenMemory.ToArray(); + try + { + WriteCore(writer, value, jsonTypeInfo); + return output.WrittenMemory.ToArray(); + } + finally + { + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + } } private static byte[] WriteBytesAsObject(object? value, JsonTypeInfo jsonTypeInfo) { Debug.Assert(jsonTypeInfo.IsConfigured); - JsonSerializerOptions options = jsonTypeInfo.Options; + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(jsonTypeInfo.Options, out PooledByteBufferWriter output); - using var output = new PooledByteBufferWriter(options.DefaultBufferSize); - using var writer = new Utf8JsonWriter(output, options.GetWriterOptions()); - WriteCoreAsObject(writer, value, jsonTypeInfo); - return output.WrittenMemory.ToArray(); + try + { + WriteCoreAsObject(writer, value, jsonTypeInfo); + return output.WrittenMemory.ToArray(); + } + finally + { + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + } } } } 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 13ff63a2bc536..19f02d7ff74a7 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 @@ -117,10 +117,17 @@ private static JsonDocument WriteDocument(in TValue value, JsonTypeInfo< // For performance, share the same buffer across serialization and deserialization. // The PooledByteBufferWriter is cleared and returned when JsonDocument.Dispose() is called. PooledByteBufferWriter output = new(options.DefaultBufferSize); - using Utf8JsonWriter writer = new(output, options.GetWriterOptions()); + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriter(options, output); - WriteCore(writer, value, jsonTypeInfo); - return JsonDocument.ParseRented(output, options.GetDocumentOptions()); + try + { + WriteCore(writer, value, jsonTypeInfo); + return JsonDocument.ParseRented(output, options.GetDocumentOptions()); + } + finally + { + Utf8JsonWriterCache.ReturnWriter(writer); + } } private static JsonDocument WriteDocumentAsObject(object? value, JsonTypeInfo jsonTypeInfo) @@ -131,10 +138,17 @@ private static JsonDocument WriteDocumentAsObject(object? value, JsonTypeInfo js // For performance, share the same buffer across serialization and deserialization. // The PooledByteBufferWriter is cleared and returned when JsonDocument.Dispose() is called. PooledByteBufferWriter output = new(options.DefaultBufferSize); - using Utf8JsonWriter writer = new(output, options.GetWriterOptions()); + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriter(options, output); - WriteCoreAsObject(writer, value, jsonTypeInfo); - return JsonDocument.ParseRented(output, options.GetDocumentOptions()); + try + { + WriteCoreAsObject(writer, value, jsonTypeInfo); + return JsonDocument.ParseRented(output, options.GetDocumentOptions()); + } + finally + { + Utf8JsonWriterCache.ReturnWriter(writer); + } } } } 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 ba35d4c4f9115..b04a132189c8b 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 @@ -115,12 +115,17 @@ private static JsonElement WriteElement(in TValue value, JsonTypeInfo(in TValue value, JsonTypeInfo { Debug.Assert(jsonTypeInfo.IsConfigured); - JsonSerializerOptions options = jsonTypeInfo.Options; + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(jsonTypeInfo.Options, out PooledByteBufferWriter output); - using var output = new PooledByteBufferWriter(options.DefaultBufferSize); - using var writer = new Utf8JsonWriter(output, options.GetWriterOptions()); - - WriteCore(writer, value, jsonTypeInfo); - return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); + try + { + WriteCore(writer, value, jsonTypeInfo); + return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); + } + finally + { + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + } } private static string WriteStringAsObject(object? value, JsonTypeInfo jsonTypeInfo) { Debug.Assert(jsonTypeInfo.IsConfigured); - JsonSerializerOptions options = jsonTypeInfo.Options; + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(jsonTypeInfo.Options, out PooledByteBufferWriter output); - using var output = new PooledByteBufferWriter(options.DefaultBufferSize); - using var writer = new Utf8JsonWriter(output, options.GetWriterOptions()); - - WriteCoreAsObject(writer, value, jsonTypeInfo); - return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); + try + { + WriteCoreAsObject(writer, value, jsonTypeInfo); + return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); + } + finally + { + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs index c5b2eebd54211..149bb76b74d84 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs @@ -89,6 +89,10 @@ public sealed partial class Utf8JsonWriter : IDisposable, IAsyncDisposable /// public int CurrentDepth => _currentDepth & JsonConstants.RemoveFlagsBitMask; + private Utf8JsonWriter() + { + } + /// /// Constructs a new instance with a specified . /// @@ -226,6 +230,29 @@ public void Reset(IBufferWriter bufferWriter) ResetHelper(); } + internal void ResetAllStateForCacheReuse() + { + ResetHelper(); + + _stream = null; + _arrayBufferWriter = null; + _output = null; + } + + internal void Reset(IBufferWriter bufferWriter, JsonWriterOptions options) + { + Debug.Assert(_output is null && _stream is null && _arrayBufferWriter is null); + + _output = bufferWriter; + _options = options; + if (_options.MaxDepth == 0) + { + _options.MaxDepth = JsonWriterOptions.DefaultMaxDepth; // If max depth is not set, revert to the default depth. + } + } + + internal static Utf8JsonWriter CreateEmptyInstanceForCaching() => new Utf8JsonWriter(); + private void ResetHelper() { BytesPending = default; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs new file mode 100644 index 0000000000000..bff98e9b81649 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs @@ -0,0 +1,96 @@ +// 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; + +namespace System.Text.Json +{ + /// + /// Defines a thread-local cache for JsonSerializer to store reusable Utf8JsonWriter/IBufferWriter instances. + /// + internal static class Utf8JsonWriterCache + { + [ThreadStatic] + private static ThreadLocalState? t_threadLocalState; + + public static Utf8JsonWriter RentWriterAndBuffer(JsonSerializerOptions options, out PooledByteBufferWriter bufferWriter) + { + ThreadLocalState state = t_threadLocalState ??= new(); + Utf8JsonWriter writer; + + if (state.RentedWriters++ == 0) + { + // First JsonSerializer call in the stack -- initialize & return the cached instances. + bufferWriter = state.BufferWriter; + writer = state.Writer; + + bufferWriter.InitializeEmptyInstance(options.DefaultBufferSize); + writer.Reset(bufferWriter, options.GetWriterOptions()); + } + else + { + // We're in a recursive JsonSerializer call -- return fresh instances. + bufferWriter = new PooledByteBufferWriter(options.DefaultBufferSize); + writer = new Utf8JsonWriter(bufferWriter, options.GetWriterOptions()); + } + + return writer; + } + + public static Utf8JsonWriter RentWriter(JsonSerializerOptions options, PooledByteBufferWriter bufferWriter) + { + ThreadLocalState state = t_threadLocalState ??= new(); + Utf8JsonWriter writer; + + if (state.RentedWriters++ == 0) + { + // First JsonSerializer call in the stack -- initialize & return the cached instance. + writer = state.Writer; + writer.Reset(bufferWriter, options.GetWriterOptions()); + } + else + { + // We're in a recursive JsonSerializer call -- return a fresh instance. + writer = new Utf8JsonWriter(bufferWriter, options.GetWriterOptions()); + } + + return writer; + } + + public static void ReturnWriterAndBuffer(Utf8JsonWriter writer, PooledByteBufferWriter bufferWriter) + { + Debug.Assert(t_threadLocalState != null); + ThreadLocalState state = t_threadLocalState; + + writer.ResetAllStateForCacheReuse(); + bufferWriter.ClearAndReturnBuffers(); + + int rentedWriters = --state.RentedWriters; + Debug.Assert((rentedWriters == 0) == (ReferenceEquals(state.BufferWriter, bufferWriter) && ReferenceEquals(state.Writer, writer))); + } + + public static void ReturnWriter(Utf8JsonWriter writer) + { + Debug.Assert(t_threadLocalState != null); + ThreadLocalState state = t_threadLocalState; + + writer.ResetAllStateForCacheReuse(); + + int rentedWriters = --state.RentedWriters; + Debug.Assert((rentedWriters == 0) == ReferenceEquals(state.Writer, writer)); + } + + private sealed class ThreadLocalState + { + public readonly PooledByteBufferWriter BufferWriter; + public readonly Utf8JsonWriter Writer; + public int RentedWriters; + + public ThreadLocalState() + { + BufferWriter = PooledByteBufferWriter.CreateEmptyInstanceForCaching(); + Writer = Utf8JsonWriter.CreateEmptyInstanceForCaching(); + } + } + } +}