Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Update UTF-8 JsonWriter APIs based on previous API feedback #2612

Merged
merged 21 commits into from Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ea5533d
Remove static factory methods.
ahsonkhan Dec 4, 2018
a0729cc
Add JsonWriterOptions and start a copy with updated APIs.
ahsonkhan Dec 5, 2018
da52411
Add WriteString overloads.
ahsonkhan Dec 6, 2018
00facae
Add WriteBoolean and WriteNull key-value APIs.
ahsonkhan Dec 6, 2018
6860fc9
Add write number (int) APIs.
ahsonkhan Dec 7, 2018
a58dafa
Split up types into separate files.
ahsonkhan Dec 7, 2018
5dbd898
Code refactoring and adding more tests, properties, etc.
ahsonkhan Dec 7, 2018
bb2540b
Add all other WriteNumber overloads and add a test.
ahsonkhan Dec 7, 2018
3a286f5
Add Guid, Date, and DateTime APIs and tests.
ahsonkhan Dec 7, 2018
cbf3599
Add skeletong for other APIs and more tests.
ahsonkhan Dec 7, 2018
f9dbe0d
Remove unnecessary test that was leftover from debugging.
ahsonkhan Dec 7, 2018
acb3268
Add stream and memory formatters, and add async pipe tests.
ahsonkhan Dec 8, 2018
9ea2e6b
Remove use of BufferWriter_T
ahsonkhan Dec 8, 2018
ff33f3f
Fix typo in if condition.
ahsonkhan Dec 10, 2018
71e227f
Remove GetSpan and use Ensure
ahsonkhan Dec 10, 2018
cc13a60
Undo change from Ensure to GetSpan. Use GetSpan to get local span.
ahsonkhan Dec 10, 2018
790df8b
Remove unused previous token type and rename MaxDepth to
ahsonkhan Dec 10, 2018
598a611
Add single value valid and invalid json tests.
ahsonkhan Dec 10, 2018
bf6f45e
Pass spans by ref instead, especially property names for the fast path.
ahsonkhan Dec 10, 2018
e1cede3
Remove WriteRawBytes, finish WriteArray, and add tests.
ahsonkhan Dec 10, 2018
e58703a
Fix build and tests.
ahsonkhan Dec 10, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/System.Buffers.Experimental/System/Range.cs
Expand Up @@ -6,7 +6,7 @@
using System.Collections.Generic;
using System.Text;

namespace System
namespace System.Buffers.Experimental
{
// TODO: consider allowing Last > First. Ennumeration will count down.
public readonly struct Range : IEnumerable<int>
Expand Down
Expand Up @@ -2,6 +2,7 @@
// 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;
using System.Text;

Expand Down Expand Up @@ -30,7 +31,7 @@ public BufferWriter(T output)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Flush()
{
var buffered = _buffered;
int buffered = _buffered;
if (buffered > 0)
{
_buffered = 0;
Expand All @@ -52,6 +53,7 @@ public void Ensure(int count = 1)
{
EnsureMore(count);
}
Debug.Assert(_span.Length >= count);
}

[MethodImpl(MethodImplOptions.NoInlining)]
Expand Down
Expand Up @@ -2,6 +2,7 @@
// 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.Experimental;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
Expand Down
@@ -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);
}
}
}
Expand Up @@ -2,7 +2,7 @@
<Import Project="..\..\tools\common.props" />
<PropertyGroup>
<Description>JSON dynamic object</Description>
<TargetFramework>netstandard1.3</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageTags>.NET json non-allocating corefxlab</PackageTags>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/System.Text.JsonLab/System.Text.JsonLab.csproj
Expand Up @@ -2,7 +2,7 @@
<Import Project="..\..\tools\common.props" />
<PropertyGroup>
<Description>Non-allocating JSON reader and writer</Description>
<TargetFramework>netstandard1.1</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageTags>.NET json non-allocating corefxlab</PackageTags>
<DebugType>pdbonly</DebugType>
Expand Down
169 changes: 169 additions & 0 deletions src/System.Text.JsonLab/System/Text/Json/BitStack.cs
@@ -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
Copy link
Member Author

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.

{
// 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;
}
}
}
73 changes: 73 additions & 0 deletions src/System.Text.JsonLab/System/Text/Json/BufferWriterT.cs
@@ -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);
}
}
}
14 changes: 14 additions & 0 deletions src/System.Text.JsonLab/System/Text/Json/JsonConstants.cs
Expand Up @@ -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?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NumberBufferLength values are large enough to represent the exact string representation of any given input plus one for a rounding digit and one for the null-terminating character.

For example, the input with the longest exact string for a double value is double.Epsilon, whose exact representation has 767 significant digits (there are an additional 307 leading zeros, giving 1074 digits total; but we don't need to track leading or trailing zeros). If a user gives us an input string that contains all 1074 digits of this value, we need to be able to parse that. If they provide additional digits, we also have to consider them to determine which direction we round (do we round down to double.Epsilon or do we round up to the next representable value above double.Epsilon). If the 768th digit is a 5, then we may be in "midpoint-rounding" and we have to continue processing the rest of the string until we find a non-zero digit to determine if this is actually midpoint (the rest of the digits in the input were 0, in which case we round up or down to the value with the first bit set) or if we are slightly above (one or more of the digits after the 768th were non-zero) in which case we round up.

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
}
}