microsoft / ClearScript

A library for adding scripting to .NET applications. Supports V8 (Windows, Linux, macOS) and JScript/VBScript (Windows).
https://microsoft.github.io/ClearScript/
MIT License
1.79k stars 148 forks source link

latency when passing value from js back to .net #604

Closed wysisoft closed 2 months ago

wysisoft commented 2 months ago

I am seeing 20-60 ms spent on passing a value from js to .net, is there any way to make this faster? Only asking because this is the same latency I'd see from using grpc between .net and nodejs.

image

ClearScriptLib commented 2 months ago

Hi @wysisoft,

There are a few issues here:

  1. At least some of the latency in your example is due to the cost of custom DateTime formatting. You could use Stopwatch for more exact timing.
  2. The dynamic infrastructure (engaged via engine.Script in the second call) incurs heavy overhead on the first call but uses inline caching to accelerate subsequent calls from the same site.
  3. Timing a single call gives you a skewed performance picture, as it doesn't give the runtime a chance to do dynamic optimization.

We can remedy all these issues. Here's a function that performs precise timing for any number of iterations:

static IEnumerable<double> GetTimes<T>(Func<T, object> action, T arg, int count) {
    for (var index = 0; index < count; index++) {
        var sw = Stopwatch.StartNew();
        action(arg);
        sw.Stop();
        yield return sw.Elapsed.TotalMilliseconds;
    }
}

We can use this function to time several ways of invoking asdf:

const int iterationCount = 1;

Func<ScriptEngine, object> invokeDynamic = static engine => engine.Script.asdf();
Console.WriteLine("Dynamic: {0:F6} ms", GetTimes(invokeDynamic, engine, iterationCount).Average());

Func<ScriptEngine, object> invokeByName = static engine => engine.Invoke("asdf");
Console.WriteLine("ByName: {0:F6} ms", GetTimes(invokeByName, engine, iterationCount).Average());

Func<ScriptObject, object> invokeAsFunction = static asdf => asdf.InvokeAsFunction();
Console.WriteLine("AsFunction: {0:F6} ms", GetTimes(invokeAsFunction, (ScriptObject)engine.Script.asdf, iterationCount).Average());

With the iteration count at 1, here's the output from our test machine. Note the long latency for the initial dynamic call:

Dynamic: 46.966600 ms
ByName: 0.501400 ms
AsFunction: 0.708200 ms

By raising the iteration count to 1000, we can let inline caching and dynamic optimization do their jobs:

Dynamic: 0.055599 ms
ByName: 0.006486 ms
AsFunction: 0.007880 ms

Good luck!

wysisoft commented 2 months ago

This is great, thank you