Skip to content

Commit a73f163

Browse files
Improve JSON validation perf (#111332)
1 parent 3e899bc commit a73f163

File tree

8 files changed

+378
-188
lines changed

8 files changed

+378
-188
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/BitStack.cs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal struct BitStack
1414

1515
private const int DefaultInitialArraySize = 2;
1616

17+
// The backing array for the stack used when the depth exceeds AllocationFreeMaxDepth.
1718
private int[]? _array;
1819

1920
// This ulong container represents a tiny stack to track the state during nested transitions.
@@ -26,8 +27,14 @@ internal struct BitStack
2627

2728
private int _currentDepth;
2829

29-
public int CurrentDepth => _currentDepth;
30+
/// <summary>
31+
/// Gets the number of elements in the stack.
32+
/// </summary>
33+
public readonly int CurrentDepth => _currentDepth;
3034

35+
/// <summary>
36+
/// Pushes <see langword="true"/> onto the stack.
37+
/// </summary>
3138
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3239
public void PushTrue()
3340
{
@@ -42,6 +49,9 @@ public void PushTrue()
4249
_currentDepth++;
4350
}
4451

52+
/// <summary>
53+
/// Pushes <see langword="false"/> onto the stack.
54+
/// </summary>
4555
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4656
public void PushFalse()
4757
{
@@ -56,7 +66,10 @@ public void PushFalse()
5666
_currentDepth++;
5767
}
5868

59-
// Allocate the bit array lazily only when it is absolutely necessary
69+
/// <summary>
70+
/// Pushes a bit onto the stack. Allocate the bit array lazily only when it is absolutely necessary.
71+
/// </summary>
72+
/// <param name="value">The bit to push onto the stack.</param>
6073
[MethodImpl(MethodImplOptions.NoInlining)]
6174
private void PushToArray(bool value)
6275
{
@@ -94,6 +107,10 @@ private void PushToArray(bool value)
94107
_array[elementIndex] = newValue;
95108
}
96109

110+
/// <summary>
111+
/// Pops the bit at the top of the stack and returns its value.
112+
/// </summary>
113+
/// <returns>The bit that was popped.</returns>
97114
[MethodImpl(MethodImplOptions.AggressiveInlining)]
98115
public bool Pop()
99116
{
@@ -110,13 +127,18 @@ public bool Pop()
110127
}
111128
else
112129
{
113-
inObject = PopFromArray();
130+
// Decrementing depth above effectively pops the last element in the array-backed case.
131+
inObject = PeekInArray();
114132
}
115133
return inObject;
116134
}
117135

136+
/// <summary>
137+
/// If the stack has a backing array allocated, this method will find the topmost bit in the array and return its value.
138+
/// This should only be called if the depth is greater than AllocationFreeMaxDepth and an array has been allocated.
139+
/// </summary>
118140
[MethodImpl(MethodImplOptions.NoInlining)]
119-
private bool PopFromArray()
141+
private readonly bool PeekInArray()
120142
{
121143
int index = _currentDepth - AllocationFreeMaxDepth - 1;
122144
Debug.Assert(_array != null);
@@ -129,6 +151,14 @@ private bool PopFromArray()
129151
return (_array[elementIndex] & (1 << extraBits)) != 0;
130152
}
131153

154+
/// <summary>
155+
/// Peeks at the bit at the top of the stack.
156+
/// </summary>
157+
/// <returns>The bit at the top of the stack.</returns>
158+
public readonly bool Peek()
159+
// If the stack is small enough, we can use the allocation-free container, otherwise check the allocated array.
160+
=> _currentDepth <= AllocationFreeMaxDepth ? (_allocationFreeContainer & 1) != 0 : PeekInArray();
161+
132162
private void DoubleArray(int minSize)
133163
{
134164
Debug.Assert(_array != null);
@@ -141,13 +171,19 @@ private void DoubleArray(int minSize)
141171
Array.Resize(ref _array, nextDouble);
142172
}
143173

174+
/// <summary>
175+
/// Optimization to push <see langword="true"/> as the first bit when the stack is empty.
176+
/// </summary>
144177
public void SetFirstBit()
145178
{
146179
Debug.Assert(_currentDepth == 0, "Only call SetFirstBit when depth is 0");
147180
_currentDepth++;
148181
_allocationFreeContainer = 1;
149182
}
150183

184+
/// <summary>
185+
/// Optimization to push <see langword="false"/> as the first bit when the stack is empty.
186+
/// </summary>
151187
public void ResetFirstBit()
152188
{
153189
Debug.Assert(_currentDepth == 0, "Only call ResetFirstBit when depth is 0");

src/libraries/System.Text.Json/src/System/Text/Json/JsonTokenType.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,70 +12,70 @@ namespace System.Text.Json
1212
/// </summary>
1313
public enum JsonTokenType : byte
1414
{
15-
// Do not re-order.
16-
// We rely on the ordering to quickly check things like IsTokenTypePrimitive
15+
// Do not re-number.
16+
// We rely on the underlying values to quickly check things like JsonReaderHelper.IsTokenTypePrimitive and Utf8JsonWriter.CanWriteValue
1717

1818
/// <summary>
1919
/// Indicates that there is no value (as distinct from <see cref="Null"/>).
2020
/// </summary>
2121
/// <remarks>
2222
/// This is the default token type if no data has been read by the <see cref="Utf8JsonReader"/>.
2323
/// </remarks>
24-
None,
24+
None = 0,
2525

2626
/// <summary>
2727
/// Indicates that the token type is the start of a JSON object.
2828
/// </summary>
29-
StartObject,
29+
StartObject = 1,
3030

3131
/// <summary>
3232
/// Indicates that the token type is the end of a JSON object.
3333
/// </summary>
34-
EndObject,
34+
EndObject = 2,
3535

3636
/// <summary>
3737
/// Indicates that the token type is the start of a JSON array.
3838
/// </summary>
39-
StartArray,
39+
StartArray = 3,
4040

4141
/// <summary>
4242
/// Indicates that the token type is the end of a JSON array.
4343
/// </summary>
44-
EndArray,
44+
EndArray = 4,
4545

4646
/// <summary>
4747
/// Indicates that the token type is a JSON property name.
4848
/// </summary>
49-
PropertyName,
49+
PropertyName = 5,
5050

5151
/// <summary>
5252
/// Indicates that the token type is the comment string.
5353
/// </summary>
54-
Comment,
54+
Comment = 6,
5555

5656
/// <summary>
5757
/// Indicates that the token type is a JSON string.
5858
/// </summary>
59-
String,
59+
String = 7,
6060

6161
/// <summary>
6262
/// Indicates that the token type is a JSON number.
6363
/// </summary>
64-
Number,
64+
Number = 8,
6565

6666
/// <summary>
6767
/// Indicates that the token type is the JSON literal <c>true</c>.
6868
/// </summary>
69-
True,
69+
True = 9,
7070

7171
/// <summary>
7272
/// Indicates that the token type is the JSON literal <c>false</c>.
7373
/// </summary>
74-
False,
74+
False = 10,
7575

7676
/// <summary>
7777
/// Indicates that the token type is the JSON literal <c>null</c>.
7878
/// </summary>
79-
Null,
79+
Null = 11,
8080
}
8181
}

src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.CompilerServices;
7+
using static System.Text.Json.Utf8JsonWriter;
78

89
namespace System.Text.Json
910
{
@@ -312,9 +313,23 @@ public static void ThrowInvalidOperationException_CannotSkipOnPartial()
312313
}
313314

314315
[DoesNotReturn]
315-
public static void ThrowInvalidOperationException_CannotMixEncodings(Utf8JsonWriter.SegmentEncoding previousEncoding, Utf8JsonWriter.SegmentEncoding currentEncoding)
316+
[MethodImpl(MethodImplOptions.NoInlining)]
317+
public static void ThrowInvalidOperationException_CannotMixEncodings(EnclosingContainerType previousEncoding, EnclosingContainerType currentEncoding)
316318
{
317-
throw GetInvalidOperationException(SR.Format(SR.CannotMixEncodings, previousEncoding, currentEncoding));
319+
throw GetInvalidOperationException(SR.Format(SR.CannotMixEncodings, GetEncodingName(previousEncoding), GetEncodingName(currentEncoding)));
320+
321+
static string GetEncodingName(EnclosingContainerType encoding)
322+
{
323+
switch (encoding)
324+
{
325+
case EnclosingContainerType.Utf8StringSequence: return "UTF-8";
326+
case EnclosingContainerType.Utf16StringSequence: return "UTF-16";
327+
case EnclosingContainerType.Base64StringSequence: return "Base64";
328+
default:
329+
Debug.Fail("Unknown encoding.");
330+
return "Unknown";
331+
};
332+
}
318333
}
319334

320335
private static InvalidOperationException GetInvalidOperationException(string message, JsonTokenType tokenType)

src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Buffers;
55
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Runtime.CompilerServices;
78
using System.Runtime.InteropServices;
89

@@ -36,13 +37,10 @@ private void ValidateWritingProperty()
3637
{
3738
if (!_options.SkipValidation)
3839
{
39-
// Make sure a new property is not attempted within an unfinalized string.
40-
ValidateNotWithinUnfinalizedString();
41-
42-
if (!_inObject || _tokenType == JsonTokenType.PropertyName)
40+
if (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName)
4341
{
4442
Debug.Assert(_tokenType != JsonTokenType.StartObject);
45-
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
43+
OnValidateWritingPropertyFailed();
4644
}
4745
}
4846
}
@@ -52,18 +50,28 @@ private void ValidateWritingProperty(byte token)
5250
{
5351
if (!_options.SkipValidation)
5452
{
55-
// Make sure a new property is not attempted within an unfinalized string.
56-
ValidateNotWithinUnfinalizedString();
57-
58-
if (!_inObject || _tokenType == JsonTokenType.PropertyName)
53+
if (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName)
5954
{
6055
Debug.Assert(_tokenType != JsonTokenType.StartObject);
61-
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
56+
OnValidateWritingPropertyFailed();
6257
}
6358
UpdateBitStackOnStart(token);
6459
}
6560
}
6661

62+
[DoesNotReturn]
63+
[MethodImpl(MethodImplOptions.NoInlining)]
64+
private void OnValidateWritingPropertyFailed()
65+
{
66+
if (IsWritingPartialString)
67+
{
68+
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
69+
}
70+
71+
Debug.Assert(_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName);
72+
ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray);
73+
}
74+
6775
private void WritePropertyNameMinimized(ReadOnlySpan<byte> escapedPropertyName, byte token)
6876
{
6977
Debug.Assert(escapedPropertyName.Length < int.MaxValue - 5);

0 commit comments

Comments
 (0)