A possible solution for JSB memory model (for solving "invalid native object")

这种新的思路是这样的
1:绑定script对象的时候nativeObject reference count +1,scriptObject添加到GC root。
2:在nativeObject release到1的时候发出解绑事件,将scriptObject从GC root中移除。
3:在nativeObject retain到2的时候发出重新绑定事件:将scriptObject重新添加到GC root中。
3:scriptObject被GC的时候release nativeObject。
整个过程可以保证scriptObject和nativeObject都能相互持有正确的引用,在任何时候只要一个对象还存活,另外一个对象必然不会被释放。
另外Huabin Ling提到的在JSB中无法监听由js继承类型所构造jsObject对象的释放问题可以通过在jsObject添加一个隐藏并且不被别人引用的属性,该属性的类型可以是我们指定的JSClass,那样我们可以比较方便的监听jsObject的GC。

1 Like

I’ll help to translate,
@cocos_cyg have proposed a possible solution for JSB memory model.

The background is that developers encounter often “Invalid native object” error, the basic reason is that the native object is being released while its JS object is always available. In the current implementation of JSB, native objects rely on Cocos2d-x’s reference count system, JS objects rely on SpiderMonkey’s garbage collection. There is no systematic synchronization between these two life cycle.

So here is the proposition:

  1. When JS code create a engine object, the nativeObject will be created also, and we can retain it to increase the reference count by 1. The JS object should be added to GC root to avoid being garbage collected

  2. When native object’s reference count reduce to 1, we consider that only JS object still hold its reference, then we remove the JS object from GC root to let the garbage collection system manage it.

  3. When the JS object is collected, its finalize hook function will release the native object so that the native object is released.

This mechanism effectively synchronized the object’s native and js life cycle.

Two problems related :

  • After the native object’s reference count reduced to 1, it can be re-added to the scene graph. At this moment, we should guarantee the JS object shouldn’t be released.
    Solution: We can re-add the JS object to GC root when the native object’s reference count increased from 1 to 2.

  • I found a problem related to JS inheritance: the child class’ finalize function will neither be inherited nor be invoked when a JS object being GC.
    Solution: Add a hidden delegate object in each JS object, which is only referenced by this parent object. Then when the JS object get GC, its hidden delegate will be released and its finalize hook will be invoked, its finalize hook function can invoke the release function of its parent object.

I find this idea very interesting and innovating ! Thanks @cocos_cyg

How is the object added the GC root ? In the cocos2d-x native code ? or the JS bindings code ?
Could you describe the new algorithm in more detail, if possible with some pseudo-code ?

Thanks!

JS bindings code is easy to add JSObject instance to GC root

it is added GC root now at version Cocos2d-JS-v3.0 RC1
a code block of jsb_cocos2dx_auto.cpp:

static bool js_cocos2d_Node_ctor(JSContext *cx, uint32_t argc, jsval *vp)
    {
        jsval *argv = JS_ARGV(cx, vp);
    	JSObject *obj = JS_THIS_OBJECT(cx, vp);
        cocos2d::Node *nobj = new cocos2d::Node();
        if (nobj) {
            nobj->autorelease();
        }
        js_proxy_t* p = jsb_new_proxy(nobj, obj);
        JS_AddNamedObjectRoot(cx, &p->obj, "cocos2d::Node");
        bool isFound = false;
        if (JS_HasProperty(cx, obj, "_ctor", &isFound))
            ScriptingCore::getInstance()->executeFunctionWithOwner(OBJECT_TO_JSVAL(obj), "_ctor", argc, argv);
        JS_SET_RVAL(cx, vp, JSVAL_VOID);
        return true;
    }

Thanks. Could you also describe when and who destroys the object ? thanks.

Actually, in this design, the JS object is always managed by SpiderMonkey’s GC system, JS_AddNamedObjectRoot will add the JS object to the root to prevent it being garbage collected. And removed from the root could let GC collect the JS object when necessary.

The moment that the JS object is removing from the root should be the moment when the native reference count reduce to 1. Because during the creation of the JS object and the native object, the native object should be retained to simulate a reference from JS object, so when the ref count reduce to 1, it means that only the JS object is referencing the native object, all native reference have been released. So it’s safe to collect the JS object at any moment, that’s why we remove it from the root at this moment.

Hope I’m clear about this

like @pandamicro say " There is no systematic synchronization between these two life cycle.", so this solution first step is let they reference each othe, and then the point step is find a moment to release this reference.

Thanks @pandamicro detailed translation my article!
And i’m very sorry for my poor english…

Hi,

Sorry, I’m still trying to understand the original idea. This is what I understood so far:

Creating an object

  1. A Sprite is created in JS, let’s say using var sprite = new Sprite("hello.png")
  2. JSB code is invoked, and it creates Sprite C++ object. It also adds the the proxied object in the root.

So, that part is easy to understand.
But, how does the “destroying the object” work ?
Who removes the object from the root ? Could you describe that in detail ?

Thanks.

Let’s make a scene like this:
1: ver sprite = new Sprite(“hello.png”);
2: scene.addChind(sprite);

101: scene.removeChild(sprite);
102: log(“script x:”+sprite.getPositionX());
103: sprite = null;
104: forceGC();

1: create a script Sprite object and a native Sprite,add script Sprite to GC root and retain native Sprite reference count to 1;
2:add sprite to current scene,native Script will retain a reference for current scene object

101:scene will release native Sprite and it’s reference is only script Sprite(it will delete at current version), and then remove script Sprite form GC root, if not it will never release.
102:it can still get native Sprite property(it will lost native object at current version and report “invalid native object”)
103:clear script reference
104:call script system GC,script Sprite will be released and it’s finalize hook will call. at the finalize callback we can release the native Sprite.

Thanks.

in 2:, what do you mean by “native Script will retain a ref” ?
Who is going to retain the reference ? Our JSB code ? The CCNode.cpp code ? someone else ?

Similar to 101:, who is going to remove the script from GC root ? Our JSB code ? The CCNode.cpp code ? Someone else ?

Thanks.

line 2: native reference counting system will add a ref,addChind will add the native Sprite into the scene children vector ,then the vector’s pushBack function will retain a ref.

void Vector::pushBack(T object)
{
    CCASSERT(object != nullptr, "The object should not be nullptr");
    _data.push_back( object );
    object->retain();
}

line 101:add code to cocos::Ref::release function like it:

void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    --_referenceCount;
    if(referenceCount==1){
           // find script proxy object
           // if exits proxy object remove it from GC root
    }
   ......
}

in line 101:a good design is like it:
When the Ref release from 2 to 1, fire a event to JSB module,or lua binding module.

void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    --_referenceCount;
    if(referenceCount==1){
          if(_ID||_luaID)
                ScriptEngineManager::getInstance()->getScriptEngine()->handleReleaseScriptObject(this);
    }
   ......
}

At handleReleaseScriptObject:

void handleReleaseScriptObject(Ref* ref){
       js_proxy_t * p = jsb_get_native_proxy(node);
       if(p)  JS_RemoveObjectRoot(cx, &p->obj);
}

Thanks. I think your solution works, but I found an issue.

Scenario A:

var sprite = new Sprite("hello.png");
// An autoreleased sprite is created in C++. The refcount will be 1
// a proxy object is created in JSB
// the proxy object is added into the root

Issues:

  • The autoreleased sprite has a ref==1. When the autorelease pool is flushed, it will have a ref==0, so the C++ sprite will be released.

Possible solution:

  • JSB should retain the autoreleased sprite.

Also, in order to be symmetric, if Ref::release() removes the object from the root, then Ref::retain() or Ref::Ref() should add the object into the root.

Perhaps some other changes are needed, but I think you idea should work. Thanks!

This issues may not exist!

Well each cocos2d::Ref instance have an original ref when it was created. this is the code block of version Cocos2d-JS-v3.0 RC1 CCRef.cpp

Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1
{
#if CC_ENABLE_SCRIPT_BINDING
    static unsigned int uObjectCount = 0;
    _luaID = 0;
    _ID = ++uObjectCount;
#endif
    
#if CC_USE_MEM_LEAK_DETECTION
    trackRef(this);
#endif
}

 So when script Sprite was created the native Sprite' ref  has been retain to 2. And on the autorelease pool flush,it's ref will release from 2 to 1, like line 101. 

if the issue doesn’t exist, better yet :slight_smile:

it’s running in my game now :wink:

well done :smile:
could you send a PR ?

@pandamicro Could yo please review @cocos_cyg PR ? We need to add a lot of tests, including performance tests. This could be a great feature for v3.1.

thanks.

Hi, Riq,

I know about the solution of @cocos_cyg, it’s based on version 2.x, but it’s not a problem, I know how to implement them in 3.0. And I agree that it’s a very important feature.

The problem is the roadmap for 3.1, we don’t have enough time to do it before 21 October, because it’s a fundamental feature and as you said, it needs a lot of tests and performance comparison. Along with all the tests we need to do for the new renderer, I believe it’s not a realistic plan.

Can we put it in the 3.2 roadmap and consider it as a core feature of 3.2 ?

@pandamicro

You are right that there might not be enough time to have it ready for v3.1. But that doesn’t mean that we have to include it for v3.1. The feature will be merged into the stable branch once it is ready.
Basically, if we are going to do something, we have to do it great: great design, great architecture, great implementation, great quality, full of performance tests, coverage tests, unit tests, etc.
But that is true for all the features that we are developing: It is true for the new renderer, it is true for the asset manager, etc.

What I’m trying to say: We have to start working on this feature sooner than later. If it is not ready for v3.1, then it will be merged in v3.2. If it is not ready for v3.2, then in will be merged for v3.3. And we only merge it if its quality is great.
So, keep working on your current plan. Make sure that all the features that are planned for v3.1, are merged only if they have great quality. If not, let’s merge them for v3.2, etc…
Let’s add this feature for v3.2… but if the team has “idle” time, they can start working on this feature earlier.

I totally agree with you. No problem, I will start it as soon as possible. And I will keep you informed @cocos_cyg