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
65 changes: 48 additions & 17 deletions src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
using System.Collections.Concurrent;
using System.Linq.Dynamic.Core.Util.Cache;
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 ParsingConfig _config;

// Static shared instance to prevent duplications of the same objects
private static ThreadSafeSlidingCache<object, Expression>? _expressions;
private static ThreadSafeSlidingCache<Expression, string>? _literals;

public ConstantExpressionHelper(ParsingConfig config)
{
private static readonly ConcurrentDictionary<object, Expression> Expressions = new();
private static readonly ConcurrentDictionary<Expression, string> Literals = new();
_config = config;

}

public static bool TryGetText(Expression expression, out string? text)
private ThreadSafeSlidingCache<Expression, string> GetLiterals()
{
_literals ??= new ThreadSafeSlidingCache<Expression, string>(
_config.ConstantExpressionSlidingCacheTimeToLive,
_config.ConstantExpressionSlidingCacheCleanupFrequency,
_config.ConstantExpressionSlidingCacheMinItemsTrigger
);
return _literals;
}

private ThreadSafeSlidingCache<object, Expression> GetExpression()
{
_expressions ??= new ThreadSafeSlidingCache<object, Expression>(
_config.ConstantExpressionSlidingCacheTimeToLive,
_config.ConstantExpressionSlidingCacheCleanupFrequency,
_config.ConstantExpressionSlidingCacheMinItemsTrigger
);
return _expressions;
}


public bool TryGetText(Expression expression, out string? text)
{
return GetLiterals().TryGetValue(expression, out text);
}

public Expression CreateLiteral(object value, string text)
{
if (GetExpression().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);
}
GetExpression().AddOrUpdate(value, constantExpression);
GetLiterals().AddOrUpdate(constantExpression, text);

return Expressions[value];
}
return constantExpression;
}
}
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 = new ConstantExpressionHelper(_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 = new ConstantExpressionHelper(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 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 = new ConstantExpressionHelper(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
23 changes: 23 additions & 0 deletions src/System.Linq.Dynamic.Core/ParsingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,27 @@ public IQueryableAnalyzer QueryableAnalyzer
/// Default value is <c>false</c>.
/// </summary>
public bool DisallowNewKeyword { get; set; } = false;

/// <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 ConstantExpressionSlidingCacheTimeToLive { 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? ConstantExpressionSlidingCacheMinItemsTrigger { get; set; } = null;


/// <summary>
/// Sets the frequency for running the cleanup process in the Constant Expression cache.
/// By default, cleanup occurs every 10 minutes.
/// </summary>
public TimeSpan ConstantExpressionSlidingCacheCleanupFrequency { get; set; } = TimeSpan.FromMinutes(10);

}
14 changes: 14 additions & 0 deletions src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace System.Linq.Dynamic.Core.Util.Cache;

internal struct CacheContainer<TValue> where TValue : notnull
{
public CacheContainer(TValue value, DateTime expirationTime)
{
Value = value;
ExpirationTime = expirationTime;
}

public TValue Value { get; }

public DateTime ExpirationTime { get; }
}
Loading