Update UTF-8 JsonWriter APIs based on previous API feedback #2612
Changes from all commits
ea5533d
a0729cc
da52411
00facae
6860fc9
a58dafa
5dbd898
bb2540b
3a286f5
cbf3599
f9dbe0d
acb3268
9ea2e6b
ff33f3f
71e227f
cc13a60
790df8b
598a611
bf6f45e
e1cede3
e58703a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Buffers; | ||
|
||
namespace System.Text.Formatting | ||
{ | ||
public struct MemoryFormatter : IBufferWriter<byte> | ||
{ | ||
private Memory<byte> _memory; | ||
|
||
public MemoryFormatter(Memory<byte> memory) | ||
{ | ||
_memory = memory; | ||
} | ||
|
||
// Slice already does the necessary argument validation ( < 0 or > _memory.Length). | ||
public void Advance(int count) | ||
=> _memory = _memory.Slice(count); | ||
|
||
public Memory<byte> GetMemory(int sizeHint = 0) | ||
{ | ||
if (sizeHint == 0) | ||
return _memory; | ||
|
||
// Do we need to slice the _memory at all? Maybe just validate sizeHint is in range and return the entire memory? | ||
return _memory.Slice(0, sizeHint); | ||
} | ||
|
||
// Slice already does the necessary argument validation ( < 0 or > _memory.Length). | ||
public Span<byte> GetSpan(int sizeHint = 0) | ||
{ | ||
Span<byte> span = _memory.Span; | ||
|
||
if (sizeHint == 0) | ||
return span; | ||
|
||
return span.Slice(0, sizeHint); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Diagnostics; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace System.Text.JsonLab | ||
{ | ||
public struct BitStack | ||
{ | ||
// We are using a ulong to represent our nested state, so we can only | ||
// go 64 levels deep without having to allocate. | ||
private const int AllocationFreeMaxDepth = sizeof(ulong) * 8; | ||
|
||
private const int DefaultInitialArraySize = 2; | ||
|
||
private int[] _array; | ||
|
||
// This ulong container represents a tiny stack to track the state during nested transitions. | ||
// The first bit represents the state of the current depth (1 == object, 0 == array). | ||
// Each subsequent bit is the parent / containing type (object or array). Since this | ||
// reader does a linear scan, we only need to keep a single path as we go through the data. | ||
// This is primarily used as an optimization to avoid having to allocate an object for | ||
// depths up to 64 (which is the default max depth). | ||
private ulong _allocationFreeContainer; | ||
|
||
private int _currentDepth; | ||
|
||
public int CurrentDepth => _currentDepth; | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public void PushTrue() | ||
{ | ||
if (_currentDepth < AllocationFreeMaxDepth) | ||
{ | ||
_allocationFreeContainer = (_allocationFreeContainer << 1) | 1; | ||
} | ||
else | ||
{ | ||
PushToArray(true); | ||
} | ||
_currentDepth++; | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public void PushFalse() | ||
{ | ||
if (_currentDepth < AllocationFreeMaxDepth) | ||
{ | ||
_allocationFreeContainer = _allocationFreeContainer << 1; | ||
} | ||
else | ||
{ | ||
PushToArray(false); | ||
} | ||
_currentDepth++; | ||
} | ||
|
||
// Allocate the bit array lazily only when it is absolutely necessary | ||
[MethodImpl(MethodImplOptions.NoInlining)] | ||
private void PushToArray(bool value) | ||
{ | ||
if (_array == null) | ||
{ | ||
_array = new int[DefaultInitialArraySize]; | ||
} | ||
|
||
int index = _currentDepth - AllocationFreeMaxDepth; | ||
|
||
Debug.Assert(index >= 0, $"Set - Negative - index: {index}, arrayLength: {_array.Length}"); | ||
|
||
// Maximum possible array length if bitLength was int.MaxValue (i.e. 67_108_864) | ||
Debug.Assert(_array.Length <= int.MaxValue / 32 + 1, $"index: {index}, arrayLength: {_array.Length}"); | ||
|
||
int elementIndex = Div32Rem(index, out int extraBits); | ||
|
||
// Grow the array when setting a bit if it isn't big enough | ||
// This way the caller doesn't have to check. | ||
if (elementIndex >= _array.Length) | ||
{ | ||
// This multiplication can overflow, so cast to uint first. | ||
Debug.Assert(index >= 0 && index > (int)((uint)_array.Length * 32 - 1), $"Only grow when necessary - index: {index}, arrayLength: {_array.Length}"); | ||
DoubleArray(elementIndex); | ||
} | ||
|
||
Debug.Assert(elementIndex < _array.Length, $"Set - index: {index}, elementIndex: {elementIndex}, arrayLength: {_array.Length}, extraBits: {extraBits}"); | ||
|
||
int newValue = _array[elementIndex]; | ||
if (value) | ||
{ | ||
newValue |= 1 << extraBits; | ||
} | ||
else | ||
{ | ||
newValue &= ~(1 << extraBits); | ||
} | ||
_array[elementIndex] = newValue; | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public bool Pop() | ||
{ | ||
_currentDepth--; | ||
bool inObject = false; | ||
if (_currentDepth < AllocationFreeMaxDepth) | ||
{ | ||
_allocationFreeContainer >>= 1; | ||
inObject = (_allocationFreeContainer & 1) != 0; | ||
} | ||
else if (_currentDepth == AllocationFreeMaxDepth) | ||
{ | ||
inObject = (_allocationFreeContainer & 1) != 0; | ||
} | ||
else | ||
{ | ||
inObject = PopFromArray(); | ||
} | ||
return inObject; | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.NoInlining)] | ||
private bool PopFromArray() | ||
{ | ||
int index = _currentDepth - AllocationFreeMaxDepth - 1; | ||
Debug.Assert(_array != null); | ||
Debug.Assert(index >= 0, $"Get - Negative - index: {index}, arrayLength: {_array.Length}"); | ||
|
||
int elementIndex = Div32Rem(index, out int extraBits); | ||
|
||
Debug.Assert(elementIndex < _array.Length, $"Get - index: {index}, elementIndex: {elementIndex}, arrayLength: {_array.Length}, extraBits: {extraBits}"); | ||
|
||
return (_array[elementIndex] & (1 << extraBits)) != 0; | ||
} | ||
|
||
private void DoubleArray(int minSize) | ||
{ | ||
Debug.Assert(_array.Length < int.MaxValue / 2, $"Array too large - arrayLength: {_array.Length}"); | ||
Debug.Assert(minSize >= 0 && minSize >= _array.Length); | ||
|
||
int nextDouble = Math.Max(minSize + 1, _array.Length * 2); | ||
Debug.Assert(nextDouble > minSize); | ||
|
||
Array.Resize(ref _array, nextDouble); | ||
} | ||
|
||
public void SetFirstBit() | ||
{ | ||
Debug.Assert(_currentDepth == 0, "Only call SetFirstBit when depth is 0"); | ||
_currentDepth++; | ||
_allocationFreeContainer = 1; | ||
} | ||
|
||
public void ResetFirstBit() | ||
{ | ||
Debug.Assert(_currentDepth == 0, "Only call ResetFirstBit when depth is 0"); | ||
_currentDepth++; | ||
_allocationFreeContainer = 0; | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
private static int Div32Rem(int number, out int remainder) | ||
{ | ||
uint quotient = (uint)number / 32; | ||
remainder = number & (32 - 1); // equivalent to number % 32, since 32 is a power of 2 | ||
return (int)quotient; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Buffers; | ||
using System.Diagnostics; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace System.Text.JsonLab | ||
{ | ||
internal ref struct BufferWriter<T> where T : IBufferWriter<byte> | ||
{ | ||
internal T _output; | ||
private int _buffered; | ||
|
||
public Span<byte> Buffer { get; private set; } | ||
|
||
public long BytesWritten | ||
{ | ||
get | ||
{ | ||
Debug.Assert(BytesCommitted <= long.MaxValue - _buffered); | ||
return BytesCommitted + _buffered; | ||
} | ||
} | ||
|
||
public long BytesCommitted { get; private set; } | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public BufferWriter(T output) | ||
{ | ||
_output = output; | ||
_buffered = 0; | ||
BytesCommitted = 0; | ||
Buffer = output.GetSpan(); | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public void Flush() | ||
{ | ||
BytesCommitted += _buffered; | ||
_output.Advance(_buffered); | ||
_buffered = 0; | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public void Advance(int count) | ||
{ | ||
Debug.Assert(count >= 0 && _buffered <= int.MaxValue - count); | ||
|
||
_buffered += count; | ||
Buffer = Buffer.Slice(count); | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public void Ensure(int count) | ||
{ | ||
Debug.Assert(count >= 0); | ||
|
||
if (Buffer.Length < count) | ||
EnsureMore(count); | ||
|
||
Debug.Assert(Buffer.Length >= count); | ||
} | ||
|
||
[MethodImpl(MethodImplOptions.NoInlining)] | ||
private void EnsureMore(int count) | ||
{ | ||
Flush(); | ||
Buffer = _output.GetSpan(count); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,5 +53,19 @@ internal static class JsonConstants | |
public static ReadOnlySpan<byte> EscapableChars => new byte[] { Quote, (byte)'n', (byte)'r', (byte)'t', Solidus, (byte)'u', (byte)'b', (byte)'f' }; | ||
|
||
#endregion Common values | ||
|
||
public const int RemoveFlagsBitMask = 0x7FFFFFFF; | ||
public const int MaxPossibleDepth = (int.MaxValue - 2_000_001_000) / 2; // 73_741_323 (to account for double space indentation), leaving 1_000 buffer for "JSONifying" | ||
public const int MaxTokenSize = 1_000_000_000; // 1 GB | ||
public const int MaxCharacterTokenSize = 1_000_000_000 / 3; // 333 million characters, i.e. 333 MB | ||
|
||
public const int MaximumInt64Length = 20; // 19 + sign (i.e. -9223372036854775808) | ||
public const int MaximumUInt64Length = 20; // i.e. 18446744073709551615 | ||
public const int MaximumDoubleLength = 32; // default (i.e. 'G') TODO: Should it be 22? | ||
public const int MaximumSingleLength = 32; // default (i.e. 'G') TODO: Should it be 13? | ||
public const int MaximumDecimalLength = 32; // default (i.e. 'G') TODO: Should it be 31? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What should this be? @tannergooding I don't understand why some of these are so large: https://github.com/dotnet/coreclr/blob/80c0a0ea2446b665d13e1632422802f4bf208ae5/src/System.Private.CoreLib/shared/System/Number.NumberBuffer.cs#L14 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The For example, the input with the longest exact string for a |
||
public const int MaximumGuidLength = 36; // default (i.e. 'D') 8 + 4 + 4 + 4 + 12 + 4 for the hyphens (e.g. 094ffa0a-0442-494d-b452-04003fa755cc) | ||
public const int MaximumDateTimeLength = 26; // default (i.e. 'G') e.g. 05/25/2017 10:30:15 -08:00 | ||
public const int MaximumDateTimeOffsetLength = 26; // default (i.e. 'G') e.g. 05/25/2017 10:30:15 -08:00 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore this. It is a copy of the internal API from https://github.com/dotnet/corefx/blob/master/src/System.Text.Json/src/System/Text/Json/BitStack.cs which I just copied for convenience. This will go away once the code moves to corefx.