What you need to know about JavaScript Engine Switcher 3.0

    Logo of the third version of JavaScript Engine Switcher


    JavaScript Engine Switcher was originally created as a supporting library and its development was largely determined by the needs of the libraries that used it. In fact, each major version of it solved one or several major tasks necessary for the further development of other libraries:


    1. In the first version, the task was to add as many adapter modules as possible for the popular JS engines that support the .NET platform. And this gave Bundle Transformer users some flexibility: on development computers they could use the MSIE module , which supports debugging of JS code using Visual Studio, and on servers that did not have a modern version of Internet Explorer or it was not installed at all, they could V8 module . Some even managed to run the Bundle Transformer in Mono on Linux and Mac using the Jurassic and Jint modules .
    2. The main objective of the second version was the implementation of .NET Core support, which was required for the new version of the ReactJS.NET library . Another important task was to create a cross-platform module capable of quickly processing large volumes of JS code (the Jurassic and Jint modules were not suitable for this), and such a module, after a number of improvements, was the ChakraCore module .
    3. In the third version, the main focus was on improving integration with the ReactJS.NET library and increasing productivity.

    In this article, we will look at some of the innovations of the third version, which for many turned out to be not obvious even after reading the release text and the “How to upgrade applications to version 3.X” section of the documentation : changes in the class JsEngineSwitcher, reorganization of exceptions, more informative error messages, interruption and preliminary compilation of scripts, the ability to change the maximum stack size in the ChakraCore and MSIE modules, as well as a new module based on NiL.JS.


    JsEngineSwitcher class changes


    In the new version, the class JsEngineSwitcherimplements the interface IJsEngineSwitcherand is no longer a singleton (its instance can be created using an operator new). To obtain a global instance instead of a property Instance, use the property Current. A property Current, unlike an obsolete property Instance, has a return type IJsEngineSwitcher. The property also Currenthas a setter, with which you can replace the standard implementation with your own:


    JsEngineSwitcher.Current = new MyJsEngineSwitcher();

    In ASP.NET Core web applications where the JavaScriptEngineSwitcher.Extensions.MsDependencyInjection package is installed , the implementation is replaced using the extension method AddJsEngineSwitcher:


    using JavaScriptEngineSwitcher.Extensions.MsDependencyInjection;
    …
        publicclassStartup
        {
            …
            publicvoidConfigureServices(IServiceCollection services)
            {
                …
                services.AddJsEngineSwitcher(new MyJsEngineSwitcher(), options =>
                    …
                )
                    …
                    ;
                …
            }
            …
        }
    …

    These changes almost always "break" applications or libraries that use the previous version of JavaScript Engine Switcher. Therefore, you need to make the following changes to your code:


    1. Change the type of variables, parameters or properties from JsEngineSwitcherto IJsEngineSwitcher.
    2. Instead of a property, Instanceuse the property everywhere Current.

    It is also worth noting that for most developers, these changes will not be particularly useful, because their main goal was to simplify unit tests (for example, blocking was used in the unit tests of the ReactJS.NET library , and now you can do without them ).


    Reorganization of exceptions


    Prior to the third version, most of the JS-engine errors turned into type exclusions JsRuntimeException, and only errors that occurred during the engine initialization process turned around JsEngineLoadException. There was also a base class JsException, from which the two above-mentioned types of exceptions were inherited, which made it possible to intercept absolutely all errors that occurred in the process of running JS engines. Despite obvious shortcomings, such an organization of exceptions fit well into the concept of a unified interface for accessing the basic features of JS engines.


    But when realizing the possibilities of interruption and preliminary compilation of scripts (I will tell about them in the following sections), a need arose for a new approach to the organization of exceptions. The first thing to do was add a new type of exception -JsInterruptedExceptionwhich was necessary to notify the user about the interruption of script execution. Then it was necessary to explicitly divide all errors that occurred while processing scripts into two groups: compilation errors (syntax analysis) and runtime errors. Also required to separate all sorts of specific errors Chakra and V8, which were not associated with the processing of scripts. It was also necessary to take into account the presence in the Jint engine of an exception that occurs when the script execution timeout (timeout) expires. As a result, a new approach to the organization of exceptions was formed, which can be represented in the form of the following hierarchical structure:


    • JsException
      • JsEngineException
        • JsEngineLoadException
      • JsFatalException
      • JsScriptException
        • JsCompilationException
        • JsRuntimeException
          • JsInterruptedException
          • JsTimeoutException
      • JsUsageException
    • JsEngineNotFoundException*

    * - this exception occurs not at the level of the JS engine, but at the level of JavaScript Engine Switcher.


    I think that the hierarchy of exceptions presented above does not need comments, because the names of the exceptions speak for themselves. With this approach, we get not only more information about the causes of the error, but we can more flexibly handle certain types of exceptions.


    Unified error message format


    Another problem with previous versions of JavaScript Engine Switcher was the difficulty in locating errors that occurred during script processing. From the exception property Messageit was difficult to understand exactly where the error occurred, so it was necessary to analyze other exception properties, which was not always convenient. In addition, a set of existing properties was also insufficient.


    Therefore JsScriptException, 2 new properties have been added to the class :


    1. Type - type of JavaScript error (for example, SyntaxErroror TypeError);
    2. DocumentName - the name of the document (usually derived from the values ​​of the following parameters: documentNamemethods Executeand Evaluate, pathmethod ExecuteFile, resourceNamemethod ExecuteResource, etc.);

    JsRuntimeExceptionOne new property has also been added to the class - CallStack , which contains a string representation of the call stack.


    Previously, a property Messagesimply copied a value from a similar property of the original .NET exception or a string representation of a JavaScript error. Often, error messages in different JS engines differed not only in the format, but also in the amount of useful information presented in them. For example, due to the lack of information about row and column number in some error messages, the developers of the ReactJS.NET library had to override the exceptions obtained from the JavaScript Engine Switcher.


    Therefore, I decided to generate my own error messages at the level of adapter modules that would have a single (unified) format. This format uses all available error information: type, description, document name, line number, column number, code fragment, and call stack. As a basis for the new format, I took the error format from the Microsoft ClearScript library .


    Below are the messages about the same compilation error that were generated by different adapter modules:


    ChakraCore
    ==========
    SyntaxError: Unexpected identifier after numeric literal
       at declinationOfSeconds.js:12:23 ->          caseIndex = number % 1O < 5 ? number % 10 : 5;
    Jint
    ====
    SyntaxError: Unexpected token ILLEGAL
       at declinationOfSeconds.js:12:25
    Jurassic
    ========
    SyntaxError: Expected operator but found 'O'
       at declinationOfSeconds.js:12
    MSIE в режиме Classic
    =====================
    SyntaxError: Expected ';'
       at declinationOfSeconds.js:12:25 ->          caseIndex = number % 1O < 5 ? number % 10 : 5;
    MSIE в режиме Chakra ActiveScript
    =================================
    SyntaxError: Expected ';'
       at declinationOfSeconds.js:12:25 ->          caseIndex = number % 1O < 5 ? number % 10 : 5;
    MSIE в режиме Chakra IE JsRT
    ============================
    SyntaxError: Expected ';'
       at 12:25 ->          caseIndex = number % 1O < 5 ? number % 10 : 5;
    MSIE в режиме Chakra Edge JsRT
    ==============================
    SyntaxError: Unexpected identifier after numeric literal
       at declinationOfSeconds.js:12:23 ->          caseIndex = number % 1O < 5 ? number % 10 : 5;
    NiL
    ===
    SyntaxError: Unexpected token 'O'
       at 12:25
    V8
    ==
    SyntaxError: Invalid or unexpected token
       at declinationOfSeconds.js:12:24 ->          caseIndex = number % 1O < 5 ? number % 10 : 5;
    Vroom
    =====
    SyntaxError: Unexpected token ILLEGAL
       at declinationOfSeconds.js:12:24

    A similar example for a runtime error:


    ChakraCore
    ==========
    TypeError: Unable to get property 'Ч' of undefined or null reference
       at transliterate (russian-translit.js:929:4) ->                      newCharValue = typeof charMapping[charValue] !== 'undefined' ?
       at Global code (Script Document:1:1)
    Jint
    ====
    TypeError: charMapping is undefined
       at russian-translit.js:929:26
    Jurassic
    ========
    TypeError: undefined cannot be converted to an object
       at transliterate (russian-translit.js:929)
       at Global code (Script Document:1)
    MSIE в режиме Classic
    =====================
    TypeError: 'undefined' is null or not an object
       at russian-translit.js:929:4
    MSIE в режиме Chakra ActiveScript
    =================================
    TypeError: Unable to get property 'Ч' of undefined or null reference
       at russian-translit.js:929:4
    MSIE в режиме Chakra IE JsRT
    ============================
    TypeError: Unable to get property 'Ч' of undefined or null reference
       at transliterate (russian-translit.js:929:4)
       at Global code (Script Document:1:1)
    MSIE в режиме Chakra Edge JsRT
    ==============================
    TypeError: Unable to get property 'Ч' of undefined or null reference
       at transliterate (russian-translit.js:929:4)
       at Global code (Script Document:1:1)
    NiL
    ===
    TypeError: Can't get property "Ч" of "undefined"
    V8
    ==
    TypeError: Cannot read property 'Ч' of undefined
       at transliterate (russian-translit.js:929:37) ->                     newCharValue = typeof charMapping[charValue] !== 'undefined' ?
       at Script Document:1:1
    Vroom
    =====
    TypeError: Cannot read property 'Ч' of undefined
       at russian-translit.js:929:37

    From the examples it can be seen that some JS engines give us completely different error descriptions and column numbers, and not always we can get a complete set of error data, but even despite these shortcomings, the uniform format gives us more information about the location of the error than original error messages.


    Tips for deploying native builds


    The main cause of errors when working with the second version of JavaScript Engine Switcher was that many developers forgot to install NuGet packages containing native assemblies for ChakraCore and V8 modules. At one time, this post was even devoted to a post in the ReactJS.NET bugtracker (a Russian translation is also available ). Now, this is mostly a bug for newbies who, for some reason, have not read the documentation.


    The authors of ReactJS.NET tried to minimize the number of such errors with the help of hints inside error messages, but the not very successful implementation of this approach led to even more confusion . The idea of ​​the prompts was good, but it required a fundamentally different implementation, namely, the implementation of JS engines at the level of adapter modules. In the new version of JavaScript Engine Switcher, such hints are added to the error message when wrapping exceptions DllNotFoundExceptionand TypeLoadExceptionexceptions JsEngineLoadException(see the implementation examples for ChakraCore , V8 and Vroom modules). Moreover, these tips are intelligent, because their generation takes into account a number of factors: the type of operating system, processor architecture and runtime (.NET Framework, .NET Core or Mono).


    For example, if you use the ChakraCore module without a native build in a 64-bit process on the Windows operating system, the error message will look like this:


    Failed to create an instance of the ChakraCoreJsEngine. Most likely it happened, because of the dependencies was not found. Try to install the JavaScriptEngineSwitcher.ChakraCore.Native.win-x64 package via NuGet. In addition, you still need to install the Microsoft Visual C ++ Redistributable for Visual Studio 2017 ( https://www.visualstudio.com/downloads/#microsoft-visual-c-redistributable-for-visual-studio-2017 ).

    The error message prompts you to install the NuGet package JavaScriptEngineSwitcher.ChakraCore.Native.win-x64, and also mentions that ChakraCore for Windows requires a Microsoft Visual C ++ redistributable component for Visual Studio 2017 to work. If this error will occur in a 32-bit process, the user will be prompted to install the JavaScriptEngineSwitcher.ChakraCore.Native.win-x86 package.


    A similar error message on Linux in the .NET Core environment will look like this:


    Failed to create an instance of the ChakraCoreJsEngine. Most likely it happened, because of its dependencies was not found. Try to install the JavaScriptEngineSwitcher.ChakraCore.Native.linux-x64 package via NuGet.

    In this case, you will be prompted to install the JavaScriptEngineSwitcher.ChakraCore.Native.linux-x64 package.


    When launched in Mono, another hint will be displayed:


    JavaScriptEngineSwitcher.ChakraCore.Native.linux--packages * ... do not support installation under the Mono, But you CAN to the install the native assembly manually ( https://github.com/Taritsyn/JavaScriptEngineSwitcher/wiki/ChakraCore#linux ).

    Since the JavaScriptEngineSwitcher.ChakraCore.Native.linux-x64 package is only compatible with .NET Core, the prompt will link to instructions for manually deploying the native build in Linux.


    You can still give many examples, but this makes no sense.


    Interrupting Script Execution


    When we give users the ability to execute arbitrary JS code on the server, we face one very serious problem - we don’t know how long it will take to execute this code. This may be a large amount of non-optimal code or a code that starts an infinite loop. In any case, it will be code that we cannot control, which will consume our server resources indefinitely. In order to somehow control this process, we need the ability to interrupt the execution of scripts. When using engines written in pure .NET (for example, Jint, Jurassic or NiL.JS), we can always start executing JS code as a task with the possibility of cancellation, but this approach will not work for other engines. Fortunately for us, the engines written in C ++ have built-in mechanisms for interrupting scripts.


    To provide access to these mechanisms IJsEngine, a property SupportsScriptInterruptionand a method have been added to the interface Interrupt. Since not all engines support this feature, you Interruptshould always check the property value before calling the method SupportsScriptInterruption(if in previous versions of JavaScript Engine Switcher you had to manually start the garbage collector, then you will immediately understand what I am talking about):


    if (engine.SupportsScriptInterruption)
    {
        engine.Interrupt();
    }

    Moreover, it is necessary to call this method in a separate thread other than the one in which the scripts are executed. After calling Interruptall previously launched methods Evaluate, Execute*and CallFunctionwill throw an exception completed JsInterruptedException.


    Since this API is low-level, for the tasks described at the beginning of the section it is recommended to use extension methods like this:


    using System;
    #if !NET40using System.Runtime.ExceptionServices;
    #endifusing System.Threading;
    using System.Threading.Tasks;
    using JavaScriptEngineSwitcher.Core;
    #if NET40using JavaScriptEngineSwitcher.Core.Extensions;
    #endifusing JavaScriptEngineSwitcher.Core.Resources;
    …
        ///<summary>/// Extension methods for <see cref="IJsEngine"/>///</summary>publicstaticclassJsEngineExtensions
        {
            ///<summary>/// Evaluates an expression within a specified time interval///</summary>///<typeparam name="T">Type of result</typeparam>///<param name="engine">JS engine</param>///<param name="expression">JS expression</param>///<param name="timeoutInterval">Interval to wait before the/// script execution times out</param>///<param name="documentName">Document name</param>///<returns>Result of the expression</returns>///<exception cref="ObjectDisposedException"/>///<exception cref="ArgumentNullException"/>///<exception cref="ArgumentException"/>///<exception cref="JsCompilationException"/>///<exception cref="JsTimeoutException"/>///<exception cref="JsRuntimeException"/>///<exception cref="JsException"/>publicstatic T Evaluate<T>(this IJsEngine engine, string expression,
                TimeSpan timeoutInterval, string documentName)
            {
                if (engine == null)
                {
                    thrownew ArgumentNullException(nameof(engine));
                }
                if (engine.SupportsScriptInterruption)
                {
                    using (var timer = new Timer(state => engine.Interrupt(), null,
                        timeoutInterval,
    #if NET40new TimeSpan(0, 0, 0, 0, -1)))
    #else
                        Timeout.InfiniteTimeSpan))
    #endif
                    {
                        try
                        {
                            return engine.Evaluate<T>(expression, documentName);
                        }
                        catch (JsInterruptedException e)
                        {
                            thrownew JsTimeoutException(
                                Strings.Runtime_ScriptTimeoutExceeded,
                                e.EngineName, e.EngineVersion, e
                            );
                        }
                    }
                }
                else
                {
    #if NET40
                    Task<T> task = Task.Factory.StartNew(() =>
    #else
                    Task<T> task = Task.Run(() =>
    #endif
                    {
                        return engine.Evaluate<T>(expression, documentName);
                    });
                    bool isCompletedSuccessfully = false;
                    try
                    {
                        isCompletedSuccessfully = task.Wait(timeoutInterval);
                    }
                    catch (AggregateException e)
                    {
                        Exception innerException = e.InnerException;
                        if (innerException != null)
                        {
    #if NET40
                            innerException.PreserveStackTrace();
                            throw innerException;
    #else
                            ExceptionDispatchInfo.Capture(innerException).Throw();
    #endif
                        }
                        else
                        {
                            throw;
                        }
                    }
                    if (isCompletedSuccessfully)
                    {
                        return task.Result;
                    }
                    else
                    {
                        thrownew JsTimeoutException(
                            Strings.Runtime_ScriptTimeoutExceeded,
                            engine.Name, engine.Version
                        );
                    }
                }
            }
            …
        }
    …

    This method is a superstructure over the engine method Evaluate<T>, which allows using the parameter to timeoutIntervalset the timeout for the script execution. The principle of this extension method is very simple. First, we check if our engine supports the built-in interrupt mechanism. If it does, then we create an instance of the class Timerthat timeoutIntervalstarts the method after the time specified in the parameter Interrupt, then we call the engine method Evaluate<T>, and in case of an error, we catch the exception JsInterruptedExceptionand wrap it in a type exception JsTimeoutException. If the engine does not support interrupts, then create an instance of the class Taskthat runs the engine method Evaluate<T>, then set the task timeout interval equal to the value of the parametertimeoutInterval, and if the task is completed by timeout, then we generate a type exception JsTimeoutException. The following is an example of using this extension method:


    using System;
    …
    using JavaScriptEngineSwitcher.Core;
    using JavaScriptEngineSwitcher.Core.Helpers;
    …
        classProgram
        {
            …
            staticvoidMain(string[] args)
            {
                conststring expression = @"function getRandomInt(minValue, maxValue) {
        minValue = Math.ceil(minValue);
        maxValue = Math.floor(maxValue);
        return Math.floor(Math.random() * (maxValue - minValue + 1)) + minValue;
    }
    function sleep(millisecondsTimeout) {
        var totalMilliseconds = new Date().getTime() + millisecondsTimeout;
        while (new Date().getTime() < totalMilliseconds)
        { }
    }
    var randomNumber = getRandomInt(1, 10);
    sleep(randomNumber * 1000);
    randomNumber;";
                using (IJsEngine engine = JsEngineSwitcher.Current.CreateDefaultEngine())
                {
                    try
                    {
                        int result = engine.Evaluate<int>(expression,
                            TimeSpan.FromSeconds(3), "randomNumber.js");
                        Console.WriteLine("результат = {0}", result);
                    }
                    catch (JsTimeoutException)
                    {
                        Console.WriteLine("Во время вычисления выражения JavaScript " +
                            "было превышено время ожидания!");
                    }
                    catch (JsException e)
                    {
                        Console.WriteLine("Во время работы JavaScript-движка произошла " +
                            "ошибка!");
                        Console.WriteLine();
                        Console.WriteLine(JsErrorHelpers.GenerateErrorDetails(e));
                    }
                }
            }
            …
        }
    …

    Using the extension method, we calculate the result of a JS expression. The result of the expression is a random integer, and this number is also the number of seconds by which the execution of this expression is delayed. Also, when calling the extension method, we specify a wait interval of 3 seconds. Since the time to calculate the expression varies from 1 to 10 seconds, in one case the result of the expression will be displayed on the console, and in the other the message about waiting time is displayed.


    It should not be difficult for you to remake this method extension to call other methods of the engine (for example, methods Execute*, CallFunctionor other overloaded versions of the method Evaluate). While I have not yet decided whether I will add such extension methods to the library itself, because it is not yet clear how much they will be in demand.


    Pre-compiling scripts


    I was asked to add support for pre-compiling scripts back in 2015 . At that moment, it was not clear how such a function could fit into the concept of a unified interface. But as time went on, support for special features such as garbage collection and script interruption began to appear in JavaScript Engine Switcher. At the last stages of work on the third version, the turn reached the preliminary compilation.


    Using precompilation, you can compile the script once, and then use it many times to initialize the JS engines. Due to the fact that the pre-compiled script does not require syntactic analysis, the initialization of the engines will be faster.


    At the moment, 5 modules-adapters support preliminary compilation: ChakraCore, Jint, Jurassic, MSIE (only in JsRT-modes) and V8. To provide access to the appropriate mechanisms of the engines in the interface IJsEnginehas been added to the property SupportsScriptPrecompilation, and 3 new methods: Precompile, PrecompileFileand PrecompileResource. The methods Precompile*return an instance of the object that implements the interface IPrecompiledScript. This object is a pre-compiled script that can be used by different engine instances (for these purposes an overloaded version of the method serves Execute). Consider a simple example of using this API:


    using System;
    …
    using JavaScriptEngineSwitcher.Core;
    using JavaScriptEngineSwitcher.Core.Helpers;
    …
        classProgram
        {
            …
            staticvoidMain(string[] args)
            {
                conststring sourceCode = @"function declinationOfSeconds(number) {
        var result,
            titles = ['секунда', 'секунды', 'секунд'],
            titleIndex,
            cases = [2, 0, 1, 1, 1, 2],
            caseIndex
            ;
        if (number % 100 > 4 && number % 100 < 20) {
            titleIndex = 2;
        }
        else {
            caseIndex = number % 10 < 5 ? number % 10 : 5;
            titleIndex = cases[caseIndex];
        }
        result = number + ' ' + titles[titleIndex];
        return result;
    }";
                conststring functionName = "declinationOfSeconds";
                constint itemCount = 4;
                int[] inputSeconds = newint[itemCount] { 0, 1, 42, 600 };
                string[] outputStrings = newstring[itemCount];
                IJsEngineSwitcher engineSwitcher = JsEngineSwitcher.Current;
                IPrecompiledScript precompiledCode = null;
                using (var engine = engineSwitcher.CreateDefaultEngine())
                {
                    if (!engine.SupportsScriptPrecompilation)
                    {
                        Console.WriteLine("{0} версии {1} не поддерживает " +
                            "предварительную компиляцию скриптов!",
                            engine.Name, engine.Version);
                        return;
                    }
                    try
                    {
                        precompiledCode = engine.Precompile(sourceCode,
                            "declinationOfSeconds.js");
                        engine.Execute(precompiledCode);
                        outputStrings[0] = engine.CallFunction<string>(functionName,
                            inputSeconds[0]);
                    }
                    catch (JsCompilationException e)
                    {
                        Console.WriteLine("Во время предварительной компиляции скрипта " +
                            "произошла ошибка!");
                        Console.WriteLine();
                        Console.WriteLine(JsErrorHelpers.GenerateErrorDetails(e));
                        return;
                    }
                    catch (JsException e)
                    {
                        Console.WriteLine("Во время работы JavaScript-движка произошла " +
                            "ошибка!");
                        Console.WriteLine();
                        Console.WriteLine(JsErrorHelpers.GenerateErrorDetails(e));
                        return;
                    }
                }
                for (int itemIndex = 1; itemIndex < itemCount; itemIndex++)
                {
                    using (var engine = engineSwitcher.CreateDefaultEngine())
                    {
                        try
                        {
                            engine.Execute(precompiledCode);
                            outputStrings[itemIndex] = engine.CallFunction<string>(
                                functionName, inputSeconds[itemIndex]);
                        }
                        catch (JsException e)
                        {
                            Console.WriteLine("Во время работы JavaScript-движка " +
                                "произошла ошибка!");
                            Console.WriteLine();
                            Console.WriteLine(JsErrorHelpers.GenerateErrorDetails(e));
                            return;
                        }
                    }
                }
                for (int itemIndex = 0; itemIndex < itemCount; itemIndex++)
                {
                    Console.WriteLine(outputStrings[itemIndex]);
                }
            }
            …
        }
    …

    First, we create an engine with which we compile the function code for declining numbers. After that, we use the property SupportsScriptPrecompilationto check whether the engine supports preliminary compilation, if not, we inform the user about it. Then, using the method we Precompilecompile the script, if the script code contains syntax errors, then type exceptions will be thrown JsCompilationException. Using the method, we Executeload the compiled script into the engine memory, i.e. produce its initialization. Then we CallFunctioncall the function using the method.declinationOfSecondsand save the result to an array. After that, the destruction of the engine. Despite the fact that the engine was destroyed, the script compiled by it continues to exist and can be used by other engines. Next, we create another 3 engines and each of them is initialized with a compiled script. After initialization, as in the case of the very first engine, we call the function and save its result into an array. At the end of the example, display the contents of this array on the screen to make sure that the initialization of the engines passed without errors.


    The previous example shows how to use precompilation, but it is a bit artificial. In real projects, most likely, you will use the engine pooling and store compiled scripts in the cache. This is the approach used in the ReactJS.NET project. The engine pooling is implemented using the JSPool library , and the choice of cache implementation depends on the framework used ( System.Runtime.Caching.MemoryCachefor the .NET Framework 4.X, System.Web.Caching.Cachefor ASP.NET 4.X and Microsoft.Extensions.Caching.Memory.IMemoryCachefor ASP.NET Core). I will not go into the details of the implementation, because it will take too much time (if you wish, you can always familiarize yourself with the source code ). Instead, we’ll consider how to precompile scripts in ReactJS.NET instead.


    By default, precompilation in ReactJS.NET is disabled. Since this feature is still considered experimental and has not yet been applied on real high-load sites. To enable it, you need to set the configuration property AllowJavaScriptPrecompilationequal true.


    In ASP.NET 4.X, for this you need to edit the file App_Start/ReactConfig.cs:


    publicstaticclassReactConfig
        {
            publicstaticvoidConfigure()
            {
                ReactSiteConfiguration.Configuration
                    …
                    .SetAllowJavaScriptPrecompilation(true)
                    …
                    ;
                …
            }
        }
    …

    In ASP.NET Core, this setting is made in the file Startup.cs:


    publicclassStartup
        {
            …
            publicvoidConfigure(IApplicationBuilder app, IHostingEnvironment env)
            {
                …
                app.UseReact(config =>
                {
                    config
                        …
                        .SetAllowJavaScriptPrecompilation(true)
                        …
                        ;
                });
                app.UseStaticFiles();
                …
            }
        }
    …

    At the end of the section, I want to show you how precompilation affects performance. As an example, consider the results of the JsExecutionBenchmark benchmark implemented by the BenchmarkDotNet library . Here, a small JS library is used to execute the Russian text in Latin as an executable script. It weighs only 14.9 KB, but this will be enough for us to see the effect of using precompilation. During testing, the latest version of JavaScript Engine Switcher (version 3.0.4) was used.


    Let's start with the results obtained when running in the .NET Framework 4.7.2:


    Module NamePreliminary compilationThe average will continue. fulfillmentGen 0 per thousand operas.Gen 1 per thousand operas.Gen 2 per thousand operas.Memory allocation for one opera.
    ChakraCoreNot41.72 ms---74.46 Kb
    Yes35.07 ms---91.79 Kb
    JintNot27.19 ms2,812.501 343.75-16,374.58 Kb
    Yes15.54 ms1,296.88640.6331.257,521.49 Kb
    JurassicNot455.70 ms2,000.001,000.00-15,575.28 Kb
    Yes78.70 ms1,000.00--7,892.94 Kb
    MSIE in Chakra mode IE JsRTNot30.97 ms---77.75 Kb
    Yes24.40 ms---90.58 Kb
    MSIE in Chakra Edge JsRT modeNot33.14 ms---78.40 Kb
    Yes32.86 ms---95.48 Kb
    V8Not41.10 ms---79.33 Kb
    Yes39.25 ms---96.17 Kb

    We get the biggest gain in percentage terms for engines written in pure .NET - the speed of the script execution on Jurassic grows 5.79 times, and on Jint 1.75 times. When these engines work, the amount of allocated memory is reduced by 2 times, and accordingly the number of garbage collection operations is reduced by more than 2 times. By reducing the number of garbage collection operations, we get a partial increase in the speed of the script. The main reason for the speed increase in Jurassic is also the cause of his worst result in this test: Jurassic always compiles the JS code into the IL code with Reflection.Emit, and then executes it. It is at the compilation stage that the main loss of performance occurs, and since during the precompilation this happens only once, we get a performance gain. Jint, on the contrary, is an interpreter and, as a result of preliminary compilation, returns a .NET object representing an abstract syntax tree. In the case of Jint, we save a resource due to the fact that we produce only one syntactic parsing operation and store one ASD in memory. According to the results of this test, you might think that Jint is the fastest engine, but this is not so, because as the amount of executed code increases, its performance will fall. In general, Jint and Jurassic show the worst results when processing large amounts of code. According to the results of this test, you might think that Jint is the fastest engine, but this is not so, because as the amount of executed code increases, its performance will fall. In general, Jint and Jurassic show the worst results when processing large amounts of code. According to the results of this test, you might think that Jint is the fastest engine, but this is not so, because as the amount of executed code increases, its performance will fall. In general, Jint and Jurassic show the worst results when processing large amounts of code.


    The best result among the wrappers over engines written in C ++ is shown by MSIE in Chakra IE JsRT mode - the execution speed increases by 26.93%. After it comes ChakraCore (18.96%), then V8 (4.71%) and MSIE in the Chakra Edge JsRT mode shows the worst result (0.85%). For me, it still remains a mystery why the engine from Internet Explorer turned out to be faster than the Edge engine. In general, such modest results for this type of engine can be explained as follows. We get the compiled script from the engine in a serialized form (as an array of bytes) and save it in managed memory. You may notice that in the last column of the table, due to the appearance of this array, the amount of allocated memory increased by 12–17 KB (in this test, we measure only managed memory). When the engine is initialized, the compiled script deserializes this array. Also add to this the cost of marshaling. Nevertheless, the performance gain is still noticeable.


    Running tests in the .NET Core 2.0 environment gives the following results (the V8 module is missing because the Microsoft ClearScript library on which it is based does not support .NET Core):


    Module NamePreliminary compilationThe average will continue. fulfillmentGen 0 per thousand operas.Gen 1 per thousand operas.Gen 2 per thousand operas.Memory allocation for one opera.
    ChakraCoreNot43.65 ms---18.07 Kb
    Yes36.37 ms---16.59 Kb
    JintNot24.87 ms2 750,001 375.00-16 300.25 Kb
    Yes15.25 ms1,281.25593.7562.507 447.44 Kb
    JurassicNot469.97 ms2,000.001,000.00-15 511.70 Kb
    Yes80.72 ms1,000.00--7 845.98 Kb
    MSIE in Chakra mode IE JsRTNot31.50 ms---20.28 Kb
    Yes24.52 ms---18.78 Kb
    MSIE in Chakra Edge JsRT modeNot35.54 ms---20.45 Kb
    Yes31.44 ms---18.99 Kb

    In general, we obtained similar results. The only thing that catches your eye is the result of MSIE in Chakra Edge JsRT mode improved (7.69%). Also in this case, using precompilation with the Chakra family of engines reduces the consumption of managed memory.


    Ability to change the maximum stack size in ChakraCore and MSIE modules


    The stack size in engines developed by Microsoft is limited by the size of the thread stack in which the engine runs. Since in modern versions of IIS it is rather small (256 Kbytes for the 32-bit version and 512 Kbytes for the 64-bit version), when running in ASP.NET large JS libraries (for example, TypeScript compiler), the stack overflow occurs. This problem has long been solved in JavaScript Engine Switcher by creating a separate thread for the execution of these engines. Previously, when creating such threads, the stack size was hard-coded in the source code, and coincided with the maximum stack size in Node.js (492 Kbytes for a 32-bit process and 984 Kbytes for a 64-bit process). Over time, it turned out that not everyone lacks this size, and on the contrary, someone wants to reduce it.MaxStackSizewith which you can set the size of the stack in bytes. The default is the same Node.js value. If you assign a value of zero to this property, then the value from the header of the executable file will be used as the maximum stack size.


    New NiL.JS based module


    In the third version, a new module-adapter NiL , which is based on the engine NiL.JS, appeared . NiL.JS is another JS engine written on pure .NET. His first version was released in 2014, but he did not get such popularity as Jurassic and Jint. Its main advantage is performance. As an example, the results of the same benchmark that was used in the section on preliminary compilation can be cited.


    When running in the .NET Framework 4.7.2, we get the following results:


    Module NameThe average will continue. fulfillmentGen 0 per thousand operas.Gen 1 per thousand operas.Gen 2 per thousand operas.Memory allocation for one opera.
    Jint27.19 ms2,812.501 343.75-16,374.58 Kb
    Jurassic455.70 ms2,000.001,000.00-15,575.28 Kb
    NiL17.80 ms1,000.00--4 424,09 Кб

    The launch results in the .NET Core 2.0 environment differ slightly:


    Module NameThe average will continue. fulfillmentGen 0 per thousand operas.Gen 1 per thousand operas.Gen 2 per thousand operas.Memory allocation for one opera.
    Jint24.87 ms2 750,001 375.00-16 300.25 Kb
    Jurassic469.97 ms2,000.001,000.00-15 511.70 Kb
    NiL19.67 ms1,000.00--4,419.95 Kb

    The results of these tests can not even comment. Such impressive results were obtained thanks to non-standard solutions that were used by the author (you can learn the details from his articles on Habré ). It is also worth noting that the authors of Jint also did not sit idly by and since 2016 they have been working on the third version of their engine, one of whose tasks is to increase productivity. If you believe the benchmark for version 3.0.0 Beta 1353 , then the preliminary version of Jint runs scripts 2.4 times faster than NiL.JS version 2.5.1200, and the memory utilization rates of them are almost the same.


    There are NiL.JS and disadvantages. Everything goes well while we run some simple code on it, but when we try to run some popular libraries, errors start to occur. Of all the Bundle Transformer modules using the JavaScript Engine Switcher, I managed to run only Hogan and Handlebars, the same situation with ReactJS.NET. In the near future I am going to document all these errors and pass them on to the author NiL.JS.


    Links


    1. JavaScript Engine Switcher project page on GitHub
    2. JavaScript Engine Switcher Documentation
    3. Translation of my post “Misconceptions about JavaScript Engine Switcher 2.X”
    4. JSPool Project Page on GitHub
    5. ReactJS.NET project website
    6. ReactJS.NET project page on GitHub


    Also popular now: