Pygraphviz 绘制 170 个图形后崩溃

dra*_*aB1 5 python windows graphviz python-3.x pygraphviz

我正在使用 pygraphviz 为不同的数据配置创建大量图表。我发现无论在图形中放入什么信息,程序在绘制第 170 个图形后都会崩溃。程序停止时没有生成错误消息。如果绘制这么多图形,是否需要重新设置?

我在 Windows 10 机器、Pygraphviz 1.5 和 graphviz 2.38 上运行 Python 3.7

    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') 
Run Code Online (Sandbox Code Playgroud)

Cri*_*ati 5

我能够通过以下方式不断重现该行为:

  1. Python 3.7.6 ( pc064 ( 64bit ),然后还有pc032 )
  2. PyGraphviz 1.5(我构建的 - 可在[GitHub]下载:CristiFati/Prebuilt-Binaries - 在各种平台上构建的各种软件。(在PyGraphviz下,自然) - 可能还想检查[SO]:在 Windows 10 64 上安装 pygraphviz -位,Python 3.6(@CristiFati 的回答)
  3. Graphviz 2.42.2 (( pc032 ) 与#2相同 )

我怀疑一个未定义行为UB代码)的地方,即使该行为是恰恰是相同的:

  • 确定169个
  • 崩溃170

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

从一开始我就注意到170 * 3 = 510(非常接近512 ( 0x200 )),但直到后来才注意到我应该有的关注(主要是因为Python进程(运行下面的代码)没有超过150 个打开的句柄在任务管理器( TM ) 和进程资源管理器( PE ) 中)。

然而,一些谷歌透露:

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

代码00.py

#!/usr/bin/env python

import sys
import os
#import time
import pygraphviz as pgv
import win32file as wfile


def handle_graph(idx, dir_name):
    graph_name = "draw_{0: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("  {0: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: {0: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: {0: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 = 169
    #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: {0:.3f}".format(time.time() - ts_local_start))
        #time.sleep(step_sleep)
    handle_graph(count, dir_name)
    #print("Global time: {0:.3f}".format(time.time() - ts_global_start - step_sleep * count))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main(*sys.argv[1:])
    print("\nDone.")
Run Code Online (Sandbox Code Playgroud)

输出

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)] 64bit 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.
Run Code Online (Sandbox Code Playgroud)

结论

  • 显然,一些文件句柄(fd s)是打开的,尽管它们没有被TMPE “看到” (可能它们在较低的级别)。但是我不知道为什么会发生这种情况(它是MS UCRT错误吗?),但就我而言,一旦子进程结束,它的流应该关闭,但我不知道如何强制它(这个将是一个适当的修复
  • 此外,尝试写入未打开)到fd(超出限制)时的行为(崩溃)似乎有点奇怪
  • 作为一种解决方法,可以增加最大打开fd数。基于以下不等式:3 * (graph_count + 1) <= max_fds,您可以了解数字。从那里,如果您将限制设置为8192(我没有对此进行测试),您应该能够处理2729 个图形(假设代码没有打开额外的fd

旁注

  • 出色的侦探工作!让我们确保这最终会出现在各自的回购问题中❤️ (3认同)
  • 这是令人印象深刻的侦探工作。如果OP直接调用“subprocess.run”而不使用标准流,即流设置为“DEVNULL”,会发生什么? (2认同)