C'est quoi le problème?

Rien de mieux qu'un cas concret pour énoncer la chose:

Sur la fin de Lorax, il y avait un grand nombre de "compeurs" (compositeurs) qui rendaient leurs images en même temps et les estimations affichées par Nuke étaient parfois surprenantes (3h-4h restantes pour des compos simples). En surveillant un fichier en train de s’écrire, je me suis rendu compte qu'il montait très lentement. Il était à 100ko, puis passait à 300ko, puis au bout de dix secondes passait à 400ko etc... J'en ai conclu que le réseau était surchargé... :reflexionIntense:

Je me suis rappelé que nous avions eu le même soucis sur Les Contes De La Nuit. Notre infrastructure était, certes, beaucoup plus modeste mais 3 After Effect en rendu plombaient l’intégralité du réseau. La solution trouvé à ce moment la fut de rendre en local puis de copier les fichier une fois finis. Les effets furent immédiats: Le réseau ne ramait plus. :sauteJoie:

J'ai donc tenté de rendre le compo d'un graphiste en local pour voir si cela diminuait le problème. Les plus gros fichiers à lires étant les fichiers sources, car plus nombreux, et l'image rendu étant relativement légère, je ne me faisais pas trop d'illusions. Et pourtant, une fois de plus, le constat est sans appel: Le rendu se finissait en 10-15 minutes (je ne rigole pas) au lieu de 3-4h... :laClasse:

Je me suis dis que c’était l’écriture qui devait poser problème. Pourtant, quand je copiais la séquence d'image fraichement calculée, la copie était très rapide. Je suis donc passé voir les graphistes les uns après les autres et en un gros après midi, le réseau était complètement désengorgé et les rendus passèrent tous.

Mais ce n’était pas une solution. Il fallait comprendre pourquoi Nuke n'arrivait pas à écrire ces images rapidement sur le réseau. Après avoir posé la question sur la mailing list nuke-users (ou j'ai pu constater que je n’étais pas seul mais que The Foundry semblait ne rien pouvoir faire), j'ai commencé à "profiler" Nuke pour savoir comment ils s'y prenait (strace et inotifywatch sont tes amis :dentcasse: ).

Les conclusions semblent évidentes mais il est toujours bon de vérifier par la pratique ce qu'on soupçonne:

Sous Nuke, si vous écrivez un EXR compressé zip 1 line, en 1920x1080, Nuke fera un peu moins de 900 (et des brouettes) accès en écriture sur le fichier. Si vous êtes en zip 16 lines, il fera environs 70 accès (1080/16). Et en non compressé c'est réellement 1080 accès. :trollface:

Dans les fait, compresser en zip 16 line n'est pas super efficace si les images doivent être lus par Nuke. Et suivant l'infrastructure de votre réseau, écrire ligne à ligne peut le mettre complètement à plat. Il est difficile d'expliquer comment finalement peu de rendus Nuke sont nécessaires pour plomber un réseau, même si ce dernier est costaud. J'ai la sensation que c'est lié au multithreading: Nuke lit des images (souvent beaucoup en même temps) sur le réseau pendant qu'il y écrit.

La solution la plus évidente reste donc d’écrire l'image (les images) rendu sur le disque local et de la copier en une fois (un seul accès) sur le disque réseau. Si vous ne disposez pas des ressources techniques nécessaires ou bien tout simplement de temps, c'est l'approche la plus simple mais sur des projets plus conséquents ça peut rapidement devenir rébarbatif et (ne l'oublions pas) source d'erreurs.

Il y a plusieurs solutions et je m’étais penché sur un prototype que je trouvais intéressant car facile à mettre en place.

problems.jpg

Le principe

  • Vous lancez un thread python qui va surveiller un dossier.
  • Toute les trois secondes le thread va lister les fichiers présents dans le dossier et voir si leur nom correspond à une expression régulière (le "modèle" de votre nom de fichier).
  • Si le fichier semble être un fichier que vous souhaitez déplacer une fois fini, il cherche ce même fichier avec ".finished" (exemple: "toto.001.exr.finished").
  • Si ce fichier existe, il déplace le fichier d'origine et supprime son équivalent en ".finished" puis relance la boucle principale.
  • Une fois le rendu fini, vous donner l'ordre au thread de s’arrêter.

Comme vous pouvez le voir, ce système nécessite que vous créiez un fichier ".finished" à chaque fois qu'une image est fini. C'est dû au fait qu'il est impossible pour le thread de savoir à quel moment une image est fini de calculer et complète (et ducoup, quand il peut le lancer la copie). La création de ce ".finished" peut être géré de mille façons différentes (Pour Maya, un "Post render frame" fait l'affaire) donc je ne rentre pas dans les détails. :siffle:

Le code

Voici le code brut de décoffrage:

import os, threading, re, time
 
class MoverThread( threading.Thread ) :
 
	def __init__( self, dirTocheck, dirToMoveIn, patternToCheck, force=False ) :
		threading.Thread.__init__( self )
 
		self._terminate = False
		self.dirTocheck = dirTocheck
		self.dirToMoveIn = dirToMoveIn
		self.force = force
 
		# regex pattern
		self.patternToCheck = patternToCheck
		self.rePattern = re.compile( patternToCheck )
 
		# sanity check
		if not os.path.isdir(self.dirTocheck) :
			raise Exception( "The given directory (dirTocheck) is not a valid directory -> %s" %  self.dirTocheck )
 
		if not os.path.isdir(self.dirToMoveIn) :
			raise Exception( "The given directory (dirToMoveIn) is not a valid directory -> %s" %  self.dirToMoveIn )
 
	def run( self ) :
 
		filesNotMoved = []
 
		while not self._terminate :
 
			# we wait 3 seconds before do anything
			time.sleep( 3 )
 
			# for every "entry" (file or folder) in the folder we check it have the good pattern. If it has, we check for a ".finished" file
			for entry in os.listdir( self.dirTocheck ) :
 
				# check the current entry is "compliant" with the given regex
				if not self.rePattern.match( entry ) :
					continue
 
				srcFilePath = os.path.join( self.dirTocheck, entry )
				dstFilePath = os.path.join( self.dirToMoveIn, entry )
 
				if os.path.isfile( srcFilePath+".finished" ) :
 
					# destination file aready exist?
					if os.path.isfile( dstFilePath ) and not self.force:
 
						# don't add the entry if it is already in the list
						if not entry in filesNotMoved :
							filesNotMoved.append( entry )
 
						continue
 
					# move the file to it new location
					os.rename( srcFilePath, dstFilePath )
					os.remove( srcFilePath+".finished" )
 
					print "File %s moved to %s" % ( entry, self.dirToMoveIn )
 
					break	# restart the while loop to avoid to continue the list of file we maybe have removed: ".finished"
 
		print "Terminated!"
 
		for fileNotMoved in filesNotMoved :
			print "Already exists: Can't move %s to %s" % ( fileNotMoved, self.dirToMoveIn )
 
 
 
	def join( self ) :
 
		self._terminate = True
 
		threading.Thread.join( self )

Comme vous pouvez le voir (ou pas), tout ce passe dans une thread.

Ca s'utilise comme ça:

import waitFinishAndCopy
myMoverThread = waitFinishAndCopy.MoverThread("/a/local/path/", "/a/network/path/", "^toto\.[0-9]{4}\.exr$")
myMoverThread.start()
# start rendering, do rendering, end rendering.
myMoverThread.join()

Et voila!

Conclusion

J’espère que ce modeste prototype vous inspirera si vous rencontrez des lenteurs sur votre réseau. :mechantCrash:

Je vous conseille de faire un peu de profilage réseau sur vos principales applications, surtout si elles sont utilisées par beaucoup de monde. Leurs comportements est toujours intéressant (et parfois surprenant).

Passez une bonne journée!

Dorian

:marioCours: