5

I am using pygraphviz to create a large number of graphs for different configurations of data. I have found that no matter what information is put in the graph the program will crash after drawing the 170th graph. There are no error messages generated the program just stops. Is there something that needs to be reset if drawing this many graphs?

I am running Python 3.7 on a Windows 10 machine, Pygraphviz 1.5, and 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') 
CristiFati
  • 38,250
  • 9
  • 50
  • 87
draB1
  • 63
  • 2
  • 7
  • PyGraphviz has a github source code repository with an Issues tab -have you tried asking there? – DisappointedByUnaccountableMod Mar 26 '20 at 22:50
  • 1
    Did you check the memory usage of the program / increase. Maybe the memory fre is not correct. Did you try moving the for statement directly before the `# Draw Graph`? Where did you load the dot code that should be drawn? – albert Mar 30 '20 at 16:30
  • 1
    To add to what @albert said, a memory issue seems the most likely problem if you are running a 32-bit version of Python. However, if this was the case, there should be a way of reducing memory usage (which would then be the answer to your question). – Minion Jim Mar 31 '20 at 15:23
  • @draB1 please check the comments and give feedback – albert Apr 04 '20 at 17:01

2 Answers2

5

I was able to constantly reproduce the behavior with:

  1. Python 3.7.6 (pc064 (64bit), then also with pc032)

  2. PyGraphviz 1.5 (that I built - available for download at [GitHub]: CristiFati/Prebuilt-Binaries - Various software built on various platforms. (under PyGraphviz, naturally).
    Might also want to check [SO]: Installing pygraphviz on Windows 10 64-bit, Python 3.6 (@CristiFati's answer))

  3. Graphviz 2.42.2 ((pc032) same as #2.)

I suspected an Undefined Behavior somewhere in the code, even if the behavior was precisely the same:

  • OK for 169 graphs

  • Crash for 170

Did some debugging (added some print(f) statements in agraph.py, and cgraph.dll (write.c)).
PyGraphviz invokes Graphviz's tools (.exes) for many operations. For that, it uses subprocess.Popen and communicates with the child process via its 3 available streams (stdin, stdout, stderr).

From the beginning I noticed that 170 * 3 = 510 (awfully close to 512 (0x200)), but didn't pay as much attention as I should have until later (mostly because the Python process (running the code below) had no more than ~150 open handles in Task Manager (TM) and also Process Explorer (PE)).

However, a bit of Googleing revealed:

Below is your code that I modified for debugging and reproducing the error. It needs (for code shortness' sake, as same thing can be achieved via CTypes) the PyWin32 package (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)

Output:

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.

Conclusions:

  • Apparently, some file handles (fds) are open, although they are not "seen" by TM or PE (probably they are on a lower level). However I don't know why this happens (is it a MS UCRT bug?), but from what I am concerned, once a child process ends, its streams should be closed, but I don't know how to force it (this would be a proper fix)

  • Also, the behavior (crash) when attempting to write (not open) to a fd (above the limit), seems a bit strange

  • As a workaround, the max open fds number can be increased. Based on the following inequality: 3 * (graph_count + 1) <= max_fds, you can get an idea about the numbers. From there, if you set the limit to 8192 (I didn't test this) you should be able handle 2729 graphs (assuming that there are no additional fds opened by the code)

Side notes:

CristiFati
  • 38,250
  • 9
  • 50
  • 87
  • 2
    This is impressive detective work. What would happen if the OP called `subprocess.run` directly *without* using the standard streams, i.e. the streams set to `DEVNULL`? – Roland Smith Apr 05 '20 at 22:34
  • @RolandSmith: Thx. it's *PyGraphviz* which does the call. I tried modifying `close_fds=True` but it didn't have any effect. Not sure how *OP* could do something without modifying the *pgv* code. But even if so, it wouldn't work because the child process needs to be interacted with and the *subprocess* hi level wrappers don't allow that. – CristiFati Apr 05 '20 at 22:44
  • I meant *without* using PyGraphviz. – Roland Smith Apr 05 '20 at 22:45
  • Hmm, don't know. But I imagine that a lot of code should be written. I am not a *Graphviz* expert, so I didn't even think going down on that road. But based on what I see in *agraph.py* (and my previous comment test), I tend to incline that it wouldn't make any difference. – CristiFati Apr 05 '20 at 22:48
  • 3
    Brilliant detective work! Let's make sure this ends up in respective repo issues ❤️ – Dima Tisnek Apr 06 '20 at 07:28
  • 1
    @DimaTisnek: Thanks. Apparently there is a *PyGraphviz* bug: https://github.com/pygraphviz/pygraphviz/issues/213. – CristiFati Apr 06 '20 at 08:36
-1

I tried you code and it generated 200 graphs with no problem (I also tried with 2000).

My suggestion is to use these versions of the packages, I installed a conda environment on mac os with python 3.7 :

graphviz 2.40.1 hefbbd9a_2

pygraphviz 1.3 py37h1de35cc_1

Florian
  • 874
  • 1
  • 8
  • 17
  • 1
    I don't think this provides a real answer as you worked on a mac and OP on Windows. Furthermore you used different versions of graphviz (newer) and pygraphviz (older). Problem might be system dependent. – albert Mar 31 '20 at 09:02
  • I agree that this does not explain what happens for the OP. But this gives the idea that there is nothing wrong with the code, there is no reset to perform (which is the OP question). Moreover, using these versions may provide a real solution for windows also. – Florian Mar 31 '20 at 20:13
  • The OP code might not be wrong, but there might be some problems with: pygrapviz 1.5 or the used python version (3.7 is the general name, might be a sub 3.7.x problem), might be a memory leak / garbage collection problem ... but we won't know till OP answers on the questions. – albert Apr 01 '20 at 08:26
  • Agreed. I still think that my answer may provide a solution which is worth trying. – Florian Apr 04 '20 at 15:49