General Discussion on the future of Spidermonkey in Cocos2d-js

I would like to start a discussion on the long term architecture around the use of Spidermonkey and JSB (Javascript Bindings) in cocos2d-js. As development on cocos creator continues, I believe that there is high value to making sure that the code around Spidermonkey and the binding layer is well understood and documented by the community. Before I get into that, I would like to share a thank you to @pandamicro, and the entire cocos2d-x team for their hard work. I think that contributing to the Spidermonkey parts of the engine can be intimidating, due to the complexity of embedding a complex VM. However, now more then ever, I believe that Spidermonkey is a mission critical part of the future success of cocos2d-x, and we need to have a community wide discussion on it’s future.

This point of this post is to outline my findings that I’ve found over the years developing in cocos2d-js. I would like to define a series of RFCs for road map items for improving the use of Spidermonkey in cocos2d-js. I would like to see what the community thinks of these topics before posting any detailed RFCs on these efforts. If the community can agree on these topics, I can post detailed RFCs that will defined specific courses of action.

Benefits of acting on these RFCs include:

  • Potentially better performance.
  • More approachable code for better community maintenance/feedback.
  • An upgrade path that will make it easier to keep up with latest Spidermonkey updates.
  • A more stable memory management system when using CC_USE_GC_FOR_NATIVE
  • To open the door for potentially adding web worker support in the future, for performance gains in JS based computations.

Items to discuss for potential RFCs (think of this as a pre-RFC):

  • Define a HAL (Highlevel Abstraction Layer) over all javascript bindings. This will allow us to easily upgrade spidermonkey/use different JS VM’s on different platforms.
  • Remove the proxy hashmap, and instead use JS_PRIVATE/other API alternatives.
  • Redefine how memory is managed with CC_USE_GC_FOR_NATIVE. This will involve tracing large c++ allocations in the JS Heap, in order for Spidermonkey to get a better idea of the current pressure on the GC.

Define a HAL (High-level Abstraction Layer) over all javascript bindings. This will allow us to easily upgrade spidermonkey/use different JS VM’s on different platforms.

In the past, (https://github.com/cocos2d/cocos2d-x/issues/16387), I have discussed the idea of upgrading Spidermonkey with the cocos team. Cocos2d-x is currently on v34 of Spidermonkey, while the latest stable release is v45. Our current version of Spidermonkey lacks native ES6 support, and is missing strong improvements made over the past several years.

One of the major pain points in upgrading Spidermonkey is that the cocos2d-x team has to maintain several different sets of bindings, targeting different Spidermonkey defined C++ APIs. While the binding-generator project makes this task easier, it is still a difficult and time consuming process. I would like to lead a community wide discussion on what such a layer would look like from an API perspective.

Remove the proxy hashmap, and instead use JS_PRIVATE/other API alternatives.

Overall, binding calls are slightly more expensive then they should be. Part of this is because of the proxy hashmap. For those that don’t know, calling a c++ defined method from JS calls a binding, that looks like this.

bool js_cocos2dx_AtlasNode_getTexture(JSContext *cx, uint32_t argc, jsval *vp)
{
    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
    JS::RootedObject obj(cx, args.thisv().toObjectOrNull());
    js_proxy_t *proxy = jsb_get_js_proxy(obj);
    cocos2d::AtlasNode* cobj = (cocos2d::AtlasNode *)(proxy ? proxy->ptr : NULL);
    JSB_PRECONDITION2( cobj, cx, false, "js_cocos2dx_AtlasNode_getTexture : Invalid Native Object");
    if (argc == 0) {
        cocos2d::Texture2D* ret = cobj->getTexture();
        JS::RootedValue jsret(cx);
        if (ret) {
            jsret = OBJECT_TO_JSVAL(js_get_or_create_jsobject<cocos2d::Texture2D>(cx, (cocos2d::Texture2D*)ret));
        } else {
            jsret = JSVAL_NULL;
        };
        args.rval().set(jsret);
        return true;
    }

    JS_ReportError(cx, "js_cocos2dx_AtlasNode_getTexture : wrong number of arguments: %d, was expecting %d", argc, 0);
    return false;
}

That call to jsb_get_js_proxy does a lookup in a std::unordered_map. std::unordered_map has known performance flaws, and is very cache-unfriendly. In addition to this, maintaining such a large hashmap over the course of the app has negative impacts to memory.

Mozilla provides different abstractions to handle this problem. One of them is the ability to set ‘private’ data in a JSObject. This ‘private’ data is a void* contained in the JSObject itself. Instead of performing a hashmap lookup when calling a binding, we instead can just pull from the private data. Private data is currently used in XMLHttpRequest to side-step the proxy lookup in most cases. I believe that this can be expanded to include most Cocos2d-x objects. I believe that the native -> JSObject hashmap (the reverse of the map I described above) will still need to be maintained.

Redefine how memory is managed with CC_USE_GC_FOR_NATIVE. This will involve tracing large c++ allocations in the JS Heap, in order for Spidermonkey to get a better idea of the current pressure on the GC.

This is coming off the back of the great work that @pandamicro and @ricardo ave been doing. Before CC_USE_GC_FOR_NATIVE, programmers would have to manually use reference counting. This had the potential to lead to leaks if your application/game code was not structured correctly.

Now we can use the Javascript garbage collector to manage all this for us. This is awesome, but in my application code, while transitioning between scenes, I end up having a small peroid where my old scene is unused, but the GC hasn’t fired yet. This means that I potentially have my old scene and my new scene in memory until the GC fires. I have solved this problem by focing GCs in critical parts of my loading flow, but there is a potential better solution.

One of the ways that Servo (mozilla’s browser written is Rust) gets around this problem, is by having Spidermonkey manage it’s allocation. Meaning that Heap allocations are managed by Spidermonkey, via hooks to custom alloc/free methods. https://blog.mozilla.org/research/2014/08/26/javascript-servos-only-garbage-collector/ is a great post about how mozilla tackled this problem.

If you read all of that, you have my thanks! I am open to any feedback, and if you have any findings that I did not mention, or if you have a different perspective on the topic, I welcome any discussion.

4 Likes