Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Sliding Cache to Constant Expression Helper #765

Merged
merged 11 commits into from
Jan 22, 2024
45 changes: 28 additions & 17 deletions src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
using System.Collections.Concurrent;
using System.Linq.Dynamic.Core.Util.Cache;
using System.Linq.Dynamic.Core.Validation;
using System.Linq.Expressions;

namespace System.Linq.Dynamic.Core.Parser
namespace System.Linq.Dynamic.Core.Parser;

internal class ConstantExpressionHelper
{
internal static class ConstantExpressionHelper
private readonly SlidingCache<object, Expression> _expressions;
private readonly SlidingCache<Expression, string> _literals;

public ConstantExpressionHelper(ParsingConfig config)
{
var parsingConfig = Check.NotNull(config);
var useConfig = parsingConfig.ConstantExpressionCacheConfig ?? new CacheConfig();

_literals = new SlidingCache<Expression, string>(useConfig);
_expressions = new SlidingCache<object, Expression>(useConfig);
}

public bool TryGetText(Expression expression, out string? text)
{
private static readonly ConcurrentDictionary<object, Expression> Expressions = new();
private static readonly ConcurrentDictionary<Expression, string> Literals = new();
return _literals.TryGetValue(expression, out text);
}

public static bool TryGetText(Expression expression, out string? text)
public Expression CreateLiteral(object value, string text)
{
if (_expressions.TryGetValue(value, out var outputValue))
{
return Literals.TryGetValue(expression, out text);
return outputValue;
}

public static Expression CreateLiteral(object value, string text)
{
if (!Expressions.ContainsKey(value))
{
ConstantExpression constantExpression = Expression.Constant(value);
var constantExpression = Expression.Constant(value);

Expressions.TryAdd(value, constantExpression);
Literals.TryAdd(constantExpression, text);
}
_expressions.AddOrUpdate(value, constantExpression);
_literals.AddOrUpdate(constantExpression, text);

return Expressions[value];
}
return constantExpression;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace System.Linq.Dynamic.Core.Parser;

internal static class ConstantExpressionHelperFactory
{
private static readonly object Lock = new();
private static ConstantExpressionHelper? _instance;

public static ConstantExpressionHelper GetInstance(ParsingConfig config)
{
if (_instance == null)
{
lock (Lock)
{
_instance ??= new ConstantExpressionHelper(config);
}
}

return _instance;
}
}
6 changes: 4 additions & 2 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class ExpressionParser
private ParameterExpression? _root;
private Type? _resultType;
private bool _createParameterCtor;
private ConstantExpressionHelper _constantExpressionHelper;

/// <summary>
/// Gets name for the `it` field. By default this is set to the KeyWord value "it".
Expand Down Expand Up @@ -81,6 +82,7 @@ public ExpressionParser(ParameterExpression[]? parameters, string expression, ob
_methodFinder = new MethodFinder(_parsingConfig, _expressionHelper);
_typeFinder = new TypeFinder(_parsingConfig, _keywordsHelper);
_typeConverterFactory = new TypeConverterFactory(_parsingConfig);
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(_parsingConfig);

if (parameters != null)
{
Expand Down Expand Up @@ -900,7 +902,7 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
}

_textParser.NextToken();
return ConstantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue);
return _constantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue);
}

_textParser.NextToken();
Expand All @@ -924,7 +926,7 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)

parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos);

return ConstantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
return _constantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
}

private Expression ParseIntegerLiteral()
Expand Down
4 changes: 3 additions & 1 deletion src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace System.Linq.Dynamic.Core.Parser
public class ExpressionPromoter : IExpressionPromoter
{
private readonly NumberParser _numberParser;
private readonly ConstantExpressionHelper _constantExpressionHelper;

/// <summary>
/// Initializes a new instance of the <see cref="ExpressionPromoter"/> class.
Expand All @@ -17,6 +18,7 @@ public class ExpressionPromoter : IExpressionPromoter
public ExpressionPromoter(ParsingConfig config)
{
_numberParser = new NumberParser(config);
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config);
}

/// <inheritdoc />
Expand Down Expand Up @@ -48,7 +50,7 @@ public ExpressionPromoter(ParsingConfig config)
}
else
{
if (ConstantExpressionHelper.TryGetText(ce, out var text))
if (_constantExpressionHelper.TryGetText(ce, out var text))
{
Type target = TypeHelper.GetNonNullableType(type);
object? value = null;
Expand Down
34 changes: 18 additions & 16 deletions src/System.Linq.Dynamic.Core/Parser/NumberParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class NumberParser
private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' };
private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' };
private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" };
private readonly ConstantExpressionHelper _constantExpressionHelper;

private readonly CultureInfo _culture;

Expand All @@ -26,6 +27,7 @@ public class NumberParser
public NumberParser(ParsingConfig? config)
{
_culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture;
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default);
}

/// <summary>
Expand Down Expand Up @@ -77,38 +79,38 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text)
{
if (qualifier == "U" || qualifier == "u")
{
return ConstantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
}

if (qualifier == "L" || qualifier == "l")
{
return ConstantExpressionHelper.CreateLiteral((long)unsignedValue, text);
return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text);
}

if (QualifiersReal.Contains(qualifier))
{
return ParseRealLiteral(text, qualifier[0], false);
}

return ConstantExpressionHelper.CreateLiteral(unsignedValue, text);
return _constantExpressionHelper.CreateLiteral(unsignedValue, text);
}

if (unsignedValue <= int.MaxValue)
{
return ConstantExpressionHelper.CreateLiteral((int)unsignedValue, text);
return _constantExpressionHelper.CreateLiteral((int)unsignedValue, text);
}

if (unsignedValue <= uint.MaxValue)
{
return ConstantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
}

if (unsignedValue <= long.MaxValue)
{
return ConstantExpressionHelper.CreateLiteral((long)unsignedValue, text);
return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text);
}

return ConstantExpressionHelper.CreateLiteral(unsignedValue, text);
return _constantExpressionHelper.CreateLiteral(unsignedValue, text);
}

if (isHexadecimal || isBinary)
Expand All @@ -135,7 +137,7 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text)
{
if (qualifier == "L" || qualifier == "l")
{
return ConstantExpressionHelper.CreateLiteral(value, text);
return _constantExpressionHelper.CreateLiteral(value, text);
}

if (QualifiersReal.Contains(qualifier))
Expand All @@ -148,10 +150,10 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text)

if (value <= int.MaxValue)
{
return ConstantExpressionHelper.CreateLiteral((int)value, text);
return _constantExpressionHelper.CreateLiteral((int)value, text);
}

return ConstantExpressionHelper.CreateLiteral(value, text);
return _constantExpressionHelper.CreateLiteral(value, text);
}

/// <summary>
Expand All @@ -163,18 +165,18 @@ public Expression ParseRealLiteral(string text, char qualifier, bool stripQualif
{
case 'f':
case 'F':
return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(float))!, text);
return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(float))!, text);

case 'm':
case 'M':
return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(decimal))!, text);
return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(decimal))!, text);

case 'd':
case 'D':
return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(double))!, text);
return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(double))!, text);

default:
return ConstantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text);
return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text);
}
}

Expand Down Expand Up @@ -285,12 +287,12 @@ private Expression ParseAsBinary(int tokenPosition, string text, bool isNegative
{
if (RegexBinary32.IsMatch(text))
{
return ConstantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text);
return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text);
}

if (RegexBinary64.IsMatch(text))
{
return ConstantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text);
return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text);
}

throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition);
Expand Down
6 changes: 6 additions & 0 deletions src/System.Linq.Dynamic.Core/ParsingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Globalization;
using System.Linq.Dynamic.Core.CustomTypeProviders;
using System.Linq.Dynamic.Core.Parser;
using System.Linq.Dynamic.Core.Util.Cache;

namespace System.Linq.Dynamic.Core;

Expand Down Expand Up @@ -234,4 +235,9 @@ public IQueryableAnalyzer QueryableAnalyzer
/// Default value is <c>false</c>.
/// </summary>
public bool DisallowNewKeyword { get; set; } = false;

/// <summary>
/// Caches constant expressions to enhance performance. Periodic cleanup is performed to manage cache size, governed by this configuration.
/// </summary>
public CacheConfig? ConstantExpressionCacheConfig { get; set; }
}
27 changes: 27 additions & 0 deletions src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace System.Linq.Dynamic.Core.Util.Cache;

/// <summary>
/// Cache Configuration Options
/// </summary>
public class CacheConfig
{
/// <summary>
/// Sets a Time-To-Live (TTL) for items in the constant expression cache to prevent uncontrolled growth.
/// Items not accessed within this TTL will be expired, allowing garbage collection to reclaim the memory.
/// Default is 10 minutes.
/// </summary>
public TimeSpan TimeToLive { get; set; } = TimeSpan.FromMinutes(10);

/// <summary>
/// Configures the minimum number of items required in the constant expression cache before triggering cleanup.
/// This prevents frequent cleanups, especially in caches with few items.
/// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items.
/// </summary>
public int? MinItemsTrigger { get; set; }

/// <summary>
/// Sets the frequency for running the cleanup process in the Constant Expression cache.
/// By default, cleanup occurs every 10 minutes.
/// </summary>
public TimeSpan CleanupFrequency { get; set; } = TimeSpan.FromMinutes(10);
}
14 changes: 14 additions & 0 deletions src/System.Linq.Dynamic.Core/Util/Cache/CacheEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace System.Linq.Dynamic.Core.Util.Cache;

internal struct CacheEntry<TValue> where TValue : notnull
{
public TValue Value { get; }

public DateTime ExpirationTime { get; }

public CacheEntry(TValue value, DateTime expirationTime)
{
Value = value;
ExpirationTime = expirationTime;
}
}
Loading