Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Argon/JsonConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,11 +522,13 @@ public static string SerializeObject(object? value, Type? type, Formatting forma

static string SerializeObjectInternal(object? value, Type? type, JsonSerializer serializer)
{
var builder = new StringBuilder(256);
var stringWriter = new StringWriter(builder, InvariantCulture);
// the text is read back before the buffer is returned to the pool, so the JSON writer
// must not close it
using var stringWriter = new PooledStringWriter();
using (var jsonWriter = new JsonTextWriter(stringWriter)
{
Formatting = serializer.Formatting.GetValueOrDefault()
Formatting = serializer.Formatting.GetValueOrDefault(),
CloseOutput = false
})
{
serializer.Serialize(jsonWriter, value, type);
Expand Down
5 changes: 5 additions & 0 deletions src/Argon/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,11 @@ internal IReferenceResolver GetReferenceResolver() =>

internal static JsonConverter? GetMatchingConverter(IList<JsonConverter>? converters, Type type)
{
if (converters is ConverterListCache cache)
{
return cache.GetMatching(type);
}

if (converters != null)
{
// indexed loop: foreach over IList<T> boxes the List<T> enumerator, and this
Expand Down
63 changes: 58 additions & 5 deletions src/Argon/JsonTextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ void ParseReadString(char quote, ReadType readType)
SetToken(text);
quoteChar = quote;
break;
case ReadType.ReadAsDateTime:
// an ISO date is parsed straight from the buffer. anything else becomes a string
// token for the caller to convert, as before
if (DateTimeUtils.TryParseDateTimeIso(stringReference.AsSpan(), out var dateTime))
{
SetToken(JsonToken.Date, dateTime, false);
quoteChar = quote;
break;
}

goto default;
case ReadType.ReadAsDateTimeOffset:
if (DateTimeUtils.TryParseDateTimeOffsetIso(stringReference.AsSpan(), out var dateTimeOffset))
{
SetToken(JsonToken.Date, dateTimeOffset, false);
quoteChar = quote;
break;
}

goto default;
case ReadType.ReadAsInt32:
case ReadType.ReadAsDecimal:
case ReadType.ReadAsDouble:
Expand Down Expand Up @@ -435,6 +455,8 @@ public override bool Read()
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -567,6 +589,8 @@ State.ArrayStart or
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -599,16 +623,17 @@ State.ArrayStart or
case ReadType.ReadAsString:
return Value;
case ReadType.ReadAsDateTime:
if (Value is DateTime time)
// already boxed as the token value, so hand that back rather than boxing again
if (Value is DateTime)
{
return time;
return Value;
}

return ReadDateTimeString((string?) Value);
case ReadType.ReadAsDateTimeOffset:
if (Value is DateTimeOffset offset)
if (Value is DateTimeOffset)
{
return offset;
return Value;
}

return ReadDateTimeOffsetString((string?) Value);
Expand Down Expand Up @@ -720,6 +745,8 @@ JsonReaderException CreateUnexpectedCharacterException(char c) =>
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -844,6 +871,8 @@ void ProcessValueComma()
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -1333,6 +1362,8 @@ bool ParsePostValue(bool ignoreComments)
SetStateBasedOnCurrent();
return false;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -1402,6 +1433,8 @@ bool ParseObject()
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -1616,6 +1649,8 @@ bool ParseValue()
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
case StringUtils.Tab:
// eat
charPos++;
Expand Down Expand Up @@ -1645,6 +1680,21 @@ void ProcessLineFeed()
OnNewLine(charPos);
}

// Indentation is a run of spaces, so step over the whole run in one tight loop instead of
// going around the caller's switch once per space. The '\0' kept at charsUsed is not a space,
// so the scan always stops inside the buffer.
void SkipSpaces()
{
var buffer = charBuffer;
var position = charPos + 1;
while (buffer[position] == ' ')
{
position++;
}

charPos = position;
}

void ProcessCarriageReturn(bool append)
{
charPos++;
Expand Down Expand Up @@ -1680,8 +1730,11 @@ void EatWhitespace()
case StringUtils.LineFeed:
ProcessLineFeed();
break;
case ' ':
SkipSpaces();
break;
default:
if (currentChar == ' ' || char.IsWhiteSpace(currentChar))
if (char.IsWhiteSpace(currentChar))
{
charPos++;
}
Expand Down
123 changes: 123 additions & 0 deletions src/Argon/Linq/ConverterListCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2007 James Newton-King. All rights reserved.
// Use of this source code is governed by The MIT License,
// as found in the license.md file.

/// <summary>
/// A converter list that remembers which converter matched each type.
/// </summary>
/// <remarks>
/// JToken.WriteTo hands its converter list to every token beneath it, and each JValue then asks
/// every converter in turn whether it converts the value's type. A document has thousands of
/// values but only a handful of distinct value types, so the answer is remembered per type for as
/// long as this list is in use. It is a list in its own right so that it can travel through the
/// public WriteTo signature unchanged.
/// </remarks>
[RequiresUnreferencedCode(MiscellaneousUtils.TrimWarning)]
[RequiresDynamicCode(MiscellaneousUtils.AotWarning)]
sealed class ConverterListCache(IList<JsonConverter> inner) :
IList<JsonConverter>
{
// a JValue holds one of a dozen or so CLR types, and a short linear probe is quicker than
// hashing the type. any types past this many are simply not remembered
const int capacity = 8;

struct Match
{
public Type Type;
public JsonConverter? Converter;
}

readonly Match[] matches = new Match[capacity];
int count;

internal static IList<JsonConverter> Wrap(IList<JsonConverter> converters)
{
if (converters.Count == 0 ||
converters is ConverterListCache)
{
return converters;
}

return new ConverterListCache(converters);
}

internal JsonConverter? GetMatching(Type type)
{
for (var index = 0; index < count; index++)
{
if (matches[index].Type == type)
{
return matches[index].Converter;
}
}

var converter = JsonSerializer.GetMatchingConverter(inner, type);
if (count < capacity)
{
matches[count].Type = type;
matches[count].Converter = converter;
count++;
}

return converter;
}

public int Count => inner.Count;

public bool IsReadOnly => inner.IsReadOnly;

public JsonConverter this[int index]
{
get => inner[index];
set
{
inner[index] = value;
count = 0;
}
}

public void Add(JsonConverter item)
{
inner.Add(item);
count = 0;
}

public void Clear()
{
inner.Clear();
count = 0;
}

public void Insert(int index, JsonConverter item)
{
inner.Insert(index, item);
count = 0;
}

public bool Remove(JsonConverter item)
{
count = 0;
return inner.Remove(item);
}

public void RemoveAt(int index)
{
inner.RemoveAt(index);
count = 0;
}

public bool Contains(JsonConverter item) =>
inner.Contains(item);

public void CopyTo(JsonConverter[] array, int arrayIndex) =>
inner.CopyTo(array, arrayIndex);

public int IndexOf(JsonConverter item) =>
inner.IndexOf(item);

public IEnumerator<JsonConverter> GetEnumerator() =>
inner.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() =>
inner.GetEnumerator();
}
2 changes: 2 additions & 0 deletions src/Argon/Linq/JArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ internal override JToken CloneToken() =>
[RequiresDynamicCode(MiscellaneousUtils.AotWarning)]
public override void WriteTo(JsonWriter writer, params IList<JsonConverter> converters)
{
converters = ConverterListCache.Wrap(converters);

writer.WriteStartArray();

foreach (var value in values)
Expand Down
20 changes: 13 additions & 7 deletions src/Argon/Linq/JContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,12 @@ internal bool TryAdd(object? content) =>
internal void AddAndSkipParentCheck(JToken token) =>
TryAddInternal(ChildrenTokens.Count, token, true);

// content read from a reader is always a new token with no parent, so it can go straight to
// InsertItem. Add first has to work out whether its argument is a token, a value to wrap or a
// collection to expand, which is several type checks for every token loaded
void AddLoaded(JToken token) =>
InsertItem(ChildrenTokens.Count, token, false);

/// <summary>
/// Adds the specified content as the first children of this <see cref="JToken" />.
/// </summary>
Expand Down Expand Up @@ -518,7 +524,7 @@ void ReadContentFrom(JsonReader r, JsonLoadSettings? settings)
case JsonToken.StartArray:
var a = new JArray();
a.SetLineInfo(lineInfo, settings);
parent.Add(a);
parent.AddLoaded(a);
parent = a;
break;

Expand All @@ -533,7 +539,7 @@ void ReadContentFrom(JsonReader r, JsonLoadSettings? settings)
case JsonToken.StartObject:
var o = new JObject();
o.SetLineInfo(lineInfo, settings);
parent.Add(o);
parent.AddLoaded(o);
parent = o;
break;
case JsonToken.EndObject:
Expand Down Expand Up @@ -567,26 +573,26 @@ void ReadContentFrom(JsonReader r, JsonLoadSettings? settings)
? new JValue(value)
: new JValue(value, knownType);
v.SetLineInfo(lineInfo, settings);
parent.Add(v);
parent.AddLoaded(v);
break;
case JsonToken.Comment:
if (settings is {CommentHandling: CommentHandling.Load})
{
v = JValue.CreateComment((string?) r.GetValue());
v.SetLineInfo(lineInfo, settings);
parent.Add(v);
parent.AddLoaded(v);
}

break;
case JsonToken.Null:
v = JValue.CreateNull();
v.SetLineInfo(lineInfo, settings);
parent.Add(v);
parent.AddLoaded(v);
break;
case JsonToken.Undefined:
v = JValue.CreateUndefined();
v.SetLineInfo(lineInfo, settings);
parent.Add(v);
parent.AddLoaded(v);
break;
case JsonToken.PropertyName:
var property = ReadProperty(r, settings, lineInfo, parent);
Expand All @@ -608,7 +614,7 @@ static JProperty ReadProperty(JsonReader reader, JsonLoadSettings? settings, IJs
{
// JObject.ValidateToken rejects duplicate names, so a separate
// pre-check would hash the name a second time on every property
parentObject.Add(property);
parentObject.AddLoaded(property);
}
catch (ArgumentException)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Argon/Linq/JObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ public JToken? this[string propertyName]
[RequiresDynamicCode(MiscellaneousUtils.AotWarning)]
public override void WriteTo(JsonWriter writer, params IList<JsonConverter> converters)
{
converters = ConverterListCache.Wrap(converters);

writer.WriteStartObject();

foreach (var property in properties.InnerList)
Expand Down
Loading
Loading