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..978df5b83ae --- /dev/null +++ b/python/GafferDispatchTest/GraphvizDispatcher.py @@ -0,0 +1,175 @@ +########################################################################## +# +# 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 "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() + + 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 ) + + 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 ) ) + outFile.write( "\tnode [shape=plaintext];\n" ) + 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