Skip to content

Performance, Caching and Concurrency

Oleg Shilo edited this page Jun 8, 2023 · 17 revisions

C# code executed with CS-Script can be extremely performant. However certain execution scenarios can lead to situations when the benefits of compiled execution, offered by CS-Script, are diminished.

The next two sections explain the internals of the script execution and also provide guidance on how to choose the best most optimized and most maintainable execution model.

A rule of thumb is "Rely on Caching and InMemoryAssembly loading".

Caching

The runtime performance of the C# scripting solution is identical to the compiled application. The truly impressive execution speed is attributed to the fact that the script at runtime is just an ordinary CLR assembly, which just happens to be compiled on-fly.

However, the compilation itself is a potential performance hit. Unfortunately, the compilation is a heavy task; initialization of the engine can bring a significant startup overhead. It is the case for both CLI and hosted script execution. And this is when caching comes to the rescue.

Hosted execution

Caching in hosted execution is conceptually similar to CLI caching but rather simplistic. No cache is persisted between scripting sessions. Thus, caching scope is limited to the hosting process. Thus, if the script code (text) has been already loaded/executed in the current process then the script assembly from the previous execution is reused instead of compiling a new one.

Caching is controlled via IsCachingEnabled property with any of Load* or Compile* methods:

CSScript.Evaluator
        .With(eval => eval.IsCachingEnabled = true)
        .LoadMethod(code);

Note that caching has some limitations. Thus, the algorithm for checking if the script is changed since the last execution is limited to verifying the script code (text) only. Thus, it needs to be used with caution. That's why caching (in hosted scenarios) is disabled by default.

CLI

With caching a given script file is never recompiled unless it's changed since the last execution. This concept is extremely similar to the Python caching model.

CLI execution caching is enabled by default. It is controlled via -c switch.

However in case of very intense sometimes instead of disabling caching completely, it may be more beneficial to perform fine caching control. These points will help you to understand some caching internals:

  • When a script file is compiled CS-Script always creates an assembly file, which is placed in the dedicated directory (cache) for further loading/execution.

  • The compiled assembly file is always created. Regardless if the caching is enabled or not. Enabling caching simply enables using the previous compilation result (assembly) if it is still up to date.

  • Location of the compiled script is deterministic and can be discovered by right-clicking the script file in explorer and selecting the corresponding option from the context menu. Alternatively, the cached script location can be deducted programmatically from the script file location:

    CSScript.GetCachedScriptPath("script full path");

    A typically all cache data is placed in %temp%\CSSCRIPT\Cache\<scriptDirHash>.

  • The cache directories also contain some extra temporary files that are needed for injecting script specific metadata into the script assembly. This metadata is used to allow script reflecting itself: Script Reflection.

  • Cache directory doesn't grow endlessly and it is of a fixed size. Any temporary files that are no longer needed are always removed on script host exit event. Purging non-temporary but cached compiled scripts (e.g. if the source scripts do not exist anymore) can be done by executing css cache -trim command.
    The script execution footprint on your system does not depend on the number of script executions but rather on the number of unique scripts present on the system and ever been executed. If it is detected that your cache is constantly growing then it needs to be reported as a defect so it can be fixed.

  • Caching is not an obstruction but a help. There were a few reports about cached files being locked by the executing process leading to compiler error "Access to ...cs.compiled file is denied". This sort of problems is always caused by another process changing the script file and truing to compile while the script assembly is still loaded for execution. This scenario is rare but not entirely unusual. It's important to understand that it is a logical problem not a technical one. And while disabling caching will prevent locking it is a very heavy price to be paid and it doesn't address the problem directly.
    The more practical and very reliable approach is to keep caching enabled but allow loading assembly as in-memory byte stream, instead of the file, leaving the compiled file completely unlocked: (details are in the Concurrency section).

Concurrency

Any script execution may be a subject to some sort of synchronization in concurrent execution scenarios.

Note: synchronization (concurrency control) may only be required for execution of the same script by two or more competing processes. If one process executes script_a.cs and another one executes script_b.cs then there is no need for any synchronization as the script files are different and their executions do not collide with each other.

Hosted script code execution
In case of hosted execution very often it is a script code (in-memory string) that is executed not the file. Thus, there is no any competition for the same resources (e.g. script file) by concurrent executions. Meaning that there is no need for any concurrency control.

Hosted script file execution
In this case there is a common resource (script file). Thus, script engine needs to synchronize the access to this resource with the other concurrent executions if any.

Standalone script file execution
In this case the execution is also based on the shared resource (script file) and concurrency control is applicable.

The most critical stage of script execution is "Compilation" and it typically needs to be atomic and synchronized system wide. During this stage script engine compiles the script into assembly and any attempts to compile the same assembly at the same time will lead the error of the underlying compiler engine caused by the file locking (unless compilation attempts are synchronized).

In order to avoid this CS-Script uses global synchronization objects, which are used to by the competing engine instances for detecting when it is OK to do the compilation. Simply put, the script engine says "I am busy compiling this script. If you want to compile it too, wait until I am done.". Using caching (section above) dramatically decreases any possibility for an access collision by avoiding unnecessary compilations.

The concurrency model is controlled by the ConcurrencyControl configuration setting (see -config), which is set to Standard by default.

  • Standard: Simple model. The script engine delays the start of the timestamp validation and the script compilation until another engine validating finishes its job (or times out). Note: the compilation may be skipped if caching is enabled and the validation reviles that the previous compilation (cache) is still up to date. Due to the limited choices with the system-wide named synchronization objects on Linux Standard is the only available synchronization model on Linux. Though it just happens to be a good default choice for Windows as well.
  • None: No concurrency control is done by the script engine. All synchronization is the responsibility of the hosting environment.

Another stage of script execution is invoking the script entry point. This stage cannot and should not be synchronized as it is reflecting the business logic it may require the script to be loaded (active) indefinitely. This, in turn, can lead to the undesired locking of the compiled script until its execution is complete and the assembly file is released. This unpleasant practical implication(s) can be fully addressed by forcing script engine to load the compiled script not as a file but rather as its in-memory image - InMemoryAssembly mode. It can be enabled with the -config switch:

Getting InMemoryAssembly value

PS C:\Users\user>css -config:get:inmemoryassembly
InMemoryAssembly: True

Setting InMemoryAssembly value

PS C:\Users\user> sudo css -config:set:inmemoryassembly=true
set: InMemoryAssembly: True

InMemoryAssembly is an enormously convenient mode that solves many concurrency problems in a very elegant way. Particularly because ConcurrencyControl is not as reliable a mechanism as needed.

Limitations

ConcurrencyControl.Standard mode has a wait timeout of only 3 seconds. Why the default timeout is not infinite (-1) but 3 seconds? Achieving a reliable release of the system-wide synch object compilingFileLock was and still is problematic:

  • Win and Linux have different implementations for their mutex equivalents.
  • .NET Framework and Mono did not offer a consistent implementation either.
  • What is even more challenging is that the actual compilers csc.exe/mono.exe had the tendency to hang in memory even after successful compilation of the assembly. .NET Core is even more guilty of this. IE not terminating the forked dotnet.exe process in case of a compilation error.

All this led to the frequent cases of the script engine waiting endlessly for no reason. Thus the ugly but more practical solution was to wait very conservatively for 3 seconds and then let cs-script proceed and either succeed (there is no active compiling at the time) or controllably fail (another compilation is still in progress).

Thus, unfortunately, the initial proper implementation of ConcurrencyControl had to be diluted over time to the less reliable but more pragmatic level. Meaning that the only way to implement a deterministic concurrency control is to do it from the script engine host process (shell or app).

What you can do in the situation. You can ensure that you are using in-memory assembly (InMemoryAssembly: True) and/or set the timeout to any other value. It might help if in your environment you do not experience those mutex-release problems.

Setting custom timeout will be available v4.7.2+. A custom timeout can be achieved by setting set environment variable CSSCRIPT_CONCURRENCY_TIMEOUT to -1 (or any other integer value).