I am trying to track down a problem with leaking cairo_t context structs
when using cairo from python. I running under an up to date fedora 20 system.
(I suspect that other structs are leaking as well, but I have not deep dived
them yet).

The attached example program can be run under valgrind to show the leak.
The program takes a repeat count that you can use to tell the leak from the 
startup overhead.

        python gdk-cairo-leak.py 10

I think the bug is in the reference counting of the cairo_t struct.
After creation the ref_count is 2 that that means that it will never be freed 
as the python object will be deleted causing cairo_destroy to be called that 
will take the ref count down to 1 but not 0 required to free the storage.

Is there a mecahnism to mark the cairo_create and not needing an extra 
cairo_reference call?

I am happy to work up a patch if someone can give me some insight into how
the reference counting is supposed to work in the class of API.

The following GDB session should show the bug. I have determined the
cr->ref_count address and set a watch point. I have annotate the key
observations with *****.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /usr/bin/python gdk-cairo-leak-barry.py 1
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7fffe67e1700 (LWP 18064)]
cairo_create
Hardware watchpoint 12: *(int *)0x9d8c60

***** 0x9d8c60 is the cr->ref_count address

Old value = 0
New value = 1
_cairo_init (cr=cr@entry=0x9d8c60, backend=backend@entry=0x7fffea9758a0 
<_cairo_default_context_backend>) at cairo.c:247
247         cr->status = CAIRO_STATUS_SUCCESS;

***** cairo_create returns the cairo_t with a ref_count of 1.

(gdb) bt 10
#0  _cairo_init (cr=cr@entry=0x9d8c60, backend=backend@entry=0x7fffea9758a0 
<_cairo_default_context_backend>) at cairo.c:247
#1  0x00007fffea681d08 in _cairo_default_context_init (cr=cr@entry=0x9d8c60, 
target=0x9d8640) at cairo-default-context.c:1445
#2  0x00007fffea681d9d in _cairo_default_context_create (target=<optimized 
out>) at cairo-default-context.c:1468
#3  0x00007fffec9be383 in gdk_cairo_create (window=0x9b32b0) at 
gdkwindow.c:3180
#4  0x00007fffef2b7d8c in ffi_call_unix64 () at ../src/x86/unix64.S:76
#5  0x00007fffef2b76bc in ffi_call (cif=cif@entry=0x7fffffffc980, 
fn=fn@entry=0x7fffec9be310 <gdk_cairo_create>, 
rvalue=rvalue@entry=0x7fffffffc960, 
    avalue=avalue@entry=0x7fffffffc8a0) at ../src/x86/ffi64.c:522
#6  0x00007ffff01ade49 in g_callable_info_invoke (info=info@entry=0x9d55e0, 
function=0x7fffec9be310 <gdk_cairo_create>, in_args=in_args@entry=0x9b6c50, 
    n_in_args=n_in_args@entry=1, out_args=out_args@entry=0x0, 
n_out_args=n_out_args@entry=0, return_value=return_value@entry=0x7fffffffcb58, 
    is_method=is_method@entry=0, throws=0, error=error@entry=0x7fffffffcb08) 
at girepository/gicallableinfo.c:680
#7  0x00007ffff01af199 in g_function_info_invoke (info=info@entry=0x9d55e0, 
in_args=0x9b6c50, n_in_args=1, out_args=0x0, n_out_args=0, 
    return_value=return_value@entry=0x7fffffffcb58, 
error=error@entry=0x7fffffffcb08) at girepository/gifunctioninfo.c:274
#8  0x00007ffff03ebab7 in _invoke_callable (function_ptr=0x0, 
callable_info=0x9d55e0, cache=0x9b52a0, state=0x7fffffffcb10) at pygi-
invoke.c:64
#9  pygi_callable_info_invoke (info=<optimized out>, py_args=<optimized out>, 
kwargs=<optimized out>, cache=<optimized out>, function_ptr=<optimized out>, 
    user_data=<optimized out>) at pygi-invoke.c:652
(More stack frames follow...)
(gdb) c
Continuing.

Breakpoint 4, gdk_cairo_create (window=0x9b32b0) at gdkwindow.c:3182
3182      if (window->impl_window->paint_stack)
(gdb) c
Continuing.

Hardware watchpoint 12: *(int *)0x9d8c60

Old value = 1
New value = 2
cairo_reference (cr=cr@entry=0x9d8c60) at cairo.c:279
279     }
(gdb) bt 10
#0  cairo_reference (cr=cr@entry=0x9d8c60) at cairo.c:279
#1  0x00007fffe5ddebac in cairo_context_from_arg (interface_info=<optimized 
out>, data=0x9d8c60) at pygi-foreign-cairo.c:63
#2  0x00007ffff03f1d2c in _pygi_marshal_to_py_interface_struct_cache_adapter 
(state=<optimized out>, callable_cache=<optimized out>, 
    arg_cache=<optimized out>, arg=<optimized out>) at pygi-marshal-to-
py.c:752
#3  0x00007ffff03ebc1d in _invoke_marshal_out_args (cache=0x9b52a0, 
state=0x7fffffffcb10) at pygi-invoke.c:543
#4  pygi_callable_info_invoke (info=<optimized out>, py_args=<optimized out>, 
kwargs=<optimized out>, cache=<optimized out>, function_ptr=<optimized out>, 
    user_data=<optimized out>) at pygi-invoke.c:657
#5  0x00007ffff7a610d3 in PyObject_Call (func=func@entry=<gi.FunctionInfo at 
remote 0x95dc38>, arg=arg@entry=(<X11Window at remote 0x95b1e0>,), 
    kw=kw@entry=0x0) at /usr/src/debug/Python-2.7.5/Objects/abstract.c:2529
#6  0x00007ffff7af537c in do_call (nk=<optimized out>, na=1, 
pp_stack=0x7fffffffcd00, func=<gi.FunctionInfo at remote 0x95dc38>)
    at /usr/src/debug/Python-2.7.5/Python/ceval.c:4316
#7  call_function (oparg=<optimized out>, pp_stack=0x7fffffffcd00) at 
/usr/src/debug/Python-2.7.5/Python/ceval.c:4121
#8  PyEval_EvalFrameEx (
    f=f@entry=Frame 0x9d5cc0, for file /usr/lib64/python2.7/site-
packages/gi/overrides/Gdk.py, line 159, in cairo_create (self=<X11Window at 
remote 0x95b1e0>), throwflag=throwflag@entry=0) at 
/usr/src/debug/Python-2.7.5/Python/ceval.c:2740
#9  0x00007ffff7af7980 in fast_function (nk=<optimized out>, na=1, n=1, 
pp_stack=0x7fffffffce60, func=<function at remote 0x8ccd70>)
    at /usr/src/debug/Python-2.7.5/Python/ceval.c:4184
(More stack frames follow...)

***** cairo_reference is called to as part of the process of
***** marshelling the cairo_t into python land.
***** As far as I can tell this is the root cause of the leak.
***** THere is no conditional logic here to prevent extra
***** ref_count increment that I could see in the code.

(gdb) c
Continuing.
sleep
^C

***** I added a time.sleep( 10 ) so that we can see the ref count of the 
returned cairo_t.

Program received signal SIGINT, Interrupt.
0x00007ffff6e1a463 in select () at ../sysdeps/unix/syscall-template.S:81
81      T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
(gdb) p *(int *)0x9d8c60
$57 = 2

**** As expected from the available code the ref_count is 2 which 1 to big.

(gdb) c
Continuing.
done

Breakpoint 3, INT_cairo_destroy (cr=0x9d8c60) at cairo.c:300
300         if (cr == NULL || CAIRO_REFERENCE_COUNT_IS_INVALID (&cr-
>ref_count))
(gdb) bt 10
#0  INT_cairo_destroy (cr=0x9d8c60) at cairo.c:300
#1  0x00007fffe67ebd72 in pycairo_dealloc (o=0x7ffff7f68310) at context.c:75
#2  0x00007ffff7a84b02 in frame_dealloc (f=Frame 0x9d5af0, for file gdk-cairo-
leak-barry.py, line 49, in draw ())
    at /usr/src/debug/Python-2.7.5/Objects/frameobject.c:460
#3  0x00007ffff7af799c in fast_function (nk=<optimized out>, na=<optimized 
out>, n=1, pp_stack=0x7fffffffcfc0, func=<function at remote 0x9515f0>)
    at /usr/src/debug/Python-2.7.5/Python/ceval.c:4186
#4  call_function (oparg=<optimized out>, pp_stack=0x7fffffffcfc0) at 
/usr/src/debug/Python-2.7.5/Python/ceval.c:4119
#5  PyEval_EvalFrameEx (
    f=f@entry=Frame 0x9d5920, for file gdk-cairo-leak-barry.py, line 34, in 
__gdkEventHandler (self=<CairoLeak(repeat_count=0, window=<X11Window at remote 
0x95b1e0>, letterbox_colour=[255, 0, 0, 255]) at remote 0x958200>, 
event=<Event at remote 0x8b58d8>, data=None), throwflag=throwflag@entry=0)
    at /usr/src/debug/Python-2.7.5/Python/ceval.c:2740
#6  0x00007ffff7af91dd in PyEval_EvalCodeEx (co=<optimized out>, 
globals=<optimized out>, locals=locals@entry=0x0, args=args@entry=0x95b068, 
argcount=3, 
    kws=kws@entry=0x0, kwcount=kwcount@entry=0, defs=defs@entry=0x0, 
defcount=defcount@entry=0, closure=0x0)
    at /usr/src/debug/Python-2.7.5/Python/ceval.c:3330
#7  0x00007ffff7a860d8 in function_call (func=<function at remote 0x951578>, 
    arg=(<CairoLeak(repeat_count=0, window=<X11Window at remote 0x95b1e0>, 
letterbox_colour=[255, 0, 0, 255]) at remote 0x958200>, <Event at remote 
0x8b58d8>, None), kw=0x0) at 
/usr/src/debug/Python-2.7.5/Objects/funcobject.c:526
#8  0x00007ffff7a610d3 in PyObject_Call (func=func@entry=<function at remote 
0x951578>, 
    arg=arg@entry=(<CairoLeak(repeat_count=0, window=<X11Window at remote 
0x95b1e0>, letterbox_colour=[255, 0, 0, 255]) at remote 0x958200>, <Event at 
remote 0x8b58d8>, None), kw=kw@entry=0x0) at 
/usr/src/debug/Python-2.7.5/Objects/abstract.c:2529
#9  0x00007ffff7a700c5 in instancemethod_call (func=<function at remote 
0x951578>, 
    arg=(<CairoLeak(repeat_count=0, window=<X11Window at remote 0x95b1e0>, 
letterbox_colour=[255, 0, 0, 255]) at remote 0x958200>, <Event at remote 
0x8b58d8>, None), kw=0x0) at 
/usr/src/debug/Python-2.7.5/Objects/classobject.c:2602
(More stack frames follow...)
(gdb) c
Continuing.

***** we hit cairo_destory on the freeing of the python side object.

Hardware watchpoint 12: *(int *)0x9d8c60

Old value = 2
New value = 1
0x00007fffea678e04 in INT_cairo_destroy (cr=0x9d8c60) at cairo.c:305
305         if (! _cairo_reference_count_dec_and_test (&cr->ref_count))

***** and the ref count goes to 1.

(gdb) c
Continuing.
[Thread 0x7ffff7fe7740 (LWP 18063) exited]
[Inferior 1 (process 18063) exited normally]

***** process exits with 1 leaked cairo_t.

(gdb) 

Barry
#!/usr/bin/python
import sys
import gi
from gi.repository import GObject, GLib, Gdk, GdkX11
import cairo
GObject.threads_init()
GLib.threads_init()

class CairoLeak:
    def __init__( self ):
        window_attrs = Gdk.WindowAttr()
        window_attrs.title = "Leak"
        window_attrs.event_mask = Gdk.EventMask.EXPOSURE_MASK
        window_attrs.width = 400
        window_attrs.height = 300
        window_attrs.wclass = Gdk.WindowWindowClass.INPUT_OUTPUT
        window_attrs.window_type = Gdk.WindowType.TOPLEVEL
        window_attrs.visual = Gdk.Screen.get_default().get_rgba_visual()
        self.window = Gdk.Window( None, window_attrs, Gdk.WindowAttributesType.TITLE | Gdk.WindowAttributesType.VISUAL )

        self.letterbox_colour = [ 255, 0, 0, 255 ]
        Gdk.Event.handler_set( self.__gdkEventHandler, None )

        self.window.show()

    def __gdkEventHandler( self, event, data ):
        self.draw()

    def draw( self ):
        cr = self.window.cairo_create()

        cr.set_operator( cairo.OPERATOR_SOURCE )
        cr.set_source_rgba( self.letterbox_colour[0] / 255.0,
                            self.letterbox_colour[1] / 255.0,
                            self.letterbox_colour[2] / 255.0,
                            self.letterbox_colour[3] / 255.0 )
        cr.paint()
        Gdk.flush()
        self.letterbox_colour = [self.letterbox_colour[2],
                                 self.letterbox_colour[0],
                                 self.letterbox_colour[1],
                                 self.letterbox_colour[3]]

def triggerLeak( leaker ):
    leaker.draw()

    global repeat_count
    repeat_count -= 1

    if repeat_count <= 0:
        global main_loop
        main_loop.quit()

    return repeat_count > 0

def main( argv ):
    global repeat_count
    repeat_count = int( sys.argv[1] )

    global main_loop
    main_loop = GLib.MainLoop()

    leaker = CairoLeak()

    GLib.timeout_add_seconds( 1, triggerLeak, leaker )

    main_loop.run()

if __name__ == '__main__':
    sys.exit( main( sys.argv ) )
_______________________________________________
python-hackers-list mailing list
python-hackers-list@gnome.org
https://mail.gnome.org/mailman/listinfo/python-hackers-list

Reply via email to