我的 PyGObject 项目中的内存泄漏 - 但似乎在 Python 之外

Memory leak in my PyGObject project - but seems to be outside of Python

(根据新发现重写)

我试图理解的一段 PyGObject 代码中存在内存泄漏。

它是可重现的,至少在这里(Debian 稳定版),使用以下代码:

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib
from gi.repository import Gtk


def foo(labels):
    for label in labels:
        label.props.label = 100000 * "x"
    GLib.timeout_add(1000, foo, labels)


def main():
    labels = []
    box = Gtk.Box(visible=True)
    for _ in range(40):
        label = Gtk.Label(visible=False)
        labels.append(label)
        box.add(label)
    window = Gtk.Window(visible=True)
    window.add(box)
    GLib.idle_add(foo, labels)
    GLib.MainLoop().run()


if __name__ == "__main__":
    main()

您可能可以调整一些数字,但这样很快就会很明显地看到内存消耗发生了什么。当我启动它时,不到 8 分钟就达到了 2GB。

guppy 内存分析器中没有任何可见内容。我可以随时制作堆快照,它们看起来或多或少都是一样的。此外 heapu() 对于无法访问但存在的对象不会显示可疑内容。

同样的情况也发生在其他变体中。这里例如用线程代替(并且只有一个标签):

def foo(label):
    label.props.label = 1000000 * "x"


def bar(label):
    while True:
        time.sleep(1)
        GLib.idle_add(foo, label)


def main():
    box = Gtk.Box(visible=True)
    label = Gtk.Label(visible=False)
    box.add(label)
    window = Gtk.Window(visible=True)
    window.add(box)
    import threading
    threading.Thread(target=bar, args=(label,)).start()
    GLib.MainLoop().run()

当我使用 .set_label() 而不是 .props.label = ... 时,它不会发生。

更新:

我已经举报了:https://gitlab.gnome.org/GNOME/pygobject/-/issues/520

原文为:

我的 PyGObject 项目再次发生内存泄漏。但它似乎在 Python.

之外

我有一个小型 GTK 应用程序。它像往常一样创建一个 window 和一个主循环,然后使用 GLib.idle_add 在主循环中调用一个函数。此函数从 window 中删除所有小部件,创建一些 Gtk.Label(带有一些文本;例如随机数),将它们放入 Gtk.Box,然后将该框放入 window。然后它使用 GLib.timeout_add 在一毫秒后再次安排自己的调用。

因此它会不断创建新内容并删除旧内容,但始终显示相同数量(和种类)的小部件。我希望看到持续的内存消耗。

该应用程序单独存在,因此我可以分析特定的内存泄漏。 trim 进一步降低代码以便发布它并不容易,我怀疑。实际代码以更加间接和复杂的方式完成所有这些工作。 :) 我知道只创建一次 UI 元素然后更新它们可能更直接。但这不是那个实验的重点。

当我运行它时,进程内存消耗是40MB,而且还在稳步攀升。我看到它上升到 2GB,但随后停止了。当我使用 guppy 进行内存分析时,一切看起来都很好。调用 guppy 的 heap() 总是显示大致相同的堆内容。没有泄漏。 heapu() 也是如此,即对于无法访问但活着的对象。没有泄漏。

但最后,它在某处泄漏了。那么,我是不是在做一些导致 GTK 端泄漏的事情?!

问题:

进一步分析该问题的好方法是什么?当涉及到所有 C 语言、gdb、valgrind 等等,以及可以与我的 Python 代码交互的 if/how 时,我相当缺乏经验。任何提示表示赞赏。

我可以访问 Linux 系统,您可以使用 https://github.com/vmware/chap 解决此类问题,如本例所示。

它是免费的开源软件,非常适合您的程序,因为它可以识别 Python 分配和通过 libc malloc 完成的分配。

无论如何,至少你的第一个程序很容易在 Linux 上复制。

我将您的代码剪切并粘贴到一个名为 usegtk.py 的文件中,并在后台启动了您的程序。 pid 为 158483 的进程 运行:

$ python3 usegtk.py &
[1] 158483

为了确保核心有足够的信息让 chap 收集损坏的符号名称,我将 pid 158483 的 coredump_filter 设置为 0x37:

$ echo 0x37 >/proc/158483/coredump_filter

然后我稍等片刻,使用 gcore 为该进程收集了一个核心,并终止了该进程,因为众所周知它正在快速增长,因为我不再需要该进程了。

然后我在chap中打开内核,等待chap提示:

$ chap core.158483
chap> 

在 chap 提示符下使用 summarize writable 显示内存使用量最大的是“libc malloc main arena pages”,而“python 竞技场".

chap> summarize writable
1 ranges take 0x2a847000 bytes for use: libc malloc main arena pages
95 ranges take 0x100f2000 bytes for use: used by module
3 ranges take 0x1800000 bytes for use: used pthread stack
19 ranges take 0x4c0000 bytes for use: python arena
3 ranges take 0x1a4000 bytes for use: libc malloc mmapped allocation
3 ranges take 0x63000 bytes for use: libc malloc heap
1 ranges take 0x21000 bytes for use: main stack
15 ranges take 0x1d000 bytes for use: unknown
140 writable ranges use 0x3c83e000 (1,015,275,520) bytes.

使用 count usedcount leaked 从 chap 提示显示已使用的分配,更具体地说是泄漏的分配,占据了内存用法:

chap> count used
78609 allocations use 0x2ab84248 (716,718,664) bytes.
chap> count leaked
7056 allocations use 0x271c52c0 (656,167,616) bytes.

使用 summarize leaked 表明,到目前为止,泄漏分配消耗的内存最多的是大小为 0x186a8 的分配,chap 无法识别类型:

chap> summarize leaked
Unrecognized allocations have 7020 instances taking 0x271c4f10(656,166,672) bytes.
   Unrecognized allocations of size 0x186a8 have 6561 instances taking 0x271c17a8(656,152,488) bytes.
   Unrecognized allocations of size 0x18 have 354 instances taking 0x2130(8,496) bytes.
   Unrecognized allocations of size 0x28 have 53 instances taking 0x848(2,120) bytes.
   Unrecognized allocations of size 0x38 have 39 instances taking 0x888(2,184) bytes.
   Unrecognized allocations of size 0x88 have 7 instances taking 0x3b8(952) bytes.
   Unrecognized allocations of size 0x48 have 6 instances taking 0x1b0(432) bytes.
Signature 7f88fe8500a0 has 2 instances taking 0x70(112) bytes.
Signature 7f88fce30930 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce31610 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce31690 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce31760 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce317e0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce31860 has 1 instances taking 0x28(40) bytes.
Signature 7f88fce32370 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce32450 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce32460 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce32470 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce34f40 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce36260 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce362e0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce36e20 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce36ea0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce37630 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce376b0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce37cc0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce39830 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce398e0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce39990 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce39a10 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce39a90 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce39b10 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce39e30 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3a180 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3a250 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3a2d0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3b8e0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3b960 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3c6d0 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3c830 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3c960 has 1 instances taking 0x18(24) bytes.
Signature 7f88fce3cab0 has 1 instances taking 0x18(24) bytes.
7056 allocations use 0x271c52c0 (656,167,616) bytes.

有几种方法可以理解 chap 无法识别的泄漏分配。一种是查看它以寻找线索。在这种情况下,我们主要对那些大小为 0x186a8 的分配感兴趣。我们可以对它们进行采样,看看样本中元素的开头:

chap> describe leaked /size 186a8 /geometricSample 100 /showUpTo 100 /showAscii true
Unreferenced allocation at 2afbd80 of size 186a8

 0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
20: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
40: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
60: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
80: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
a0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
c0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
e0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Unreferenced allocation at 3f1c090 of size 186a8

 0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
20: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
40: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
60: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
80: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
a0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
c0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
e0: 7878787878787878 7878787878787878 7878787878787878 7878787878787878   xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2 allocations use 0x30d50 (200,016) bytes.

鉴于分配中 'x' 的 运行 和 0x186a8 = 100008,这是合理的,因为 libc malloc 将 return 作为一个分配的结果请求 100,000 字节,我们可以合理猜测以下语句是导致泄漏的语句:

    label.props.label = 100000 * "x"

要修复泄漏,可以将上面的行更改为以下内容:

    label.set_label(100000 * "x")

如果您 运行 修改过的程序,您会注意到它没有增长。在单个进程的两个内核上使用 chap 运行 修改后的程序显示仍然存在轻微泄漏,但它显然只在启动时发生。这是 chap 报告从新程序中泄露的两个内核的内容:

chap> summarize leaked
Unrecognized allocations have 464 instances taking 0x35a0(13,728) bytes.
   Unrecognized allocations of size 0x18 have 385 instances taking 0x2418(9,240) bytes.
   Unrecognized allocations of size 0x28 have 43 instances taking 0x6b8(1,720) bytes.
   Unrecognized allocations of size 0x38 have 22 instances taking 0x4d0(1,232) bytes.
   Unrecognized allocations of size 0x88 have 7 instances taking 0x3b8(952) bytes.
   Unrecognized allocations of size 0x48 have 6 instances taking 0x1b0(432) bytes.
   Unrecognized allocations of size 0x98 have 1 instances taking 0x98(152) bytes.
Pattern %ContainerPythonObject has 31 instances taking 0x618(1,560) bytes.
   Matches of size 0x38 have 20 instances taking 0x460(1,120) bytes.
   Matches of size 0x28 have 11 instances taking 0x1b8(440) bytes.
495 allocations use 0x3bb8 (15,288) bytes.

可以追踪剩余的泄漏,但它们似乎并不那么有趣,因为它们似乎只在启动时发生并且不会占用那么多内存。