kossmak / pyv8

Automatically exported from code.google.com/p/pyv8
0 stars 0 forks source link

python object is not garbage collected when it's in a javascript closure #186

Open GoogleCodeExporter opened 8 years ago

GoogleCodeExporter commented 8 years ago
What steps will reproduce the problem?

Run the following code (also attached):

import gc
import sys
import inspect
import weakref

import PyV8

class PyObj(object):
    def __init__(self):
        pass

class Global(object):
    def Obj(self):
        return PyObj()

    def out(self, v):
        print "-- Script out: %s --" % (v,)

tracks = []
def obj_gone(ref):
    print "**Obj deleted**"
def track_obj(obj):
    tracks.append(weakref.ref(obj, obj_gone))

def run_case(name, js):
    print "Case %s:" % (name,)
    ctxt = PyV8.JSContext(Global())
    with ctxt:
        obj = ctxt.eval(js)
        track_obj(obj)

        print "refcount:", sys.getrefcount(obj)
        print "V8 theObj=null"
        ctxt.eval("theObj = null;")
        print "Py obj=None"
        obj = None
        print "V8 gc"
        PyV8.JSEngine.collect()
        print "Py gc"
        gc.collect()
        print "Py gc.garbage:", gc.garbage
    print

gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE |
             gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS)

run_case("one", """
    function tighty() {
        var localObj = Obj();
        localObj.foobar = function () {
            out("hi" + this);
        };
        return localObj;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj;""")

run_case("two", """
    function tighty() {
        var localObj = Obj();
        localObj.foobar = function () {
            out("hi" + localObj);
        };
        return localObj;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj;""")

What is the expected output? What do you see instead?

I expect the object to get deleted in both cases, that is:

-- Script out: hi[object Object] --
refcount: 3
V8 theObj=null
Py obj=None
V8 gc
**Obj deleted**
Py gc
Py gc.garbage: []

However, in the second case, the object is never deleted:

Case two:
-- Script out: hi[object Object] --
refcount: 3
V8 theObj=null
Py obj=None
V8 gc
Py gc
Py gc.garbage: []

What version of the product are you using? On what operating system?

Python 2.6.6 with PyV8-1.0-preview-r443.win32-py2.6.exe on Windows 7

Please provide any additional information below.

Seems to be an issue with how V8's garbage collector interacts w/ Python 
objects within closures... it seems to not release its reference counts even 
though it's no longer accessible from anywhere. 

Original issue reported on code.google.com by csaft...@gmail.com on 17 Jul 2013 at 7:29

Attachments:

GoogleCodeExporter commented 8 years ago
The following case gets the object to delete (manually kill the ref):

    function makeClosure(forObj, f) {
        var res = function () {
            f(forObj);
        };
        res.wipe = function () {
            forObj = null;
        };
        return res;
    }

    function tighty() {
        var localObj = Obj();

        var clos = makeClosure(localObj, function (obj) {
            out("hi" + obj.x);
        });

        localObj.foobar = clos;

        return localObj;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj.foobar.wipe();
    theObj;

Original comment by csaft...@gmail.com on 17 Jul 2013 at 7:40

GoogleCodeExporter commented 8 years ago
This work-around works:

    var x = 9999;
    function tighty() {
        var localObj = Obj();

        localObj.foobar = safeBind(localObj, function (localObj) {
            out("hi" + localObj.x);
        });

        return localObj;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj;

given the following classes:

class SafelyBound(object):
    """Apparently python gc likes this more than lambdas."""
    def __init__(self, py_obj, js_func):
        self.py_obj = py_obj
        self.js_func = js_func

    def __call__(self, *args):
        self.js_func(self.py_obj, *args)

class PyObj(object):
    def __init__(self):
        self.x = 100

class Global(object):
    def Obj(self):
        return PyObj()

    def out(self, v):
        print "-- Script out: %s --" % (v,)

    def safeBind(self, py_obj, js_func):
        """Bind in Python instead of JS to work around pyv8 memory leak."""
        return SafelyBound(py_obj, js_func)

Interestingly, it *doesn't* work if safeBind is instead defined as a lambda:

    def safeBind(self, py_obj, js_func):
        """Bind in Python instead of JS to work around pyv8 memory leak."""
        return lambda *args: js_func(py_obj, *args)

Original comment by csaft...@gmail.com on 17 Jul 2013 at 8:24

GoogleCodeExporter commented 8 years ago
Garbage collection works properly if it's a javascript object that is 
containing the python object:

    var x = 9999;
    function tighty() {
        var jsObj = {};
        jsObj.pyObj = Obj();

        jsObj.foobar = function () {
            out("hi" + jsObj.pyObj.x);
        };

        return jsObj;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj;

Original comment by csaft...@gmail.com on 17 Jul 2013 at 9:24

GoogleCodeExporter commented 8 years ago
Update: GC also works if there are JS objects nested in JS Objects which end up 
in a python object. Given:

class PyObj(PyV8.JSClass):
    def __init__(self, name):
        self.name = name
        self.x = 100

    def __del__(self):
        print "PyObj(%s) __del__" % (self.name,)

class Global(object):
    def Obj(self, name):
        return PyObj(name)

    def out(self, v):
        print "-- Script out: %s --" % (v,)

Then for the following case:

run_case("deepnesting-ok", """
    var x = 9999;
    function tighty() {
        var this_ = {};

        this_.localObj = {};
        this_.localObj.bark = {};
        this_.localObj.bark.haha = {};
        this_.localObj.bark.haha.bazoonga = Obj("bazoonga");

        this_.foobar = function () {
            out("hi" + this_.localObj.bark.haha.bazoonga.x);
        };

        return this_;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj;
""")

'bazoonga' is collected:

Case deepnesting-ok:
-- Script out: hi100 --
refcount: 2
V8 theObj=null
Py obj=None
**Obj deleted**
V8 gc
PyObj(bazoonga) __del__
Py gc
Py gc.garbage: []

However, for the following case:

run_case("deepnesting-screwed", """
    var x = 9999;
    function tighty() {
        var this_ = {};

        this_.localObj = {};
        this_.localObj.bark = Obj("bark");
        this_.localObj.bark.haha = {};
        this_.localObj.bark.haha.bazoonga = Obj("bazoonga");

        this_.foobar = function () {
            out("hi" + this_.localObj.bark.haha.bazoonga.x);
        };

        return this_;
    }
    var theObj = tighty();
    theObj.foobar();
    theObj;
""")

'bark' is collected, but 'bazoonga' is not:

Case deepnesting-screwed:
-- Script out: hi100 --
refcount: 2
V8 theObj=null
Py obj=None
**Obj deleted**
V8 gc
PyObj(bark) __del__
Py gc
Py gc.garbage: []

Original comment by csaft...@gmail.com on 17 Jul 2013 at 10:10

GoogleCodeExporter commented 8 years ago
Hmm I just realized at least some of the above might have to do with toString() 
of JSClass being called & thus exhibiting issue 185.

Original comment by csaft...@gmail.com on 17 Jul 2013 at 10:21

GoogleCodeExporter commented 8 years ago
Seems it's ok to put py objects onto a js object, and py objects onto those py 
objects, but you can't have a Py object on a JS object on a Py object - it 
won't get GCed (attached whole file):

run_case("deepnesting-screwed", """ //FAIL
    var x = 9999;
    function tighty() {
        var this_ = {};

        var haha = Obj("Haha"); //collected
        haha.zomg = Obj("zomg?"); //collected

        this_.localObj = {};
        this_.localObj.bark = Obj("bark"); //collected
        this_.localObj.bark.bore = Obj("bore"); //collected
        this_.localObj.bark.bore.fork = Obj("fork"); //collected
        this_.localObj.bark.js = {};
        this_.localObj.bark.js.bazoonga = Obj("bazoonga"); //** not collected **

        return this_;
    }
    var theObj = tighty();
    theObj;
""")

Original comment by csaft...@gmail.com on 18 Jul 2013 at 3:36

Attachments:

GoogleCodeExporter commented 8 years ago
To summarize: The bug seems to be whenever you have a chain of Python -> JS -> 
Python objects. That is:

* JS -> Python: this is ok (e.g. python obj in a JS container, or as a local 
variable of a JS function, or it is referenced in a JS closure).
* JS -> Python -> JS: this *might be* ok (e.g. python obj is in a container or 
is a local var, and it contains JS objects). the python obj gets freed, so I 
assume the JS one gets garbage collected, but I have no way of checking.
* JS -> Python -> JS -> Python: this is *not ok*. (e.g. python obj is in a 
container os is a local var, and it contains a JS object that itself then 
contains a Python object, or a python object contains a closure which 
references another python object). In this case, the 2nd python object in the 
chain doesn't get GCd.

Hopefully these memory leak issues can be resolved - otherwise PyV8 has been 
quite great!

Original comment by csaft...@gmail.com on 18 Jul 2013 at 3:55