ScriptItem performance

Jul 12 at 1:59 PM
I noticed some performance issues with the usage of ScriptItem.
What I did was I had a constructor which received a script item as argument. And because ScriptItem has accessors set to internal, I had to use it as DynamicObject or dynamic.

What I wanted was to create a flat table of the object, just by getting the object keys as column name and their values as row values. The Properties are retrieved with the "GetDynamicMemberNames()" function, so no problems there. But for each of the properties I needed to create a Binder and get the value (see code below)

Now anybody can guess that this is not very fast. I tried to create a simple table with 3 rows and 13 columns. And it took a good +/- 1500 milliseconds on one run. I did the same with JSON.net JObject and because accessing properties isn't such a hassle their I got a performance of 0 milliseconds on one run. My temporary fix is that I call JSON.stringify before the constructor and in the constructor I uses JSON.NET his parser.

Anyway, my question or point of discussion: Isn't ScriptItem flawed by design? Why the use of dynamics? Take a look at how JSON.net encodes JS objects with strict types. In essence are all JS Objects KeyValuePairs where the Value types can vary.

Or am I doing something terrible wrong?
public void ParseDynamic(DynamicObject o) {
            
            var props = o.GetDynamicMemberNames();

            bool isArray = true;
            // try cast to int
            try {
                foreach (var k in props) { Int32.Parse(k); }
            } catch { isArray = false; }

            if (!isArray)
                AddRow(o);
            else {
                foreach (var k in props) { AddRow((DynamicObject)GetMember(o, k)); }
            }


        }

private void AddRow(DynamicObject obj) {
            var cols = obj.GetDynamicMemberNames();
            var row = new MiDataRow(this);
            rows.Add(row);

            // Add all columns
            foreach (var col in cols) {
                if (!columns.Contains(col)) {
                    columns.Add(col);
                    ResizeRows();
                }

                row[col] = GetMember(obj, col).ToString();
            }

        }

private static object GetMember(DynamicObject o, string propName) {
            var binder = Binder.GetMember(CSharpBinderFlags.None,
                  propName, o.GetType(),
                  new List<CSharpArgumentInfo>{
                               CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)});
            var callsite = CallSite<Func<CallSite, object, object>>.Create(binder);

            return callsite.Target(callsite, o);
        }
Coordinator
Jul 12 at 6:26 PM
Edited Jul 12 at 6:31 PM
Hi blackshade,

Why the use of dynamics?

.NET's dynamic infrastructure provides a convenient way to access script objects in a way that's both .NET-friendly and engine-neutral. A dictionary-style interface might also be convenient, but it's not clear what advantage it would have over the combination of GetDynamicMemberNames() and dynamic indexing.

The problem with your GetMember() method is that it recreates the binder and callsite each time. Try this instead:
private static object GetMember(object o, string propName) {
    return ((dynamic)o)[propName];
}
This implementation is simpler and takes advantage of inline caching to speed up repeated calls. In our experiments it's about 22x faster on average in Release builds.

Still, using JSON tunneling as you suggest makes sense in many cases. ClearScript provides live access to script objects, but if all you need is a snapshot, then JSON is a great way to minimize marshaling and improve performance.

Thanks for your question, and good luck!
Jul 12 at 6:32 PM
Thank you for the reply.
I will try and benchmark your suggestion and post the results below.
Jul 12 at 6:45 PM
Ok thanks!

It is still +/- 60 milliseconds (ScriptItem) against +/- 3 milliseconds (String -> Json.net) on the initial run. But after that the JIT has optimized both to nano seconds.
So thanks! These differences can be explained by the fact that on the first run the JIT will optimize a part of the code path the seconds also takes.

I will look more into it and do some better benchmarks to see which one is really faster. Still I expect that the String version is faster.
If that is the cast that really is such a shame.
But more on this later, thanks anyway!
Jul 12 at 8:32 PM
Ok I did some more benchmarking and I did the following, where rec is a flat json object.
var arr = [rec, rec, rec];
    var strJson = JSON.stringify(arr);
    new MiTable("form", arr);
    new MiTable("form", strJson);


    startTime();
    for (var i = 0; i < 1000; i++)
        new MiTable("form", strJson);
    stopTime();

    startTime();
    for (var i = 0; i < 1000; i++)
        new MiTable("form", arr);
    stopTime();
Both the strJson and the JSON function implement the same idea, but the strJson de-serialises the string, and still managed to be faster.
My point stays, isn't it better to not make ScriptItem dynamic. Because within Javascript all object can be of fixed types and all objects are key value pairs anyway. Or maybe is it possible to make use of JSON.net JObject instead of dynamic ScriptItem? Even translating the ScriptItem to JObject results in a higher benchmark than the above benchmark.

Anyway, thanks for your time and I hope you can do something with my findings.
Coordinator
Jul 13 at 5:41 PM
Hi blackshade,

The purpose of a script item in ClearScript is to allow .NET code to access an actual "live" script object, as opposed to a serialized copy of its data.

Yes, it's definitely faster to read data from a managed collection such as a Json.NET JObject or a standard .NET dictionary - and if reading a script object's properties quickly is what you need to do, then we encourage and recommend JSON tunneling. Hopping the boundary between the managed and script environments is expensive, and JSON lets you transfer an entire object's data in one hop.

However, this is very different from what script items do. Some examples:
  • Writing to a script item modifies the actual script object to which it's bound.
  • Unlike JSON, script items provide access to JavaScript properties that are not enumerable.
  • Script items invoke property getters and setters as necessary.
  • Script items that are functions can be invoked to execute script code.
  • Script items provide access to script objects that aren't JSON-friendly (e.g., VBScript objects).
Cheers!
Jul 13 at 5:49 PM
Hi,

Ahh I see, I thought you would only get a snapshot of the given script item. But if you would run the script async of the c# code, the changes in scriptitem would be live.

I didn't think about that. The structure of the ScriptItem makes a whole lot of more sense to me now.

Thank you again sir! I will stick to my JSON tunneling because that is what I need.
If I haven't said so: Nice work on this project, love it.
Coordinator
Jul 14 at 2:49 PM
Thanks for your feedback!