Skip to content

Commit 64f1c65

Browse files
authored
Merge pull request #2888 from alicevision/dev/expressionVars
[core] Refactoring variable management and expression evaluation for performances
2 parents 1b2d5e2 + c95c257 commit 64f1c65

File tree

7 files changed

+128
-86
lines changed

7 files changed

+128
-86
lines changed

meshroom/core/attribute.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ def _getEvalValue(self):
131131
env = self.node.nodePlugin.configFullEnv if self.node.nodePlugin else os.environ
132132
substituted = Template(self.value).safe_substitute(env)
133133
try:
134-
varResolved = substituted.format(**self.node._cmdVars)
134+
varResolved = substituted.format(**self.node._expVars, **self.node._staticExpVars)
135135
return varResolved
136136
except (KeyError, IndexError):
137137
# Catch KeyErrors and IndexErros to be able to open files created prior to the
138-
# support of relative variables (when self.node._cmdVars was not used to evaluate
138+
# support of relative variables (when self.node._expVars was not used to evaluate
139139
# expressions in the attribute)
140140
return substituted
141141
return self.value
@@ -194,7 +194,12 @@ def _applyExpr(self):
194194
elif self.isInput and Attribute.isLinkExpression(v):
195195
# value is a link to another attribute
196196
link = v[1:-1]
197-
linkNodeName, linkAttrName = link.split('.')
197+
linkNodeName, linkAttrName = "", ""
198+
try:
199+
linkNodeName, linkAttrName = link.split('.')
200+
except ValueError as err:
201+
logging.warning('Retrieve Connected Attribute from Expression failed.')
202+
logging.warning(f'Expression: "{link}"\nError: "{err}".')
198203
try:
199204
node = g.node(linkNodeName)
200205
if not node:

meshroom/core/desc/node.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,13 @@ def getMrNodeType(self):
300300
return MrNodeType.COMMANDLINE
301301

302302
def buildCommandLine(self, chunk) -> str:
303+
cmdLineVars = chunk.node.createCmdLineVars()
303304
cmdPrefix = chunk.node.nodeDesc.plugin.commandPrefix
304305
cmdSuffix = chunk.node.nodeDesc.plugin.commandSuffix
305306
if chunk.node.isParallelized and chunk.node.size > 1:
306307
cmdSuffix = " " + self.commandLineRange.format(**chunk.range.toDict()) + " " + cmdSuffix
307308

308-
return cmdPrefix + chunk.node.nodeDesc.commandLine.format(**chunk.node._cmdVars) + cmdSuffix
309+
return cmdPrefix + chunk.node.nodeDesc.commandLine.format(**chunk.node._expVars, **chunk.node._staticExpVars, **cmdLineVars) + cmdSuffix
309310

310311
def processChunk(self, chunk):
311312
cmd = self.buildCommandLine(chunk)

meshroom/core/graphIO.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ def serializeNode(self, node: Node) -> dict:
160160

161161
del nodeData["outputs"]
162162
del nodeData["uid"]
163-
del nodeData["internalFolder"]
164163
del nodeData["parallelization"]
165164

166165
return nodeData

meshroom/core/node.py

Lines changed: 92 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -409,25 +409,24 @@ def updateStatusFromCache(self):
409409
@property
410410
def statusFile(self):
411411
if self.range.blockSize == 0:
412-
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "status")
412+
return os.path.join(self.node.internalFolder, "status")
413413
else:
414-
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder,
414+
return os.path.join(self.node.internalFolder,
415415
str(self.index) + ".status")
416416

417417
@property
418418
def statisticsFile(self):
419419
if self.range.blockSize == 0:
420-
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "statistics")
420+
return os.path.join(self.node.internalFolder, "statistics")
421421
else:
422-
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder,
423-
str(self.index) + ".statistics")
422+
return os.path.join(self.node.internalFolder, str(self.index) + ".statistics")
424423

425424
@property
426425
def logFile(self):
427426
if self.range.blockSize == 0:
428-
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "log")
427+
return os.path.join(self.node.internalFolder, "log")
429428
else:
430-
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder,
429+
return os.path.join(self.node.internalFolder,
431430
str(self.index) + ".log")
432431

433432
def saveStatusFile(self):
@@ -674,14 +673,15 @@ def __init__(self, nodeType: str, position: Position = None, parent: BaseObject
674673
self.packageVersion: str = ""
675674
self._internalFolder: str = ""
676675
self._sourceCodeFolder: str = ""
676+
self._internalFolderExp = "{cache}/{nodeType}/{uid}"
677677

678678
# temporary unique name for this node
679679
self._name: str = f"_{nodeType}_{uuid.uuid1()}"
680680
self.graph = None
681681
self.dirty: bool = True # whether this node's outputs must be re-evaluated on next Graph update
682682
self._chunks = ListModel(parent=self)
683683
self._uid: str = uid
684-
self._cmdVars: dict = {}
684+
self._expVars: dict = {}
685685
self._size: int = 0
686686
self._logManager: Optional[LogManager] = None
687687
self._position: Position = position or Position()
@@ -695,6 +695,11 @@ def __init__(self, nodeType: str, position: Position = None, parent: BaseObject
695695

696696
self.globalStatusChanged.connect(self.updateDuplicatesStatusAndLocked)
697697

698+
self._staticExpVars = {
699+
"nodeType": self.nodeType,
700+
"nodeSourceCodeFolder": self.sourceCodeFolder
701+
}
702+
698703
def __getattr__(self, k):
699704
try:
700705
# Throws exception if not in prototype chain
@@ -913,7 +918,7 @@ def minDepth(self):
913918

914919
@property
915920
def valuesFile(self):
916-
return os.path.join(self.graph.cacheDir, self.internalFolder, 'values')
921+
return os.path.join(self.internalFolder, 'values')
917922

918923
def getInputNodes(self, recursive, dependenciesOnly):
919924
return self.graph.getInputNodes(self, recursive=recursive,
@@ -954,53 +959,48 @@ def _computeUid(self):
954959
uidAttributes.append(self.nodeType)
955960
self._uid = hashValue(uidAttributes)
956961

957-
def _buildCmdVars(self):
962+
def _computeInternalFolder(self, cacheDir):
963+
self._internalFolder = self._internalFolderExp.format(
964+
cache=cacheDir or self.graph.cacheDir,
965+
nodeType=self.nodeType,
966+
uid=self._uid)
967+
968+
def _buildExpVars(self):
958969
"""
959970
Generate command variables using input attributes and resolved output attributes
960971
names and values.
961972
"""
962-
def _buildAttributeCmdVars(cmdVars, name, attr):
973+
def _buildAttributeExpVars(expVars, name, attr):
963974
if attr.enabled:
964-
group = attr.desc.group(attr.node) \
965-
if callable(attr.desc.group) else attr.desc.group
966-
if group is not None:
967-
# If there is a valid command line "group"
968-
v = attr.getValueStr(withQuotes=True)
969-
cmdVars[name] = f"--{name} {v}"
970-
# xxValue is exposed without quotes to allow to compose expressions
971-
cmdVars[name + "Value"] = attr.getValueStr(withQuotes=False)
975+
# xxValue is exposed without quotes to allow to compose expressions
976+
expVars[name + "Value"] = attr.getValueStr(withQuotes=False)
972977

973-
# List elements may give a fully empty string and will not be sent to the command line.
974-
# String attributes will return only quotes if it is empty and thus will be send to the command line.
975-
# But a List of string containing 1 element,
976-
# and this element is an empty string will also return quotes and will be sent to the command line.
977-
if v:
978-
cmdVars[group] = cmdVars.get(group, "") + " " + cmdVars[name]
979-
elif isinstance(attr, GroupAttribute):
978+
if isinstance(attr, GroupAttribute):
980979
assert isinstance(attr.value, DictModel)
981980
# If the GroupAttribute is not set in a single command line argument,
982981
# the sub-attributes may need to be exposed individually
983982
for v in attr._value:
984-
_buildAttributeCmdVars(cmdVars, v.name, v)
983+
_buildAttributeExpVars(expVars, v.name, v)
985984

986-
self._cmdVars["uid"] = self._uid
987-
self._cmdVars["nodeCacheFolder"] = self.internalFolder
988-
self._cmdVars["nodeSourceCodeFolder"] = self.sourceCodeFolder
985+
self._expVars = {
986+
"uid": self._uid,
987+
"nodeCacheFolder": self._internalFolder,
988+
}
989989

990990
# Evaluate input params
991991
for name, attr in self._attributes.objects.items():
992992
if attr.isOutput:
993993
continue # skip outputs
994-
_buildAttributeCmdVars(self._cmdVars, name, attr)
994+
_buildAttributeExpVars(self._expVars, name, attr)
995995

996996
# For updating output attributes invalidation values
997-
cmdVarsNoCache = self._cmdVars.copy()
998-
cmdVarsNoCache["cache"] = ""
997+
expVarsNoCache = self._expVars.copy()
998+
expVarsNoCache["cache"] = ""
999999

10001000
# Use "self._internalFolder" instead of "self.internalFolder" because we do not want it to
10011001
# be resolved with the {cache} information ("self.internalFolder" resolves
10021002
# "self._internalFolder")
1003-
cmdVarsNoCache["nodeCacheFolder"] = self._internalFolder.format(**cmdVarsNoCache)
1003+
expVarsNoCache["nodeCacheFolder"] = self._internalFolderExp.format(**expVarsNoCache, **self._staticExpVars)
10041004

10051005
# Evaluate output params
10061006
for name, attr in self._attributes.objects.items():
@@ -1022,8 +1022,8 @@ def _buildAttributeCmdVars(cmdVars, name, attr):
10221022
format(nodeName=self.name, attrName=attr.name))
10231023
if defaultValue is not None:
10241024
try:
1025-
attr.value = defaultValue.format(**self._cmdVars)
1026-
attr._invalidationValue = defaultValue.format(**cmdVarsNoCache)
1025+
attr.value = defaultValue.format(**self._expVars)
1026+
attr._invalidationValue = defaultValue.format(**expVarsNoCache)
10271027
except KeyError as e:
10281028
logging.warning('Invalid expression with missing key on "{nodeName}.{attrName}" with '
10291029
'value "{defaultValue}".\nError: {err}'.
@@ -1035,15 +1035,60 @@ def _buildAttributeCmdVars(cmdVars, name, attr):
10351035
format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue,
10361036
err=str(e)))
10371037

1038+
# xxValue is exposed without quotes to allow to compose expressions
1039+
self._expVars[name + 'Value'] = attr.getValueStr(withQuotes=False)
1040+
1041+
1042+
def createCmdLineVars(self):
1043+
"""
1044+
Generate command variables using input attributes and resolved output attributes
1045+
names and values.
1046+
"""
1047+
def _buildAttributeCmdLineVars(cmdLineVars, name, attr):
1048+
if attr.enabled:
1049+
group = attr.desc.group(attr.node) \
1050+
if callable(attr.desc.group) else attr.desc.group
1051+
if group:
1052+
# If there is a valid command line "group"
1053+
v = attr.getValueStr(withQuotes=True)
1054+
1055+
# List elements may give a fully empty string and will not be sent to the command line.
1056+
# String attributes will return only quotes if it is empty and thus will be send to the command line.
1057+
# But a List of string containing 1 element,
1058+
# and this element is an empty string will also return quotes and will be sent to the command line.
1059+
if v:
1060+
cmdLineVars[group] = cmdLineVars.get(group, "") + f" --{name} {v}"
1061+
elif isinstance(attr, GroupAttribute):
1062+
assert isinstance(attr.value, DictModel)
1063+
# If the GroupAttribute is not set in a single command line argument,
1064+
# the sub-attributes may need to be exposed individually
1065+
for v in attr._value:
1066+
_buildAttributeCmdLineVars(cmdLineVars, v.name, v)
1067+
1068+
cmdLineVars = {}
1069+
1070+
# Evaluate input params
1071+
for name, attr in self._attributes.objects.items():
1072+
if attr.isOutput:
1073+
continue # skip outputs
1074+
_buildAttributeCmdLineVars(cmdLineVars, name, attr)
1075+
1076+
# Evaluate output params
1077+
for name, attr in self._attributes.objects.items():
1078+
if attr.isInput:
1079+
continue # skip inputs
1080+
if not attr.desc.group:
1081+
continue # skip attributes without group
1082+
10381083
v = attr.getValueStr(withQuotes=True)
10391084

1040-
self._cmdVars[name] = f'--{name} {v}'
1041-
# xxValue is exposed without quotes to allow to compose expressions
1042-
self._cmdVars[name + 'Value'] = attr.getValueStr(withQuotes=False)
1085+
if not v:
1086+
continue # skip empty strings
10431087

1044-
if v:
1045-
self._cmdVars[attr.desc.group] = \
1046-
self._cmdVars.get(attr.desc.group, '') + ' ' + self._cmdVars[name]
1088+
cmdLineVars[attr.desc.group] = \
1089+
cmdLineVars.get(attr.desc.group, '') + f' --{name} {v}'
1090+
1091+
return cmdLineVars
10471092

10481093
@property
10491094
def isParallelized(self):
@@ -1285,26 +1330,21 @@ def updateInternals(self, cacheDir=None):
12851330
folder = ''
12861331

12871332
# Update command variables / output attributes
1288-
self._cmdVars = {
1289-
"cache": cacheDir or self.graph.cacheDir,
1290-
"nodeType": self.nodeType,
1291-
"nodeCacheFolder": self._internalFolder,
1292-
"nodeSourceCodeFolder": self.sourceCodeFolder
1293-
}
12941333
self._computeUid()
1295-
self._buildCmdVars()
1334+
self._computeInternalFolder(cacheDir)
1335+
self._buildExpVars()
12961336
if self.nodeDesc:
12971337
self.nodeDesc.postUpdate(self)
12981338
# Notify internal folder change if needed
1299-
if self.internalFolder != folder:
1339+
if self._internalFolder != folder:
13001340
self.internalFolderChanged.emit()
13011341

13021342
def updateInternalAttributes(self):
13031343
self.internalAttributesChanged.emit()
13041344

13051345
@property
13061346
def internalFolder(self):
1307-
return self._internalFolder.format(**self._cmdVars)
1347+
return self._internalFolder
13081348

13091349
@property
13101350
def sourceCodeFolder(self):
@@ -1360,7 +1400,7 @@ def prepareLogger(self, iteration=-1):
13601400
if iteration != -1:
13611401
chunk = self.chunks[iteration]
13621402
logFileName = str(chunk.index) + ".log"
1363-
logFile = os.path.join(self.graph.cacheDir, self.internalFolder, logFileName)
1403+
logFile = os.path.join(self.internalFolder, logFileName)
13641404
# Setup logger
13651405
rootLogger = logging.getLogger()
13661406
self._logManager = LogManager(rootLogger, logFile)
@@ -1776,7 +1816,6 @@ def __init__(self, nodeType, position=None, parent=None, uid=None, **kwargs):
17761816

17771817
self.packageName = self.nodeDesc.packageName
17781818
self.packageVersion = self.nodeDesc.packageVersion
1779-
self._internalFolder = "{cache}/{nodeType}/{uid}"
17801819
self._sourceCodeFolder = self.nodeDesc.sourceCodeFolder
17811820

17821821
for attrDesc in self.nodeDesc.inputs:
@@ -1863,7 +1902,6 @@ def toDict(self):
18631902
'split': self.nbParallelizationBlocks
18641903
},
18651904
'uid': self._uid,
1866-
'internalFolder': self._internalFolder,
18671905
'inputs': {k: v for k, v in inputs.items() if v is not None}, # filter empty values
18681906
'internalInputs': {k: v for k, v in internalInputs.items() if v is not None},
18691907
'outputs': outputs,
@@ -1926,7 +1964,6 @@ def __init__(self, nodeType, nodeDict, position=None, issue=CompatibilityIssue.U
19261964
self._inputs = self.nodeDict.get("inputs", {})
19271965
self._internalInputs = self.nodeDict.get("internalInputs", {})
19281966
self.outputs = self.nodeDict.get("outputs", {})
1929-
self._internalFolder = self.nodeDict.get("internalFolder", "")
19301967
self._uid = self.nodeDict.get("uid", None)
19311968

19321969
# Restore parallelization settings

meshroom/core/nodeFactory.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ def __init__(
5151
self.internalInputs = self.nodeData.get("internalInputs", {})
5252
self.outputs = self.nodeData.get("outputs", {})
5353
self.version = self.nodeData.get("version", None)
54-
self.internalFolder = self.nodeData.get("internalFolder")
5554
self.position = Position(*self.nodeData.get("position", []))
5655
self.uid = self.nodeData.get("uid", None)
5756
self.nodeDesc = None
@@ -202,8 +201,8 @@ def _tryUpgradeCompatibilityNode(self, node: CompatibilityNode) -> Union[Node, C
202201
logging.warning(f"Compatibility issue in template: performing automatic upgrade on '{self.name}'")
203202
return node.upgrade()
204203

205-
# Backward compatibility: "internalFolder" was not serialized.
206-
if not self.internalFolder:
204+
# Backward compatibility: "uid" was not serialized.
205+
if not self.uid:
207206
logging.warning(f"No serialized output data: performing automatic upgrade on '{self.name}'")
208207
return node.upgrade()
209208

0 commit comments

Comments
 (0)