Utiliser Graphviz pour générer des graphes de dépendance
Par Narann le mercredi, 20 juillet 2011, 21:26 - Infographie 3D - Boulot - Lien permanent
Aujourd'hui je voudrais vous présenter Graphviz, un outil qui génère des graphes depuis des fichiers texte. :seSentCon:
Je m'en suis servi en production pour générer des graphes de dépendance de révision (La révision "machin" du cache "machin" a été généré depuis la révision "machin" du model "machin" et est utilisé dans le set "truc", "bidule" et "bitonio").
Je ne peux bien évidement pas vous montrer les graphes générés ici mais sachez qu'on découvrait pas mal de choses en les regardant... Justement parce que ce genre d'outil permet de visualiser rapidement des choses que notre cerveau a du mal à concevoir tout seul. Dans le cas d'un dataflow un peu compliqué, ça détend les neurones. :nevroz:
(Merci à Adrien Herubel de m'avoir fait découvrir cet outil)
D'autres graphiques impressionnants sur cette magnifique galerie.
Principe
Le principe est très simple: Vous générez un fichier texte contenant les informations du graphe.
Vous donnez ensuite ce fichier à un des outils de Graphviz (principalement dot) qui génère ainsi une image de votre graphe. :hehe:
Un premier exemple, simple
Installer graphviz pour votre OS, lancez un terminal et c'est parti! :grenadelauncher:
Le langage dot est très simple et il s'apprend quasiment uniquement par la pratique.
Voici un exemple que je vais détailler:
digraph g { graph [ rankdir = LR bgcolor= grey50 ] node [ fontsize = "10" shape = box style = "rounded,filled" ] myNode1 [ label = "Node One" color = lightsalmon fillcolor = grey75 ] myNode2 [ label = "Node Two" color = lightcoral fillcolor = grey75 ] myNode3 [ label = "Node Three" color = lightpink fillcolor = grey75 ] myNode4 [ label = "Node Four" color = orange fillcolor = grey75 ] myNode1 -> myNode2 [ penwidth = 1, fontsize = 8, label = "Connection One" ] myNode2 -> myNode3 [ penwidth = 1, fontsize = 8, label = "Connection Two" ] myNode1 -> myNode4 [ penwidth = 1, fontsize = 8, label = "Connection Three" ] subgraph cluster_sg1 { label = "subGraph One" style = rounded color = springgreen4 myNode1 myNode4 } subgraph cluster_sg2 { label = "subGraph Two" style = rounded color = violetred3 myNode2 myNode3 } }
Copiez tout ça dans un fichier monFichier.dot puis faites:
dot -Tpng monFichier.dot -o monGraphe.png
Ici, -Tpng sert à dire à dot de créer un graph au format png. (Il y en a d'autres comme -Tsvg. La liste complète est disponible dans la doc de dot, bas de la première page).
Une fois généré, vous devriez avoir ça:
Notez qu'on a pas besoin de réfléchir au positionnement des nodes. On déclare, les nodes, les connexions et il génère le graphe. Cette "puissance" peut être bloquante quand justement, on essaye d'organiser un peu les graphes qu'on sort... :reflechi:
Et maintenant les explications.
Vous allez voir que Graphviz peut être très flexible quand vous décrivez vos graphes. Il n'y a pas une seule façon de déclarer un graphe, d’où l’intérêt d’expérimenter un maximum. :idee:
C'est aussi la porte ouverte au n'importe quoi. C'est à vous de faire attention. :papi:
digraph g { ... }
Il ne doit y en avoir qu'un seul et c'est celui qui contient votre graphe. C'est un peu la racine.
:longBar:
graph [ rankdir = LR bgcolor= grey50 ]
Contient tout les attributs par défaut du graphe (cf dotguide, page 33).
- rankdir: Permet de définir le sens du graphe (ici, Left to Right)
- bgcolor: La couleur du fond du graphe, à choisir parmi les nombreuses couleurs disponibles.
:longBar:
node [ fontsize = "10" shape = box style = "rounded,filled" ]
Contient tout les attributs par défaut des nodes (cf dotguide, page 30).
- fontsize: La taille de la police de caractère utilisée pour les nodes.
- shape: La forme des nodes (cf dotguide, page 38).
- style: La "mise en page" des nodes. Ici, bords arrondis (j'aime bien :seSentCon: ) et bg coloré.
:longBar:
myNode1 [ label = "Node One" color = lightsalmon fillcolor = grey75 ]
S'en suit maintenant la description de chaque node.
Par défaut, les valeurs des attributs sont celles définies dans la déclaration node, juste au dessus.
Ensuite, chaque paramètre vient s'ajouter:
- label: Ce qui est écrit dans le node (nous verrons plus loin quand on souhaite faire de la mise en forme plus complexe)
- color: La couleur des bords du node, à choisir parmi les nombreuses couleurs disponibles.
- fillcolor: La couleur du fond du node.
On déclare plusieurs nodes comme ça puis on attaque les connections.
:longBar:
myNode1 -> myNode2 [ penwidth = 1, fontsize = 8, label = "Connection One" ]
Les attributs et leurs valeurs par défauts sont dispos sur le dotguide, page 32.
- penwidth: La taille du tracé de la connexion (Inutile dans ce cas là car, comme vous pouvez le constater dans la doc, la taille par défaut est déjà à 1). :baffed:
- fontsize: La taille du texte de la connexion.
- label: Ce qui est écrit dans la connection (notez que j’écris rarement un texte sur une connexion).
:longBar:
Viennent ensuite les subgraphs qui sont tout à fait optionnel mais peuvent servir à donner un peu d'ordre à vos graphes:
subgraph cluster_sg1 { label = "subGraph One" style = rounded color = springgreen4 myNode1 myNode4 }
- label: Le nom du subgraph qui s'affichera.
- style: Le "style" du subgraph (comme les nodes).
- color: La couleur des contours du subgraph.
Viennent ensuite la liste des nodes contenu dans le subgraph.
Important: Les identifiants des subgraphs doivent toujours commencer par "cluster" sinon ils ne s'afficheront pas. Je m'en suis fait des bosses à me taper la tête contre les murs avec ça. :aupoil:
Nous avons donc fini avec le premier exemple. Nous allons maintenant prendre un cas plus concret. :enerve:
Un graphe de dépendance de révision
Comme je ne m'en suis servi que dans le cadre d'une production CG, je placerai mon exemple dans ce contexte. Peut être existe t'il mille façons d'utiliser dot et que la mienne est bien la dernière, mais après tout, c'est ce qui nous intéresse non? :dentcasse:
Nous allons prendre l'exemple d'un graphe de dépendance de révision. L'avantage de ce genre de graph, c'est qu'il est souvent de type tree.
Avant propos, Python est ton amis
Je vais passer brièvement sur le "comment" de la génération des fichiers .dot.
Pour générer un tel fichier, je me suis servi des classes en Python auquel je rajoutais des attributs.
Chaque nœud du graphe est en fait un objet Python de type RevNode contenant tous ses attributs ainsi qu'une liste de tous ses enfants.
L'idée est de créer une liste de RevNode, chacun ayant ces propres attributs et les autres Nodes auxquels il se connecte.
Bien entendu, vous récupérez automatiquement ces informations dans la base de donnée de votre pipeline. :sourit:
Vous partez donc d'une révision que vous choisissez, puis vous la parcourez en arbre via une fonction récursive.
class RevNode() : def __init__( self ) : self.nodeName = "" # Ici, un identifiant unique du node, c'est à vous de voir self.assetName = "" # "chaussette" self.assetType = "" # anim, model, etc... self.rev = "" # "Last", "Validated", "5", etc... self.childrenList = [] # List de tout les RevNode enfant de celui ci def getAllRevNode( self ) : """Renvoit ce node plus ces enfants""" revNodeList = [] revNodeList.append( self ) # on ajoute ce node for revNode in self.childrenList : revNodeList += revNode.getAllAssetNode() return assetNodeList
Arrivé à ce stade, vous avez une liste de RevNode.
Il faut ensuite générer le fichier .dot contenant tout ça. Nous allons voir que l'on peut "formater" le contenu d'un node de manière plus personnalisé grâce à une possibilité intéressante de dot qui permet de lui donner un label qui sera interprété en HTML (cf dotguide, page 9).
HTML dans dot
Il faut préciser à dot que le label que vous lui donnez devra être interprété en HTML.
Pour ça, rien de plus simple, il faut simplement placer le contenu HTML entre < ... > :
label = < maDescriptionHTML >
Le plus dur étant de justement avoir de l'HTML valide. :sourit:
Un exemple:
<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> <TR><TD ALIGN="right" ><FONT COLOR="deeppink">Name:</FONT></TD><TD ALIGN="left" >Chausset</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Model</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD ALIGN="left" ><TD>5</TD></TR> </TABLE>
Mettez tout ça entre < ... >.
Expérimentez! :sourit:
Let's go!
Voici un graphe qui pourrait ressembler à un graphe de dépendance:
digraph g { graph [ rankdir = RL bgcolor= grey50 ] node [ fontsize = "10" shape = box style = "rounded,filled" ] edge [ fontsize = "8" ] myNodeSet1 [ label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Name:</FONT></TD><TD ALIGN="left" >Etagere</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Set</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >7</TD></TR> </TABLE>> color = blue4 fillcolor = deepskyblue1 ] myNodeModel1 [ label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> <TR><TD ALIGN="right" ><FONT COLOR="darkgreen">Name:</FONT></TD><TD ALIGN="left" >Chaussette</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Model</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >5</TD></TR> </TABLE>> color = darkgreen fillcolor = darkolivegreen4 ] myNodeCache1 [ label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> <TR><TD ALIGN="right" ><FONT COLOR="deeppink">Name:</FONT></TD><TD ALIGN="left" >Chaussette</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Cache</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >5</TD></TR> </TABLE>> color = violetred3 fillcolor = coral ] myNodeModel2 [ label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> <TR><TD ALIGN="right" ><FONT COLOR="darkgreen">Name:</FONT></TD><TD ALIGN="left" >Chemise</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Model</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >12</TD></TR> </TABLE>> color = darkgreen fillcolor = darkolivegreen4 ] myNodeCache2 [ label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> <TR><TD ALIGN="right" ><FONT COLOR="deeppink">Name:</FONT></TD><TD ALIGN="left" >Chemise</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Cache</TD></TR> <TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >12</TD></TR> </TABLE>> color = violetred3 fillcolor = coral ] myNodeModel1 -> myNodeCache1 myNodeModel2 -> myNodeCache2 myNodeCache1 -> myNodeSet1 [label = "used in"] myNodeModel2 -> myNodeSet1 [label = "used in"] subgraph cluster_model1 { label = "Models" style = "rounded, filled" color = darkgreen fillcolor = darkolivegreen3 myNodeModel1 myNodeModel2 } subgraph cluster_cache1 { label = "Caches" style = "rounded, filled" color = violetred3 fillcolor = darksalmon myNodeCache1 myNodeCache2 } }
Le résultat:
L'objectif est donc de faire un script python capable de générer un tel fichier, de l’exécuter à la volé et de l'afficher à l'utilisateur. :jdicajdirien:
Ce graphe est minuscule. Ça commence à devenir intéressant quand les informations abondent (dans le cas d'une "vraie" prod, les infos, il en pleut à torrent! :hehe: ).
Liens
N'oubliez pas de passer en revue les exemples proposés dans la galerie qui sont tous disponibles avec leur code dot.
Le guide contient aussi pas mal d'infos.
Sans oublier internet qui regorge d'exemples. :hehe:
Conclusion
Et voilà! J'espère que ça vous aura donné envie d'essayer de faire des graphes quand la situation l'exige. :laClasse:
Je pense qu'on ne se rend compte de l’intérêt d'un tel truc qu'une fois qu'on en a vu un qui correspond à nos propres besoins.
A bientôt!
Dorian
Commentaires
C'est intéressant, mais j'ai du mal à voir l'intégration dans un pipe...
Si j'ai bien compris tu l'utilises un peu comme "InK" (asset manager chez MacGuff).
Comment font les utilisateurs pour ajouter des nodes et créer des liens entre ceux ci?
C'est les différent softs "maya,nuke..." qui modifient la base de donnée à la création des scènes?
Non, pas comme InK ^^.
Hehe, c'est le premier truc auquel on pense: Éditer le bousin.
En fait non, ce n'est pas éditable du tout. :(
Ce billet avait pour simple intérêt de présenter un moyen (très utilisé) de faire des graphs: Graphviz.
Ça peut être des graphs de n'importe quoi mais quand on a un gros paquet de donne interconnecte entre eux, ça aide beaucoup!.
Faire un éditeur de graph, ça ne se fait pas en quelque lignes. L’intérêt d'un tel système dans le cas d'un soft comme InK est assez limitée (complètement inutile) en effet.
Personnellement, j'ai eu a l'utiliser sur une prod ou il était très difficile de tracker les dépendances des assets (genre l'anim du plan "trucmuche" a été fait sur le set "machin" lui même généré en utilisant la version "x" du prop "bidule", ce qui explique le décalage quand le perso "toto" prend le prop "bidule" justement...).