I found reference leaks in scaled_font_text_to_glyphs(). On the successful path it leaks one argument tuple per converted glyph and, when cluster output is enabled, one additional tuple per converted cluster. The leak is deterministic and scales with the amount of converted text, I measured ~414 MB of growth in a simple loop (details at the bottom).
File: cairo/font.c
Function: scaled_font_text_to_glyphs
Relevant code (glyph loop):
for(i=0; i < num_glyphs; i++) {
cairo_glyph_t *glyph = &glyphs[i];
glyph_args = Py_BuildValue(
"(kdd)", glyph->index, glyph->x, glyph->y);
if (glyph_args == NULL)
goto error;
pyglyph = PyObject_Call(
(PyObject *)&PycairoGlyph_Type, glyph_args, NULL);
if (pyglyph == NULL) {
Py_DECREF (glyph_args);
goto error;
}
PyList_SET_ITEM (glyph_list, i, pyglyph);
}
Py_BuildValue() returns a new owned reference. PyObject_Call() borrows its argument tuple and does not steal it. The failure branch decrefs glyph_args, but the successful branch inserts only pyglyph into the list and never releases glyph_args.
The cluster loop has the same bug:
cluster_args = Py_BuildValue(
"(ii)", cluster->num_bytes, cluster->num_glyphs);
if (cluster_args == NULL)
goto error;
pycluster = PyObject_Call(
(PyObject *)&PycairoTextCluster_Type, cluster_args, NULL);
if (pycluster == NULL) {
Py_DECREF (cluster_args);
goto error;
}
PyList_SET_ITEM (cluster_list, i, pycluster);
Note that the leak is larger than one empty tuple per glyph: the leaked tuple also owns the boxed int and the two float objects built for it, so nothing in it is ever reclaimed. The measurement below works out to roughly 190 bytes per converted glyph.
Suggested fix
Decref the args tuple right after the call, at both sites:
pyglyph = PyObject_Call(
(PyObject *)&PycairoGlyph_Type, glyph_args, NULL);
Py_DECREF (glyph_args);
if (pyglyph == NULL)
goto error;
PyList_SET_ITEM (glyph_list, i, pyglyph);
Apply the equivalent ordering to cluster_args.
Reproducer
import cairo, gc, resource
def rss():
gc.collect()
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss # KiB
surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, 10, 10)
sf = cairo.Context(surf).get_scaled_font()
text = "hello world" * 10 # 110 glyphs
for _ in range(100):
sf.text_to_glyphs(0, 0, text) # warm up allocators
before = rss()
for _ in range(20000):
sf.text_to_glyphs(0, 0, text)
after = rss()
print(f"RSS {before} -> {after} KiB (+{after - before})")
Output on pycairo 1.29.1 (built from current master), Python 3.13.5,
cairo 1.16.0:
RSS 18916 -> 433192 KiB (+414276)
That is 20000 × 110 = 2,200,000 leaked tuples. RSS returns to normal with the
fix applied.
I found reference leaks in
scaled_font_text_to_glyphs(). On the successful path it leaks one argument tuple per converted glyph and, when cluster output is enabled, one additional tuple per converted cluster. The leak is deterministic and scales with the amount of converted text, I measured ~414 MB of growth in a simple loop (details at the bottom).File:
cairo/font.cFunction:
scaled_font_text_to_glyphsRelevant code (glyph loop):
Py_BuildValue()returns a new owned reference.PyObject_Call()borrows its argument tuple and does not steal it. The failure branch decrefsglyph_args, but the successful branch inserts onlypyglyphinto the list and never releasesglyph_args.The cluster loop has the same bug:
Note that the leak is larger than one empty tuple per glyph: the leaked tuple also owns the boxed
intand the twofloatobjects built for it, so nothing in it is ever reclaimed. The measurement below works out to roughly 190 bytes per converted glyph.Suggested fix
Decref the args tuple right after the call, at both sites:
Apply the equivalent ordering to
cluster_args.Reproducer
Output on pycairo 1.29.1 (built from current master), Python 3.13.5,
cairo 1.16.0:
That is 20000 × 110 = 2,200,000 leaked tuples. RSS returns to normal with the
fix applied.