From 0097c842990a3f2920b6db969dcd0986e13a0e64 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 20 Dec 2024 16:39:51 -0500 Subject: [PATCH 1/2] GraphvizDispatcher : Add DOT file debug dispatcher --- Changes.md | 1 + .../GafferDispatchTest/GraphvizDispatcher.py | 162 ++++++++++++++++++ python/GafferDispatchTest/__init__.py | 1 + 3 files changed, 164 insertions(+) create mode 100644 python/GafferDispatchTest/GraphvizDispatcher.py diff --git a/Changes.md b/Changes.md index bbd386b062d..131125755a3 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,7 @@ Improvements - VisualiserTool : Changed `dataName` input widget for choosing the primitive variable to visualise to a list of available variable names for the current selection. - Tweaks nodes : Moved list of tweaks to a collapsible "Tweaks" section in the NodeEditor. +- GafferDispatchTest : Added GraphvizDispatcher for outputting batch graphs in Graphviz DOT format. It can be added to a script using the Python editor : `import GafferDispatchTest; root.addChild( GafferDispatchTest.GraphvizDispatcher())`. Fixes ----- diff --git a/python/GafferDispatchTest/GraphvizDispatcher.py b/python/GafferDispatchTest/GraphvizDispatcher.py new file mode 100644 index 00000000000..46a27061b76 --- /dev/null +++ b/python/GafferDispatchTest/GraphvizDispatcher.py @@ -0,0 +1,162 @@ +########################################################################## +# +# Copyright (c) 2025, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Image Engine Design Inc nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import IECore + +import Gaffer +import GafferDispatch + +class GraphvizDispatcher( GafferDispatch.Dispatcher ) : + + def __init__( self, name = "GraphvizDispatcher" ) : + + GafferDispatch.Dispatcher.__init__( self, name ) + + self["fileName"] = Gaffer.StringPlug() + self["includeFrames"] = Gaffer.BoolPlug( defaultValue = True ) + self["contextVariables"] = Gaffer.StringPlug() + + def __walkBatches( self, batch, outFile, scriptNode ) : + + if "id" in batch.blindData() : + return + + nodeName = batch.plug().node().relativeName( scriptNode ) if batch.plug() is not None else "" + nodeType = batch.plug().node().typeName() if batch.plug() is not None else "None" + batch.blindData()["id"] = ( + nodeName + batch.context().hash().toString() + ) if batch.context() is not None else IECore.MurmurHash().toString() + + frameString = None + if self["includeFrames"].getValue() : + frameString = "-" + frameList = IECore.frameListFromList( [ int( i ) for i in batch.frames() ] ) + if len( frameList.asList() ) > 0 : + if frameList.start == frameList.end : + frameString = str( frameList.start ) + elif frameList.step != 1 : + frameString = "{} - {} x {}".format( frameList.start, frameList.end, frameList.step ) + else : + frameString = "{} - {}".format( frameList.start, frameList.end ) + + contextVariables = {} + + if batch.context() is not None : + scriptContext = scriptNode.context() + matchPattern = self["contextVariables"].getValue() + for entry in [ k for k in batch.context().keys() if k != "frame" and not k.startswith( "ui:" ) and not k.startswith( "dispatcher:" ) ] : + if IECore.StringAlgo.matchMultiple( entry, matchPattern ) and ( entry not in scriptContext.keys() or batch.context()[entry] != scriptContext[entry] ) : + contextVariables[entry] = batch.context().substitute( str( batch.context()[entry] ) ) + + batchString = '\t"{}" [label="{}\n\t\t{}'.format( batch.blindData()["id"], nodeName, nodeType ) + if frameString is not None : + batchString += "\n\t\t{}".format( frameString ) + if len( contextVariables ) > 0 : + batchString += "\n\t\t" + "\n\t\t".join( + [ "{} = {}".format( k, v ) for k, v in contextVariables.items() ] + ) + + batchString += '"];\n' + outFile.write( batchString ) + + for preTask in batch.preTasks() : + self.__walkBatches( preTask, outFile, scriptNode ) + + outFile.write( + '\t"{}"->"{}";\n'.format( preTask.blindData()["id"], batch.blindData()["id"] ) + ) + + def _doDispatch( self, rootBatch ) : + + jobName = self["jobName"].getValue() + fileName = self["fileName"].getValue() + + scriptNode = rootBatch.preTasks()[0].node().scriptNode() + + with open( fileName, "w" ) as outFile : + outFile.write( "strict digraph {} {{\n".format( jobName ) ) + self.__walkBatches( rootBatch, outFile, scriptNode ) + outFile.write( "}" ) + +IECore.registerRunTimeTyped( GraphvizDispatcher, typeName = "GafferDispatch::GraphvizDispatcher" ) +GafferDispatch.Dispatcher.registerDispatcher( "Graphviz", GraphvizDispatcher ) + +Gaffer.Metadata.registerNode( + + GraphvizDispatcher, + + "description", + """ + Creates a file in Graphviz DOT language representing dependency graph + of the batches being dispatched. + """, + + plugs = { + + "fileName" : [ + + "description", + """ + The name of DOT language file to save. + """, + + "plugValueWidget:type", "GafferUI.FileSystemPathPlugValueWidget", + "path:leaf", True, + + ], + + "includeFrames" : [ + + "description", + """ + Whether or not to include frame ranges in the task description. + """, + + ], + + "contextVariables" : [ + + "description", + """ + The names of context variables to include in the batch description. + Names should be separated by spaces and can use Gaffer's standard wildcards. + """, + + ], + + } + +) diff --git a/python/GafferDispatchTest/__init__.py b/python/GafferDispatchTest/__init__.py index b12d1463404..928f1fd3ed0 100644 --- a/python/GafferDispatchTest/__init__.py +++ b/python/GafferDispatchTest/__init__.py @@ -39,6 +39,7 @@ from .TextWriter import TextWriter from .LoggingTaskNode import LoggingTaskNode from .DebugDispatcher import DebugDispatcher +from .GraphvizDispatcher import GraphvizDispatcher from .ErroringTaskNode import ErroringTaskNode # Test cases From 61d537974e850c7f7cb9f6f0db76c5389ab44cd7 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 8 Jan 2025 17:42:19 -0500 Subject: [PATCH 2/2] GraphvizDispatcher : Use HTML table formatting --- .../GafferDispatchTest/GraphvizDispatcher.py | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/python/GafferDispatchTest/GraphvizDispatcher.py b/python/GafferDispatchTest/GraphvizDispatcher.py index 46a27061b76..978df5b83ae 100644 --- a/python/GafferDispatchTest/GraphvizDispatcher.py +++ b/python/GafferDispatchTest/GraphvizDispatcher.py @@ -54,43 +54,55 @@ def __walkBatches( self, batch, outFile, scriptNode ) : if "id" in batch.blindData() : return - nodeName = batch.plug().node().relativeName( scriptNode ) if batch.plug() is not None else "" + nodeName = batch.plug().node().relativeName( scriptNode ) if batch.plug() is not None else "root" nodeType = batch.plug().node().typeName() if batch.plug() is not None else "None" batch.blindData()["id"] = ( nodeName + batch.context().hash().toString() ) if batch.context() is not None else IECore.MurmurHash().toString() - frameString = None - if self["includeFrames"].getValue() : - frameString = "-" - frameList = IECore.frameListFromList( [ int( i ) for i in batch.frames() ] ) - if len( frameList.asList() ) > 0 : - if frameList.start == frameList.end : - frameString = str( frameList.start ) - elif frameList.step != 1 : - frameString = "{} - {} x {}".format( frameList.start, frameList.end, frameList.step ) - else : - frameString = "{} - {}".format( frameList.start, frameList.end ) - - contextVariables = {} - - if batch.context() is not None : - scriptContext = scriptNode.context() - matchPattern = self["contextVariables"].getValue() - for entry in [ k for k in batch.context().keys() if k != "frame" and not k.startswith( "ui:" ) and not k.startswith( "dispatcher:" ) ] : - if IECore.StringAlgo.matchMultiple( entry, matchPattern ) and ( entry not in scriptContext.keys() or batch.context()[entry] != scriptContext[entry] ) : - contextVariables[entry] = batch.context().substitute( str( batch.context()[entry] ) ) - - batchString = '\t"{}" [label="{}\n\t\t{}'.format( batch.blindData()["id"], nodeName, nodeType ) - if frameString is not None : - batchString += "\n\t\t{}".format( frameString ) - if len( contextVariables ) > 0 : - batchString += "\n\t\t" + "\n\t\t".join( - [ "{} = {}".format( k, v ) for k, v in contextVariables.items() ] - ) - - batchString += '"];\n' - outFile.write( batchString ) + if nodeName == "root" : + outFile.write( "\t{} [shape=oval label=<{}>];\n".format( batch.blindData()["id"], nodeName ) ) + else : + + frameString = None + if self["includeFrames"].getValue() : + frameString = "-" + frameList = IECore.frameListFromList( [ int( i ) for i in batch.frames() ] ) + if len( frameList.asList() ) > 0 : + if frameList.start == frameList.end : + frameString = str( frameList.start ) + elif frameList.step != 1 : + frameString = "{} - {} x {}".format( frameList.start, frameList.end, frameList.step ) + else : + frameString = "{} - {}".format( frameList.start, frameList.end ) + + contextVariables = {} + + if batch.context() is not None : + scriptContext = scriptNode.context() + matchPattern = self["contextVariables"].getValue() + for entry in [ k for k in batch.context().keys() if k != "frame" and not k.startswith( "ui:" ) and not k.startswith( "dispatcher:" ) ] : + if IECore.StringAlgo.matchMultiple( entry, matchPattern ) and ( entry not in scriptContext.keys() or batch.context()[entry] != scriptContext[entry] ) : + contextVariables[entry] = batch.context().substitute( str( batch.context()[entry] ) ) + + batchString = '' + batchString += ''.format( nodeName ) + batchString += ''.format( nodeType ) + + + if frameString is not None : + batchString += "".format( frameString ) + if len( contextVariables ) > 0 : + batchString += "" + batchString += "' + + batchString += '
{}
{}
Frames:{}
Context Variables" + batchString += "".join( + [ "{} = {}
".format( k, v ) for k, v in contextVariables.items() ] + ) + batchString += '
' + + outFile.write( '\t"{}" [label=<{}>];\n'.format( batch.blindData()["id"], batchString ) ) for preTask in batch.preTasks() : self.__walkBatches( preTask, outFile, scriptNode ) @@ -108,6 +120,7 @@ def _doDispatch( self, rootBatch ) : with open( fileName, "w" ) as outFile : outFile.write( "strict digraph {} {{\n".format( jobName ) ) + outFile.write( "\tnode [shape=plaintext];\n" ) self.__walkBatches( rootBatch, outFile, scriptNode ) outFile.write( "}" )