From f663ad3813a87639031e2064d997ba77e9d1a89c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 25 Apr 2024 10:26:35 +0200 Subject: [PATCH] Cache JavaScript Expressions (#5273) * Add caching for scripts in Jint JavaScript engine A new feature has been implemented to cache the scripts in the Jint JavaScript engine for performance improvement. A new property, "ScriptCacheTimeout", has been introduced to determine the script caching duration. The scripts will be recompiled after the specified duration has elapsed. If the value is null, the scripts will be cached indefinitely. Also, dependencies and changes have been made in the relevant classes to support this feature. * Set default value for ScriptCacheTimeout in JintOptions The change sets a default value for the ScriptCacheTimeout property in the JintOptions.cs file. Previously, ScriptCacheTimeout did not have a default value. Now, ScriptCacheTimeout defaults to one day if no other value is specified. * Refactor JsonSerializerOptions creation in JintJavaScriptEvaluator This change breaks down the creation of JsonSerializerOptions into several lines to improve readability. It also keeps JavaScriptEncoder as the encoder but presents the creation of the object in a more explicit manner. --- .../Features/JavaScriptFeature.cs | 2 ++ .../Elsa.JavaScript/Options/JintOptions.cs | 9 +++++++ .../Services/JintJavaScriptEvaluator.cs | 24 +++++++++++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs b/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs index 7700d3708..3bc8b2945 100644 --- a/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs +++ b/src/modules/Elsa.JavaScript/Features/JavaScriptFeature.cs @@ -1,3 +1,4 @@ +using Elsa.Caching.Features; using Elsa.Common.Features; using Elsa.Expressions.Features; using Elsa.Extensions; @@ -24,6 +25,7 @@ namespace Elsa.JavaScript.Features; /// [DependsOn(typeof(MediatorFeature))] [DependsOn(typeof(ExpressionsFeature))] +[DependsOn(typeof(MemoryCacheFeature))] public class JavaScriptFeature : FeatureBase { /// diff --git a/src/modules/Elsa.JavaScript/Options/JintOptions.cs b/src/modules/Elsa.JavaScript/Options/JintOptions.cs index c2aa19c5f..f543c60a6 100644 --- a/src/modules/Elsa.JavaScript/Options/JintOptions.cs +++ b/src/modules/Elsa.JavaScript/Options/JintOptions.cs @@ -22,6 +22,15 @@ public class JintOptions /// public bool AllowConfigurationAccess { get; set; } + /// + /// The timeout for script caching. + /// + /// + /// The ScriptCacheTimeout property specifies the duration for which the scripts are cached in the Jint JavaScript engine. When a script is executed, it is compiled and cached for future use. This caching improves performance by avoiding repetitive compilation of the same script. + /// If the value of ScriptCacheTimeout is null, the scripts are cached indefinitely. If a time value is specified, the scripts will be recompiled after the specified duration has elapsed. + /// + public TimeSpan? ScriptCacheTimeout { get; set; } = TimeSpan.FromDays(1); + /// /// A list of callbacks that are invoked when the Jint engine is created. Use this to configure the engine. /// diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index 16163f7b3..a6dff6543 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -12,14 +12,16 @@ using Elsa.JavaScript.Helpers; using Elsa.JavaScript.Notifications; using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; +using Esprima; +using Esprima.Ast; using Humanizer; using Jint; using Jint.Runtime.Interop; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; // ReSharper disable ConvertClosureToMethodGroup - namespace Elsa.JavaScript.Services; /// @@ -28,15 +30,17 @@ namespace Elsa.JavaScript.Services; public class JintJavaScriptEvaluator : IJavaScriptEvaluator { private readonly INotificationSender _mediator; + private readonly IMemoryCache _memoryCache; private readonly JintOptions _jintOptions; private readonly IConfiguration _configuration; /// /// Constructor. /// - public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions scriptOptions) + public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions scriptOptions, IMemoryCache memoryCache) { _mediator = mediator; + _memoryCache = memoryCache; _jintOptions = scriptOptions.Value; _configuration = configuration; } @@ -176,9 +180,21 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func)(() => context.GetOutput(activityOutput.ActivityId, outputName))); } - private static object? ExecuteExpressionAndGetResult(Engine engine, string expression) + private object? ExecuteExpressionAndGetResult(Engine engine, string expression) { - var result = engine.Evaluate(expression); + var cacheKey = "jint:script:" + expression.GetHashCode(StringComparison.Ordinal); + + var parsedScript = _memoryCache.GetOrCreate(cacheKey, entry => + { + if (_jintOptions.ScriptCacheTimeout.HasValue) + entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value); + + var parser = new JavaScriptParser(new ParserOptions { AllowReturnOutsideFunction = true }); + var script = parser.ParseScript(expression); + return script; + })!; + + var result = engine.Evaluate(parsedScript); return result.ToObject(); }