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
17 changes: 14 additions & 3 deletions src/Argon.DataSets/DataTableConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,33 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
var table = (DataTable) value;
var resolver = serializer.ContractResolver as DefaultContractResolver;

// resolve column names once per table, not once per cell
var columnCount = table.Columns.Count;
var columns = new DataColumn[columnCount];
var resolvedColumnNames = new string[columnCount];
for (var i = 0; i < columnCount; i++)
{
var column = table.Columns[i];
columns[i] = column;
resolvedColumnNames[i] = resolver?.GetResolvedPropertyName(column.ColumnName) ?? column.ColumnName;
}

writer.WriteStartArray();

foreach (DataRow row in table.Rows)
{
writer.WriteStartObject();
foreach (DataColumn column in row.Table.Columns)
for (var i = 0; i < columnCount; i++)
{
var columnValue = row[column];
var columnValue = row[columns[i]];

if (serializer.NullValueHandling == NullValueHandling.Ignore &&
columnValue == DBNull.Value)
{
continue;
}

writer.WritePropertyName(resolver?.GetResolvedPropertyName(column.ColumnName) ?? column.ColumnName);
writer.WritePropertyName(resolvedColumnNames[i]);
serializer.Serialize(writer, columnValue);
}

Expand Down
6 changes: 5 additions & 1 deletion src/Argon.FSharp/DiscriminatedUnionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
// memoized: CanConvert runs for every value (de)serialized once this converter is
// registered, and FSharpType.IsUnion does attribute reflection on each call
static readonly ThreadSafeStore<Type, bool> isUnionCache = new(_ => FSharpType.IsUnion(_, null));

public override bool CanConvert(Type type) =>
FSharpType.IsUnion(type, null);
isUnionCache.Get(type);
}
17 changes: 10 additions & 7 deletions src/Argon.FSharp/FSharpListConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ public class FSharpListConverter :
{
static MethodInfo readList = typeof(FSharpListConverter).GetMethod("ReadList")!;

// cached closed delegates: MakeGenericMethod + Invoke per call allocates and wraps
// exceptions in TargetInvocationException
static ThreadSafeStore<Type, Func<JsonReader, JsonSerializer, object>> readListCache = new(CreateReadListDelegate);

static Func<JsonReader, JsonSerializer, object> CreateReadListDelegate(Type genericArgument) =>
(Func<JsonReader, JsonSerializer, object>) Delegate.CreateDelegate(
typeof(Func<JsonReader, JsonSerializer, object>),
readList.MakeGenericMethod(genericArgument));

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartArray();
Expand All @@ -27,13 +36,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
}

var genericArgument = type.GetGenericArguments()[0];
return readList.MakeGenericMethod(genericArgument)
.Invoke(
null,
[
reader,
serializer
]);
return readListCache.Get(genericArgument)(reader, serializer);
}

public static FSharpList<T> ReadList<T>(JsonReader reader, JsonSerializer serializer)
Expand Down
41 changes: 29 additions & 12 deletions src/Argon.FSharp/FSharpMapConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@
public class FSharpMapConverter :
JsonConverter
{
static MethodInfo writeMap = typeof(FSharpMapConverter).GetMethod("WriteMap")!;
// cached closed delegates: MakeGenericMethod + Invoke per call allocates and wraps
// exceptions in TargetInvocationException
static ThreadSafeStore<Type, Action<JsonWriter, object, JsonSerializer>> writeMapCache = new(CreateWriteMapDelegate);

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
static Action<JsonWriter, object, JsonSerializer> CreateWriteMapDelegate(Type mapType)
{
var genericArguments = value.GetType().GetGenericArguments();
writeMap.MakeGenericMethod(genericArguments[0], genericArguments[1])
.Invoke(
null,
[writer, value, serializer]);
var arguments = mapType.GetGenericArguments();
var method = typeof(FSharpMapConverter)
.GetMethod(nameof(WriteMapBoxed), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(arguments[0], arguments[1]);
return (Action<JsonWriter, object, JsonSerializer>) Delegate.CreateDelegate(
typeof(Action<JsonWriter, object, JsonSerializer>),
method);
}

static void WriteMapBoxed<T, K>(JsonWriter writer, object value, JsonSerializer serializer)
where T : notnull =>
WriteMap(writer, (FSharpMap<T, K>) value, serializer);

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
writeMapCache.Get(value.GetType())(writer, value, serializer);

public static void WriteMap<T, K>(JsonWriter writer, FSharpMap<T, K> value, JsonSerializer serializer)
where T : notnull =>
serializer.Serialize(writer, value.ToDictionary(_ => _.Key, _ => _.Value));
Expand All @@ -29,15 +40,21 @@ public static void WriteMap<T, K>(JsonWriter writer, FSharpMap<T, K> value, Json
return null;
}

var arguments = type.GetGenericArguments();
return readMap.MakeGenericMethod(arguments[0], arguments[1])
.Invoke(
null,
[reader, serializer]);
return readMapCache.Get(type)(reader, serializer);
}

static MethodInfo readMap = typeof(FSharpMapConverter).GetMethod("ReadMap")!;

static ThreadSafeStore<Type, Func<JsonReader, JsonSerializer, object>> readMapCache = new(CreateReadMapDelegate);

static Func<JsonReader, JsonSerializer, object> CreateReadMapDelegate(Type mapType)
{
var arguments = mapType.GetGenericArguments();
return (Func<JsonReader, JsonSerializer, object>) Delegate.CreateDelegate(
typeof(Func<JsonReader, JsonSerializer, object>),
readMap.MakeGenericMethod(arguments[0], arguments[1]));
}

public static FSharpMap<T, K> ReadMap<T, K>(JsonReader reader, JsonSerializer serializer)
where T : notnull
{
Expand Down
40 changes: 31 additions & 9 deletions src/Argon.JsonPath/BooleanQueryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ class BooleanQueryExpression(QueryOperator @operator, object left, object? right
public readonly object Left = left;
public readonly object? Right = right;

static IEnumerable<JToken> GetResult(JToken root, JToken t, object? o)
{
if (o is JToken resultToken)
{
return [resultToken];
}
// constant operands never change, so the single-element wrappers IsMatch needs
// are built once instead of per candidate token
readonly JToken[]? leftConstant = left is JToken leftToken ? [leftToken] : null;
readonly JToken[]? rightConstant = right is JToken rightToken ? [rightToken] : null;

static IEnumerable<JToken> GetFilterResult(JToken root, JToken t, object? o)
{
if (o is List<PathFilter> pathFilters)
{
return JPath.Evaluate(pathFilters, root, t, JTokenExtensions.DefaultSettings);
Expand All @@ -25,13 +25,35 @@ public override bool IsMatch(JToken root, JToken t, JsonSelectSettings settings)
{
if (Operator == QueryOperator.Exists)
{
return GetResult(root, t, Left).Any();
return leftConstant != null ||
GetFilterResult(root, t, Left).Any();
}

// single constant right operand is the dominant filter shape (e.g. @.a == 1):
// compare directly without materializing result collections
if (rightConstant != null)
{
var rightResult = rightConstant[0];
if (leftConstant != null)
{
return MatchTokens(leftConstant[0], rightResult, settings);
}

foreach (var leftResult in GetFilterResult(root, t, Left))
{
if (MatchTokens(leftResult, rightResult, settings))
{
return true;
}
}

return false;
}

using var leftResults = GetResult(root, t, Left).GetEnumerator();
using var leftResults = (leftConstant ?? GetFilterResult(root, t, Left)).GetEnumerator();
if (leftResults.MoveNext())
{
var rightResultsEn = GetResult(root, t, Right);
var rightResultsEn = GetFilterResult(root, t, Right);
var rightResults = rightResultsEn as ICollection<JToken> ?? rightResultsEn.ToList();

do
Expand Down
32 changes: 29 additions & 3 deletions src/Argon.JsonPath/JTokenExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
/// <summary>
using System.Collections.Concurrent;

/// <summary>
/// Extensions to <see cref="JToken" />.
/// </summary>
public static class JTokenExtensions
{
// A parsed JPath is immutable and evaluation holds no per-query state, so parsing
// (per-segment substrings + filter tree) can be paid once per distinct expression.
// The wholesale clear keeps the cache bounded for pathological dynamic paths.
const int pathCacheLimit = 512;
static readonly ConcurrentDictionary<string, JPath> pathCache = new(StringComparer.Ordinal);

static JPath ParsePath(string path)
{
if (pathCache.TryGetValue(path, out var jPath))
{
return jPath;
}

jPath = new(path);

if (pathCache.Count >= pathCacheLimit)
{
pathCache.Clear();
}

pathCache[path] = jPath;
return jPath;
}

/// <summary>
/// Selects a <see cref="JToken" /> using a JSONPath expression. Selects the token that matches the object path.
/// </summary>
Expand Down Expand Up @@ -37,7 +63,7 @@ public static class JTokenExtensions
/// <returns>A <see cref="JToken" />.</returns>
public static JToken? SelectToken(this JToken token, string path, JsonSelectSettings? settings)
{
var jPath = new JPath(path);
var jPath = ParsePath(path);

settings ??= DefaultSettings;
JToken? result = null;
Expand Down Expand Up @@ -95,7 +121,7 @@ public static IEnumerable<JToken> SelectTokens(this JToken token, string path, J
{
settings ??= DefaultSettings;

var p = new JPath(path);
var p = ParsePath(path);
return p.Evaluate(token, token, settings);
}
}
5 changes: 4 additions & 1 deletion src/Argon/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -685,8 +685,11 @@ internal IReferenceResolver GetReferenceResolver() =>
{
if (converters != null)
{
foreach (var converter in converters)
// indexed loop: foreach over IList<T> boxes the List<T> enumerator, and this
// runs for every value serialized/deserialized
for (var i = 0; i < converters.Count; i++)
{
var converter = converters[i];
if (converter.CanConvert(type))
{
return converter;
Expand Down
18 changes: 8 additions & 10 deletions src/Argon/JsonTextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ void ParseReadString(char quote, ReadType readType)
break;
case ReadType.ReadAsInt32:
case ReadType.ReadAsDecimal:
case ReadType.ReadAsDouble:
case ReadType.ReadAsBoolean:
// caller will convert result
break;
Expand Down Expand Up @@ -647,7 +648,8 @@ JsonReaderException CreateUnexpectedCharacterException(char c) =>
break;
case '"':
case '\'':
ParseString(currentChar, ReadType.Read);
// ReadAsBoolean skips the intermediate string token; the span is re-parsed below
ParseString(currentChar, ReadType.ReadAsBoolean);
return ReadBooleanString(stringReference.AsSpan());
case 'n':
HandleNull();
Expand Down Expand Up @@ -1724,7 +1726,7 @@ void ParseReadNumber(ReadType readType, char firstChar, int initialPosition)
if (singleDigit)
{
// digit char values start at 48
numberValue = (decimal) firstChar - 48;
numberValue = BoxedPrimitives.Get((decimal) firstChar - 48);
}
else
{
Expand All @@ -1747,13 +1749,11 @@ void ParseReadNumber(ReadType readType, char firstChar, int initialPosition)
if (singleDigit)
{
// digit char values start at 48
numberValue = (double) firstChar - 48;
numberValue = BoxedPrimitives.Get((double) firstChar - 48);
}
else
{
var number = stringReference.ToString();

if (double.TryParse(number, NumberStyles.Float, InvariantCulture, out var value))
if (double.TryParse(stringReference.AsSpan(), NumberStyles.Float, InvariantCulture, out var value))
{
numberValue = BoxedPrimitives.Get(value);
}
Expand All @@ -1772,7 +1772,7 @@ void ParseReadNumber(ReadType readType, char firstChar, int initialPosition)
if (singleDigit)
{
// digit char values start at 48
numberValue = (long) firstChar - 48;
numberValue = BoxedPrimitives.Get((long) firstChar - 48);
numberType = JsonToken.Integer;
}
else
Expand Down Expand Up @@ -1811,9 +1811,7 @@ void ParseReadNumber(ReadType readType, char firstChar, int initialPosition)
}
else
{
var number = stringReference.ToString();

if (double.TryParse(number, NumberStyles.Float, InvariantCulture, out var d))
if (double.TryParse(stringReference.AsSpan(), NumberStyles.Float, InvariantCulture, out var d))
{
numberValue = BoxedPrimitives.Get(d);
}
Expand Down
Loading
Loading