|
1 | 1 | // Licensed to the .NET Foundation under one or more agreements.
|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 |
|
| 4 | +using System.Buffers; |
4 | 5 | using System.Diagnostics;
|
| 6 | +using System.Diagnostics.CodeAnalysis; |
5 | 7 | using System.IO;
|
6 | 8 | using System.IO.Pipelines;
|
7 |
| -using System.Net; |
| 9 | +using System.Runtime.CompilerServices; |
8 | 10 | using System.Threading;
|
9 | 11 | using System.Threading.Tasks;
|
10 | 12 |
|
11 | 13 | namespace System.Text.Json
|
12 | 14 | {
|
13 | 15 | internal sealed class PooledByteBufferWriter : PipeWriter, IDisposable
|
14 | 16 | {
|
| 17 | + // This class allows two possible configurations: if rentedBuffer is not null then |
| 18 | + // it can be used as an IBufferWriter and holds a buffer that should eventually be |
| 19 | + // returned to the shared pool. If rentedBuffer is null, then the instance is in a |
| 20 | + // cleared/disposed state and it must re-rent a buffer before it can be used again. |
| 21 | + private byte[]? _rentedBuffer; |
| 22 | + private int _index; |
| 23 | + private readonly Stream? _stream; |
| 24 | + |
15 | 25 | private const int MinimumBufferSize = 256;
|
16 | 26 |
|
17 |
| - private ArrayBuffer _buffer; |
18 |
| - private readonly Stream? _stream; |
| 27 | + // Value copied from Array.MaxLength in System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Array.cs. |
| 28 | + public const int MaximumBufferSize = 0X7FFFFFC7; |
| 29 | + |
| 30 | + private PooledByteBufferWriter() |
| 31 | + { |
| 32 | +#if NET |
| 33 | + // Ensure we are in sync with the Array.MaxLength implementation. |
| 34 | + Debug.Assert(MaximumBufferSize == Array.MaxLength); |
| 35 | +#endif |
| 36 | + } |
19 | 37 |
|
20 |
| - public PooledByteBufferWriter(int initialCapacity) |
| 38 | + public PooledByteBufferWriter(int initialCapacity) : this() |
21 | 39 | {
|
22 |
| - _buffer = new ArrayBuffer(initialCapacity, usePool: true); |
| 40 | + Debug.Assert(initialCapacity > 0); |
| 41 | + |
| 42 | + _rentedBuffer = ArrayPool<byte>.Shared.Rent(initialCapacity); |
| 43 | + _index = 0; |
23 | 44 | }
|
24 | 45 |
|
25 | 46 | public PooledByteBufferWriter(int initialCapacity, Stream stream) : this(initialCapacity)
|
26 | 47 | {
|
27 | 48 | _stream = stream;
|
28 | 49 | }
|
29 | 50 |
|
30 |
| - public ReadOnlySpan<byte> WrittenSpan => _buffer.ActiveSpan; |
| 51 | + public ReadOnlyMemory<byte> WrittenMemory |
| 52 | + { |
| 53 | + get |
| 54 | + { |
| 55 | + Debug.Assert(_rentedBuffer != null); |
| 56 | + Debug.Assert(_index <= _rentedBuffer.Length); |
| 57 | + return _rentedBuffer.AsMemory(0, _index); |
| 58 | + } |
| 59 | + } |
31 | 60 |
|
32 |
| - public ReadOnlyMemory<byte> WrittenMemory => _buffer.ActiveMemory; |
| 61 | + public int WrittenCount |
| 62 | + { |
| 63 | + get |
| 64 | + { |
| 65 | + Debug.Assert(_rentedBuffer != null); |
| 66 | + return _index; |
| 67 | + } |
| 68 | + } |
33 | 69 |
|
34 |
| - public int Capacity => _buffer.Capacity; |
| 70 | + public int Capacity |
| 71 | + { |
| 72 | + get |
| 73 | + { |
| 74 | + Debug.Assert(_rentedBuffer != null); |
| 75 | + return _rentedBuffer.Length; |
| 76 | + } |
| 77 | + } |
35 | 78 |
|
36 |
| - public void Clear() => _buffer.Discard(_buffer.ActiveLength); |
| 79 | + public int FreeCapacity |
| 80 | + { |
| 81 | + get |
| 82 | + { |
| 83 | + Debug.Assert(_rentedBuffer != null); |
| 84 | + return _rentedBuffer.Length - _index; |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + public void Clear() |
| 89 | + { |
| 90 | + ClearHelper(); |
| 91 | + } |
37 | 92 |
|
38 |
| - public void ClearAndReturnBuffers() => _buffer.ClearAndReturnBuffer(); |
| 93 | + public void ClearAndReturnBuffers() |
| 94 | + { |
| 95 | + Debug.Assert(_rentedBuffer != null); |
39 | 96 |
|
40 |
| - public void Dispose() => _buffer.Dispose(); |
| 97 | + ClearHelper(); |
| 98 | + byte[] toReturn = _rentedBuffer; |
| 99 | + _rentedBuffer = null; |
| 100 | + ArrayPool<byte>.Shared.Return(toReturn); |
| 101 | + } |
| 102 | + |
| 103 | + private void ClearHelper() |
| 104 | + { |
| 105 | + Debug.Assert(_rentedBuffer != null); |
| 106 | + Debug.Assert(_index <= _rentedBuffer.Length); |
| 107 | + |
| 108 | + _rentedBuffer.AsSpan(0, _index).Clear(); |
| 109 | + _index = 0; |
| 110 | + } |
| 111 | + |
| 112 | + // Returns the rented buffer back to the pool |
| 113 | + public void Dispose() |
| 114 | + { |
| 115 | + if (_rentedBuffer == null) |
| 116 | + { |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + ClearHelper(); |
| 121 | + byte[] toReturn = _rentedBuffer; |
| 122 | + _rentedBuffer = null; |
| 123 | + ArrayPool<byte>.Shared.Return(toReturn); |
| 124 | + } |
41 | 125 |
|
42 | 126 | public void InitializeEmptyInstance(int initialCapacity)
|
43 | 127 | {
|
44 | 128 | Debug.Assert(initialCapacity > 0);
|
45 |
| - Debug.Assert(_buffer.ActiveLength == 0); |
| 129 | + Debug.Assert(_rentedBuffer is null); |
46 | 130 |
|
47 |
| - _buffer.EnsureAvailableSpace(initialCapacity); |
| 131 | + _rentedBuffer = ArrayPool<byte>.Shared.Rent(initialCapacity); |
| 132 | + _index = 0; |
48 | 133 | }
|
49 | 134 |
|
50 |
| - public static PooledByteBufferWriter CreateEmptyInstanceForCaching() => new PooledByteBufferWriter(initialCapacity: 0); |
| 135 | + public static PooledByteBufferWriter CreateEmptyInstanceForCaching() => new PooledByteBufferWriter(); |
51 | 136 |
|
52 |
| - public override void Advance(int count) => _buffer.Commit(count); |
| 137 | + public override void Advance(int count) |
| 138 | + { |
| 139 | + Debug.Assert(_rentedBuffer != null); |
| 140 | + Debug.Assert(count >= 0); |
| 141 | + Debug.Assert(_index <= _rentedBuffer.Length - count); |
| 142 | + _index += count; |
| 143 | + } |
53 | 144 |
|
54 | 145 | public override Memory<byte> GetMemory(int sizeHint = MinimumBufferSize)
|
55 | 146 | {
|
56 |
| - Debug.Assert(sizeHint > 0); |
57 |
| - |
58 |
| - _buffer.EnsureAvailableSpace(sizeHint); |
59 |
| - return _buffer.AvailableMemory; |
| 147 | + CheckAndResizeBuffer(sizeHint); |
| 148 | + return _rentedBuffer.AsMemory(_index); |
60 | 149 | }
|
61 | 150 |
|
62 | 151 | public override Span<byte> GetSpan(int sizeHint = MinimumBufferSize)
|
63 | 152 | {
|
64 |
| - Debug.Assert(sizeHint > 0); |
65 |
| - |
66 |
| - _buffer.EnsureAvailableSpace(sizeHint); |
67 |
| - return _buffer.AvailableSpan; |
| 153 | + CheckAndResizeBuffer(sizeHint); |
| 154 | + return _rentedBuffer.AsSpan(_index); |
68 | 155 | }
|
69 | 156 |
|
70 | 157 | #if NET
|
71 |
| - internal void WriteToStream(Stream destination) => destination.Write(_buffer.ActiveSpan); |
| 158 | + internal void WriteToStream(Stream destination) |
| 159 | + { |
| 160 | + destination.Write(WrittenMemory.Span); |
| 161 | + } |
72 | 162 | #else
|
73 |
| - internal void WriteToStream(Stream destination) => destination.Write(_buffer.ActiveMemory); |
| 163 | + internal void WriteToStream(Stream destination) |
| 164 | + { |
| 165 | + Debug.Assert(_rentedBuffer != null); |
| 166 | + destination.Write(_rentedBuffer, 0, _index); |
| 167 | + } |
74 | 168 | #endif
|
75 | 169 |
|
| 170 | + private void CheckAndResizeBuffer(int sizeHint) |
| 171 | + { |
| 172 | + Debug.Assert(_rentedBuffer != null); |
| 173 | + Debug.Assert(sizeHint > 0); |
| 174 | + |
| 175 | + int currentLength = _rentedBuffer.Length; |
| 176 | + int availableSpace = currentLength - _index; |
| 177 | + |
| 178 | + // If we've reached ~1GB written, grow to the maximum buffer |
| 179 | + // length to avoid incessant minimal growths causing perf issues. |
| 180 | + if (_index >= MaximumBufferSize / 2) |
| 181 | + { |
| 182 | + sizeHint = Math.Max(sizeHint, MaximumBufferSize - currentLength); |
| 183 | + } |
| 184 | + |
| 185 | + if (sizeHint > availableSpace) |
| 186 | + { |
| 187 | + int growBy = Math.Max(sizeHint, currentLength); |
| 188 | + |
| 189 | + int newSize = currentLength + growBy; |
| 190 | + |
| 191 | + if ((uint)newSize > MaximumBufferSize) |
| 192 | + { |
| 193 | + newSize = currentLength + sizeHint; |
| 194 | + if ((uint)newSize > MaximumBufferSize) |
| 195 | + { |
| 196 | + ThrowHelper.ThrowOutOfMemoryException_BufferMaximumSizeExceeded((uint)newSize); |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + byte[] oldBuffer = _rentedBuffer; |
| 201 | + |
| 202 | + _rentedBuffer = ArrayPool<byte>.Shared.Rent(newSize); |
| 203 | + |
| 204 | + Debug.Assert(oldBuffer.Length >= _index); |
| 205 | + Debug.Assert(_rentedBuffer.Length >= _index); |
| 206 | + |
| 207 | + Span<byte> oldBufferAsSpan = oldBuffer.AsSpan(0, _index); |
| 208 | + oldBufferAsSpan.CopyTo(_rentedBuffer); |
| 209 | + oldBufferAsSpan.Clear(); |
| 210 | + ArrayPool<byte>.Shared.Return(oldBuffer); |
| 211 | + } |
| 212 | + |
| 213 | + Debug.Assert(_rentedBuffer.Length - _index > 0); |
| 214 | + Debug.Assert(_rentedBuffer.Length - _index >= sizeHint); |
| 215 | + } |
| 216 | + |
76 | 217 | public override async ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
|
77 | 218 | {
|
78 | 219 | Debug.Assert(_stream is not null);
|
| 220 | +#if NET |
79 | 221 | await _stream.WriteAsync(WrittenMemory, cancellationToken).ConfigureAwait(false);
|
| 222 | +#else |
| 223 | + Debug.Assert(_rentedBuffer != null); |
| 224 | + await _stream.WriteAsync(_rentedBuffer, 0, _index, cancellationToken).ConfigureAwait(false); |
| 225 | +#endif |
80 | 226 | Clear();
|
81 | 227 |
|
82 | 228 | return new FlushResult(isCanceled: false, isCompleted: false);
|
83 | 229 | }
|
84 | 230 |
|
85 | 231 | public override bool CanGetUnflushedBytes => true;
|
86 |
| - public override long UnflushedBytes => _buffer.ActiveLength; |
| 232 | + public override long UnflushedBytes => _index; |
87 | 233 |
|
88 | 234 | // This type is used internally in JsonSerializer to help buffer and flush bytes to the underlying Stream.
|
89 | 235 | // It's only pretending to be a PipeWriter and doesn't need Complete or CancelPendingFlush for the internal usage.
|
90 | 236 | public override void CancelPendingFlush() => throw new NotImplementedException();
|
91 | 237 | public override void Complete(Exception? exception = null) => throw new NotImplementedException();
|
92 | 238 | }
|
| 239 | + |
| 240 | + internal static partial class ThrowHelper |
| 241 | + { |
| 242 | + [DoesNotReturn] |
| 243 | + [MethodImpl(MethodImplOptions.NoInlining)] |
| 244 | + public static void ThrowOutOfMemoryException_BufferMaximumSizeExceeded(uint capacity) |
| 245 | + { |
| 246 | + throw new OutOfMemoryException(SR.Format(SR.BufferMaximumSizeExceeded, capacity)); |
| 247 | + } |
| 248 | + } |
93 | 249 | }
|
0 commit comments