Pygraphviz 在绘制 170 个图后崩溃
Posted
技术标签:
【中文标题】Pygraphviz 在绘制 170 个图后崩溃【英文标题】:Pygraphviz crashes after drawing 170 graphs 【发布时间】:2020-07-07 15:19:45 【问题描述】:我正在使用 pygraphviz 为不同的数据配置创建大量图表。我发现无论图表中放入什么信息,程序在绘制第 170 个图表后都会崩溃。没有生成错误消息,程序只是停止。画这么多图有什么需要重新设置的吗?
我在 Windows 10 机器上运行 Python 3.7、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 源代码存储库 - 你试过在那里询问吗? 你有没有检查程序的内存使用/增加。也许内存 fre 不正确。您是否尝试将 for 语句直接移动到# Draw Graph
之前?你在哪里加载了应该绘制的点代码?
补充一下@albert 所说的,如果您运行的是 32 位版本的 Python,内存问题似乎是最有可能出现的问题。但是,如果是这种情况,应该有一种方法可以减少内存使用量(这就是您问题的答案)。
@draB1 请检查 cmets 并提供反馈
【参考方案1】:
我能够通过以下方式不断重现该行为:
-
Python 3.7.6 (pc064 (64bit),然后还有 pc032)
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) )
Graphviz 2.42.2 ((pc032) 同 #2.)
我怀疑代码中某处存在未定义行为 (UB),即使该行为准确地一样的:
OK 169 图表 170 崩溃做了一些调试(在 agraph.py 和 cgraph.dll 中添加了一些 print(f) 语句(write. c))。 PyGraphviz 调用 Graphviz 的工具 (.exes) 进行许多操作。为此,它使用 subprocess.Popen 并通过其 3 个可用流(stdin、stdout、stderr)。
从一开始我就注意到170 * 3 = 510
(非常接近512 (0x200)),但直到后来我才注意到(主要是因为 Python 进程(运行下面的代码)在任务管理器中打开的句柄不超过 ~150 个(TM) 以及 Process Explorer (PE))。
然而,一点 Google 透露:
[SO]: Is there a limit on number of open files in Windows (@stackprogrammer's answer)(从这里开始)
[MS.Docs]: _setmaxstdio(声明(重点是我的)):
C 运行时 I/O 现在支持在 low I/O level 同时打开多达 8,192 个文件。此级别包括使用 _open、_read 和 _write 系列 I/O 函数打开和访问的文件。默认情况下,stream I/O level 最多可以同时打开 512 个文件。此级别包括使用 fopen、fgetc 和 fputc 系列函数打开和访问的文件。 512 在流 I/O 级别打开文件的限制可以增加到最大 8,192 _setmaxstdio 函数的使用。
[SO]: Python: Which command increases the number of open files on Windows? (@NorthCat's answer)
以下是我为调试和重现错误而修改的代码。它需要(为了代码简洁,同样的事情可以通过 CTypes 实现)PyWin32 包 (python -m pip install pywin32
)。
code00.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:dbit 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.")
输出:
e:\Work\Dev\***\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.
结论:
显然,一些文件句柄 (fds) 是打开的,尽管 TM 或 PE 没有“看到”它们(可能它们处于较低水平)。但是我不知道为什么会发生这种情况(它是 MS UCRT 错误吗?),但就我而言,一旦子进程结束,它的流应该被关闭,但我不知道如何强制它(这将是一个适当的修复) 此外,尝试写入(未打开)到fd(超过限制),似乎有点奇怪 作为一种解决方法,可以增加 max 打开 fd 的数量。根据以下不等式:3 * (graph_count + 1) <= max_fds
,您可以对数字有所了解。从那里开始,如果您将限制设置为 8192(我没有对此进行测试),您应该能够处理 2729 图表(假设有代码没有打开额外的fd)
旁注:
在调查过程中,我遇到或注意到了几个相邻的问题,我试图解决这些问题:
图形化: [GitLab]: graphviz/graphviz - [Issue #1481]: MSB4018 The NativeCodeAnalysis task failed unexpectedly.(合并于 20200406) PyGraphviz: [GitHub]: pygraphviz/pygraphviz - AGraph Graphviz handle close mechanism此行为也存在一个问题(可能是同一作者):[GitHub]: pygraphviz/pygraphviz - Pygraphviz crashes after drawing 170 graphs
【讨论】:
这是一部令人印象深刻的侦探作品。如果 OP 直接不使用 调用subprocess.run
会发生什么使用标准流,即设置为DEVNULL
的流?
@RolandSmith:谢谢。调用的是 PyGraphviz。我尝试修改close_fds=True
,但没有任何效果。不确定 OP 如何在不修改 pgv 代码的情况下做某事。但即使这样,它也不会起作用,因为子进程需要与之交互,而 subprocess 高级包装器不允许这样做。
我的意思是没有使用 PyGraphviz。
出色的侦探作品!让我们确保这最终出现在相应的回购问题中❤️
@DimaTisnek:谢谢。显然有一个 PyGraphviz 错误:github.com/pygraphviz/pygraphviz/issues/213.【参考方案2】:
我尝试了你的代码,它生成了 200 个没有问题的图表(我也尝试了 2000 个)。
我的建议是使用这些版本的软件包,我在 mac os 上使用 python 3.7 安装了 conda 环境:
graphviz 2.40.1 hefbbd9a_2
pygraphviz 1.3 py37h1de35cc_1
【讨论】:
我认为这不能提供真正的答案,因为您在 Mac 上工作并在 Windows 上使用 OP。此外,您使用了不同版本的 graphviz(较新)和 pygraphviz(较旧)。问题可能与系统有关。 我同意这并不能解释 OP 会发生什么。但这给出了代码没有问题的想法,没有执行重置(这是 OP 问题)。此外,使用这些版本也可以为 windows 提供真正的解决方案。 OP 代码可能没有错,但可能有一些问题:pygrapviz 1.5 或使用的python 版本(3.7 是通用名称,可能是子 3.7.x 问题),可能是内存泄漏/垃圾收集问题......但直到 OP 回答这些问题我们才会知道。 同意。我仍然认为我的回答可能会提供一个值得尝试的解决方案。以上是关于Pygraphviz 在绘制 170 个图后崩溃的主要内容,如果未能解决你的问题,请参考以下文章