Skip to content

Commit b673eea

Browse files
authored
Improve performance of polymorphism (#42538)
1 parent 4df1664 commit b673eea

File tree

8 files changed

+234
-101
lines changed

8 files changed

+234
-101
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.MetadataDb.cs

Lines changed: 71 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -86,29 +86,40 @@ private struct MetadataDb : IDisposable
8686

8787
internal int Length { get; private set; }
8888
private byte[] _data;
89-
#if DEBUG
90-
private readonly bool _isLocked;
91-
#endif
89+
90+
private bool _convertToAlloc; // Convert the rented data to an alloc when complete.
91+
private bool _isLocked; // Is the array the correct fixed size.
92+
// _isLocked _convertToAlloc truth table:
93+
// false false Standard flow. Size is not known and renting used throughout lifetime.
94+
// true false Used by JsonElement.ParseValue() for primitives and JsonDocument.Clone(). Size is known and no renting.
95+
// false true Used by JsonElement.ParseValue() for arrays and objects. Renting used until size is known.
96+
// true true not valid
97+
98+
private MetadataDb(byte[] initialDb, bool isLocked, bool convertToAlloc)
99+
{
100+
_data = initialDb;
101+
_isLocked = isLocked;
102+
_convertToAlloc = convertToAlloc;
103+
Length = 0;
104+
}
92105

93106
internal MetadataDb(byte[] completeDb)
94107
{
95108
_data = completeDb;
96-
Length = completeDb.Length;
97-
98-
#if DEBUG
99109
_isLocked = true;
100-
#endif
110+
_convertToAlloc = false;
111+
Length = completeDb.Length;
101112
}
102113

103-
internal MetadataDb(int payloadLength)
114+
internal static MetadataDb CreateRented(int payloadLength, bool convertToAlloc)
104115
{
105116
// Assume that a token happens approximately every 12 bytes.
106117
// int estimatedTokens = payloadLength / 12
107118
// now acknowledge that the number of bytes we need per token is 12.
108119
// So that's just the payload length.
109120
//
110-
// Add one token's worth of data just because.
111-
int initialSize = DbRow.Size + payloadLength;
121+
// Add one row worth of data since we need at least one row for a primitive type.
122+
int initialSize = payloadLength + DbRow.Size;
112123

113124
// Stick with ArrayPool's rent/return range if it looks feasible.
114125
// If it's wrong, we'll just grow and copy as we would if the tokens
@@ -120,30 +131,17 @@ internal MetadataDb(int payloadLength)
120131
initialSize = OneMegabyte;
121132
}
122133

123-
_data = ArrayPool<byte>.Shared.Rent(initialSize);
124-
Length = 0;
125-
#if DEBUG
126-
_isLocked = false;
127-
#endif
134+
byte[] data = ArrayPool<byte>.Shared.Rent(initialSize);
135+
return new MetadataDb(data, isLocked: false, convertToAlloc);
128136
}
129137

130-
internal MetadataDb(MetadataDb source, bool useArrayPools)
138+
internal static MetadataDb CreateLocked(int payloadLength)
131139
{
132-
Length = source.Length;
133-
134-
#if DEBUG
135-
_isLocked = !useArrayPools;
136-
#endif
140+
// Add one row worth of data since we need at least one row for a primitive type.
141+
int size = payloadLength + DbRow.Size;
137142

138-
if (useArrayPools)
139-
{
140-
_data = ArrayPool<byte>.Shared.Rent(Length);
141-
source._data.AsSpan(0, Length).CopyTo(_data);
142-
}
143-
else
144-
{
145-
_data = source._data.AsSpan(0, Length).ToArray();
146-
}
143+
byte[] data = new byte[size];
144+
return new MetadataDb(data, isLocked: true, convertToAlloc: false);
147145
}
148146

149147
public void Dispose()
@@ -154,9 +152,7 @@ public void Dispose()
154152
return;
155153
}
156154

157-
#if DEBUG
158155
Debug.Assert(!_isLocked, "Dispose called on a locked database");
159-
#endif
160156

161157
// The data in this rented buffer only conveys the positions and
162158
// lengths of tokens in a document, but no content; so it does not
@@ -165,28 +161,51 @@ public void Dispose()
165161
Length = 0;
166162
}
167163

168-
internal void TrimExcess()
164+
/// <summary>
165+
/// If using array pools, trim excess if necessary.
166+
/// If not using array pools, release the temporary array pool and alloc.
167+
/// </summary>
168+
internal void CompleteAllocations()
169169
{
170-
// There's a chance that the size we have is the size we'd get for this
171-
// amount of usage (particularly if Enlarge ever got called); and there's
172-
// the small copy-cost associated with trimming anyways. "Is half-empty" is
173-
// just a rough metric for "is trimming worth it?".
174-
if (Length <= _data.Length / 2)
170+
if (!_isLocked)
175171
{
176-
byte[] newRent = ArrayPool<byte>.Shared.Rent(Length);
177-
byte[] returnBuf = newRent;
178-
179-
if (newRent.Length < _data.Length)
172+
if (_convertToAlloc)
180173
{
181-
Buffer.BlockCopy(_data, 0, newRent, 0, Length);
182-
returnBuf = _data;
183-
_data = newRent;
174+
Debug.Assert(_data != null);
175+
byte[] returnBuf = _data;
176+
_data = _data.AsSpan(0, Length).ToArray();
177+
_isLocked = true;
178+
_convertToAlloc = false;
179+
180+
// The data in this rented buffer only conveys the positions and
181+
// lengths of tokens in a document, but no content; so it does not
182+
// need to be cleared.
183+
ArrayPool<byte>.Shared.Return(returnBuf);
184+
}
185+
else
186+
{
187+
// There's a chance that the size we have is the size we'd get for this
188+
// amount of usage (particularly if Enlarge ever got called); and there's
189+
// the small copy-cost associated with trimming anyways. "Is half-empty" is
190+
// just a rough metric for "is trimming worth it?".
191+
if (Length <= _data.Length / 2)
192+
{
193+
byte[] newRent = ArrayPool<byte>.Shared.Rent(Length);
194+
byte[] returnBuf = newRent;
195+
196+
if (newRent.Length < _data.Length)
197+
{
198+
Buffer.BlockCopy(_data, 0, newRent, 0, Length);
199+
returnBuf = _data;
200+
_data = newRent;
201+
}
202+
203+
// The data in this rented buffer only conveys the positions and
204+
// lengths of tokens in a document, but no content; so it does not
205+
// need to be cleared.
206+
ArrayPool<byte>.Shared.Return(returnBuf);
207+
}
184208
}
185-
186-
// The data in this rented buffer only conveys the positions and
187-
// lengths of tokens in a document, but no content; so it does not
188-
// need to be cleared.
189-
ArrayPool<byte>.Shared.Return(returnBuf);
190209
}
191210
}
192211

@@ -197,10 +216,6 @@ internal void Append(JsonTokenType tokenType, int startLocation, int length)
197216
(tokenType == JsonTokenType.StartArray || tokenType == JsonTokenType.StartObject) ==
198217
(length == DbRow.UnknownSize));
199218

200-
#if DEBUG
201-
Debug.Assert(!_isLocked, "Appending to a locked database");
202-
#endif
203-
204219
if (Length >= _data.Length - DbRow.Size)
205220
{
206221
Enlarge();
@@ -213,6 +228,8 @@ internal void Append(JsonTokenType tokenType, int startLocation, int length)
213228

214229
private void Enlarge()
215230
{
231+
Debug.Assert(!_isLocked, "Appending to a locked database");
232+
216233
byte[] toReturn = _data;
217234
_data = ArrayPool<byte>.Shared.Rent(toReturn.Length * 2);
218235
Buffer.BlockCopy(toReturn, 0, _data, 0, toReturn.Length);

0 commit comments

Comments
 (0)