Pygraphviz在绘制了170张图后崩溃。

5
我正在使用pygraphviz来创建大量不同数据配置的图形。我发现无论放入什么信息,程序在绘制第170个图之后会崩溃。没有生成任何错误消息,程序只是停止了。如果要绘制这么多图形,是否需要重置一些东西?
我正在运行Python 3.7,Windows 10机器上的Pygraphviz 1.5和graphviz 2.38。
    for graph_number in range(200):
        config_graph = pygraphviz.AGraph(strict=False, directed=False, compound=True, ranksep='0.2', nodesep='0.2')

        # Create Directory
        if not os.path.exists('Graph'):
            os.makedirs('Graph')

        # Draw Graph      
        print('draw_' + str(graph_number))
        config_graph.layout(prog = 'dot')
        config_graph.draw('Graph/'+str(graph_number)+'.png') 

PyGraphviz有一个Github源代码存储库,带有Issues选项卡-您尝试在那里提问了吗? - DisappointedByUnaccountableMod
1
你有检查程序/增加的内存使用情况吗?也许内存释放不正确。你尝试将for语句直接移动到“#绘制图形”之前了吗?你在哪里加载应该被绘制的点代码? - albert
1
补充一下@albert所说的,如果你正在运行32位版本的Python,则内存问题似乎是最可能的问题。但是,如果是这种情况,应该有一种减少内存使用的方法(这将是你问题的答案)。 - Minion Jim
@draB1 请查看注释并提供反馈。 - albert
2个回答

5

我能够在以下环境下不断重现这种行为:

  1. Python 3.7.6 (pc064 (64位), 然后也是通过 pc032)

  2. PyGraphviz 1.5 (我自己编译的 - 可在 [GitHub]: CristiFati/Prebuilt-Binaries - Various software built on various platforms. 下载(当然是在 PyGraphviz 下)。
    也可以查看 [SO]: Installing pygraphviz on Windows 10 64-bit, Python 3.6 (@CristiFati's answer))

  3. Graphviz 2.42.2 ((pc032) 同 #2.)

我怀疑在代码中有某处未定义的行为,即使行为是精确相同的

  • 169个图形可成功运行

  • 170个图形会崩溃

进行了一些调试(在 agraph.pycgraph.dll (write.c) 中添加了一些 print(f) 语句)。
PyGraphviz 对许多操作都会调用 Graphviz 的工具(.exe)。为此,它使用 subprocess.Popen 并通过其3个可用流 (stdin, stdout, stderr) 与子进程通信。

从一开始我就注意到 170 * 3 = 510 (非常接近 512 (0x200)),但直到后来我没有给予足够的关注(主要是因为Python进程(运行以下代码)在任务管理器 (TM) 和Process Explorer (PE)中最多只有约150个打开句柄)。

然而,经过一番谷歌搜索之后,发现:

以下是我为了调试和重现错误而修改的代码。为了代码简短起见(因为可以通过CTypes实现相同的效果),它需要PyWin32包(python -m pip install pywin32)。

code00.py

#!/usr/bin/env python

import os
import sys
#import time

import pygraphviz as pgv
import win32file as wfile


def handle_graph(idx, dir_name):
    graph_name = "draw_{:03d}".format(idx)
    graph_args = {
        "name": graph_name,
        "strict": False,
        "directed": False,
        "compound": True,
        "ranksep": "0.2",
        "nodesep": "0.2",
    }
    graph = pgv.AGraph(**graph_args)
    # Draw Graph      
    img_base_name = graph_name + ".png"
    print("  {:s}".format(img_base_name))
    graph.layout(prog="dot")
    img_full_name = os.path.join(dir_name, img_base_name)
    graph.draw(img_full_name)
    graph.close()  # !!! Has NO (visible) effect, but I think it should be called anyway !!!


def main(*argv):
    print("OLD max open files: {:d}".format(wfile._getmaxstdio()))
    # 513 is enough for your original code (170 graphs), but you can set it up to 8192
    #wfile._setmaxstdio(513)  # !!! COMMENT this line to reproduce the crash !!!
    print("NEW max open files: {:d}".format(wfile._getmaxstdio()))

    dir_name = "Graph"
    # Create Directory
    if not os.path.isdir(dir_name):
        os.makedirs(dir_name)

    #ts_global_start = time.time()
    start = 0
    count = 170
    #count = 1
    step_sleep = 0.05
    for i in range(start, start + count):
        #ts_local_start = time.time()
        handle_graph(i, dir_name)
        #print("  Time: {:.3f}".format(time.time() - ts_local_start))
        #time.sleep(step_sleep)
    handle_graph(count, dir_name)
    #print("Global time: {:.3f}".format(time.time() - ts_global_start - step_sleep * count))


if __name__ == "__main__":
    print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                   64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.\n")
    sys.exit(rc)

输出:

e:\Work\Dev\StackOverflow\q060876623> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" ./code00.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 064bit on win32

OLD max open files: 512
NEW max open files: 513
  draw_000.png
  draw_001.png
  draw_002.png

...

  draw_167.png
  draw_168.png
  draw_169.png

Done.

结论:

  • 显然,有一些文件句柄(fd)是打开的,但它们不被TMPE所"看到"(可能在更低层次上)。然而,我不知道这为什么会发生(是MS UCRT的一个bug吗?),但就我所关心的而言,一旦子进程结束,它的流应该被关闭,但我不知道如何强制执行它(这将是一个适当的修复方法)

  • 此外,在尝试对一个fd(超过限制的)进行写入(而非打开)时的行为(崩溃),似乎有点奇怪

  • 作为解决方法,可以增加最大打开fd数。基于以下不等式:3 * (graph_count + 1) <= max_fds,您可以对数字有一个了解。从那里开始,如果您将限制设置为8192(我没有测试这个),则应该能够处理2729个图形(假设代码没有打开其他fd)

附注:


2
这是令人印象深刻的侦探工作。如果 OP 直接调用 subprocess.run,而不使用标准流,即将流设置为 DEVNULL,会发生什么? - Roland Smith
@RolandSmith:谢谢。是PyGraphviz在进行调用。我尝试修改close_fds=True,但没有任何效果。不确定OP如何在不修改pgv代码的情况下做一些事情。但即使如此,也不会起作用,因为子进程需要进行交互,而subprocess高级包装器不允许这样做。 - CristiFati
我的意思是不使用PyGraphviz。 - Roland Smith
嗯,我不知道。但是我想很多代码需要编写。我不是Graphviz专家,所以我甚至没有考虑那条路。但根据我在agraph.py中看到的(和我之前的评论测试),我倾向于认为这不会有任何区别。 - CristiFati
3
太棒了!我们确保这个问题被归入到相应的存储库里 ❤️ - Dima Tisnek
1
@DimaTisnek:谢谢。显然有一个 PyGraphviz 的 bug:https://github.com/pygraphviz/pygraphviz/issues/213。 - CristiFati

-1

我尝试了你的代码,生成了200个图表,没有任何问题(我也尝试了2000个)。

我的建议是使用这些软件包的版本,我在Mac OS上安装了一个conda环境,使用Python 3.7:

graphviz 2.40.1 hefbbd9a_2

pygraphviz 1.3 py37h1de35cc_1


1
我认为这并没有提供真正的答案,因为你在Mac上工作,而OP在Windows上。此外,你使用了不同版本的graphviz(更新的)和pygraphviz(较旧的)。问题可能是系统相关的。 - albert
我同意这并没有解释发生了什么。但是这表明代码没有问题,也没有需要执行的重置(这是OP的问题)。此外,使用这些版本可能也为Windows提供了真正的解决方案。 - Florian
OP代码可能没有问题,但是可能存在一些问题:pygrapviz 1.5或使用的Python版本(3.7是通用名称,可能存在子3.7.x问题),可能存在内存泄漏/垃圾回收问题...但在OP回答问题之前我们无法确定。 - albert
同意。我仍然认为我的答案可能提供一个值得尝试的解决方案。 - Florian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接