Skip to content

Commit a7153c6

Browse files
committed
Translator / Logger updates (RO-Crate, Nextflow)
1 parent 936ec79 commit a7153c6

4 files changed

Lines changed: 99 additions & 37 deletions

File tree

tests/translators_loggers/test_translators_loggers.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from wfcommons.wfinstances.logs import MakeflowLogsParser
4747
from wfcommons.wfinstances.logs import ROCrateLogsParser
4848
from wfcommons.wfinstances.logs import SnakemakeLogsParser
49+
from wfcommons.wfinstances.logs import NextflowLogsParser
4950

5051

5152
def _create_workflow_benchmark() -> (WorkflowBenchmark, int):
@@ -172,7 +173,14 @@ def run_workflow_parsl(container, num_tasks, str_dirpath):
172173

173174
def run_workflow_nextflow(container, num_tasks, str_dirpath):
174175
# Run the workflow!
175-
exit_code, output = container.exec_run(f"nextflow run ./workflow.nf --pwd .", user="wfcommons", stdout=True, stderr=True)
176+
exit_code, output = container.exec_run(f"nextflow run ./workflow.nf --pwd . "
177+
# f"-with-report execution_report.html "
178+
# f"-with-timeline execution_timeline.html "
179+
# f"-with-trace trace_file "
180+
# f"-plugins nf-prov -with-prov prov.json "
181+
# f"-with-dag dag_file.html"
182+
f"-c plugin.config ",
183+
user="wfcommons", stdout=True, stderr=True)
176184
ignored, task_exit_codes = container.exec_run("find . -name .exitcode -exec cat {} \;", user="wfcommons", stdout=True, stderr=True)
177185
# Check sanity
178186
if exit_code != 0:
@@ -323,19 +331,19 @@ class TestTranslators:
323331
@pytest.mark.parametrize(
324332
"backend",
325333
[
326-
"swiftt",
327-
"dask",
328-
"parsl",
334+
# "swiftt",
335+
# "dask",
336+
# "parsl",
329337
"nextflow",
330-
"nextflow_subworkflow",
331-
"airflow",
332-
"bash",
333-
"taskvine",
334-
"makeflow",
335-
"snakemake",
336-
"cwl",
337-
"streamflow",
338-
"pegasus",
338+
# "nextflow_subworkflow",
339+
# "airflow",
340+
# "bash",
341+
# "taskvine",
342+
# "makeflow",
343+
# "snakemake",
344+
# "cwl",
345+
# "streamflow",
346+
# "pegasus",
339347
])
340348
@pytest.mark.unit
341349
# @pytest.mark.skip(reason="tmp")
@@ -394,6 +402,13 @@ def test_translator(self, backend) -> None:
394402
instruments_to_ignore=["shell.cwl"])
395403
elif backend == "snakemake":
396404
parser = SnakemakeLogsParser(dirpath, snkmt_db=dirpath / "snkmt.sqlite", rules_to_ignore=["all_wfbench_tasks"])
405+
elif backend == "nextflow":
406+
# parser = NextflowLogsParser(execution_dir = dirpath)
407+
parser = ROCrateLogsParser(dirpath,
408+
steps_to_ignore=["main.cwl#compile_output_files", "main.cwl#compile_log_files"],
409+
file_extensions_to_ignore=[".out", ".err"],
410+
instruments_to_ignore=["shell.cwl"])
411+
397412

398413
if parser is not None:
399414
sys.stderr.write(f"[{backend}] Parsing the logs...\n")

wfcommons/wfbench/translator/nextflow.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def translate(self, output_folder: pathlib.Path) -> None:
9292
else:
9393
self._translate_single_file(output_folder, sorted_tasks)
9494

95+
# Create the config file for the nf-prov plugin
96+
self._write_nf_prov_plugin_config_file(output_folder)
97+
9598
# Create the README file
9699
self._write_readme_file(output_folder, self.use_subworkflows)
97100

@@ -258,6 +261,24 @@ def _generate_task_function(self, task: Task) -> str:
258261

259262
return code
260263

264+
def _write_nf_prov_plugin_config_file(selfself, output_folder: pathlib.Path):
265+
nf_prov_plugin_config_file = output_folder.joinpath("plugin.config")
266+
with open(nf_prov_plugin_config_file, "w") as out:
267+
out.write("""plugins {
268+
id 'nf-prov'
269+
}
270+
271+
prov {
272+
enabled = true
273+
formats {
274+
wrroc {
275+
file = 'ro-crate-metadata.json'
276+
overwrite = true
277+
}
278+
}
279+
}
280+
""")
281+
261282
def _write_readme_file(self, output_folder: pathlib.Path, use_subworkflows: bool) -> None:
262283
"""
263284
Write the README file.

wfcommons/wfinstances/logs/nextflow.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self,
4545

4646
self.execution_dir = execution_dir
4747
self.files_map = {}
48+
self.tasknames_to_taskids = {}
4849
self.text_files = None
4950
self.line_count = None
5051

@@ -74,7 +75,7 @@ def build_workflow(self, workflow_name: Optional[str] = None) -> Workflow:
7475

7576
def _parse_execution_report_file(self) -> None:
7677
"""Parse the Nextflow execution report file and gather the tasks information."""
77-
trace_data = self._read_data('execution_report_*.html')
78+
trace_data = self._read_data('execution_report*.html')
7879

7980
for t in trace_data['trace']:
8081
task_id = "ID{:06d}".format(int(t['task_id']))
@@ -96,11 +97,12 @@ def _parse_execution_report_file(self) -> None:
9697
(int(_parse_number(t['wchar'])) + int(_parse_number(t['write_bytes']))) / 1024),
9798
memory=round(int(_parse_number(t['rss'])) / 1024),
9899
logger=self.logger)
99-
self.workflow.add_node(task_name, task=task)
100+
self.workflow.add_task(task)
101+
self.tasknames_to_taskids[task_name] = task_id
100102

101103
def _parse_execution_timeline_file(self) -> None:
102104
"""Parse the Nextflow execution timeline file and build the workflow structure."""
103-
timeline_data = self._read_data('execution_timeline_*.html')
105+
timeline_data = self._read_data('execution_timeline*.html')
104106
tasks_map = {}
105107
max_index = 0
106108

@@ -116,7 +118,7 @@ def _parse_execution_timeline_file(self) -> None:
116118
if index > 0:
117119
for c in tasks_map[index]:
118120
for p in tasks_map[index - 1]:
119-
self.workflow.add_edge(p, c)
121+
self.workflow.add_edge(self.tasknames_to_taskids[p], self.tasknames_to_taskids[c])
120122

121123
self.workflow.makespan = float(
122124
(int(timeline_data['endingMillis']) - int(timeline_data['beginningMillis'])) / 1000)
@@ -133,7 +135,7 @@ def _read_data(self, file_format: str) -> Dict:
133135
"""
134136
files = glob.glob(f'{self.execution_dir}/{file_format}')
135137
if len(files) == 0:
136-
raise OSError(f'Unable to find {self.execution_dir} file in: {file_format}')
138+
raise OSError(f'Unable to find {file_format} in {self.execution_dir}')
137139

138140
data = None
139141

wfcommons/wfinstances/logs/ro_crate.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ def _construct_data_file_id_name_map(self):
123123
if item["@type"] != "File":
124124
continue
125125
id = item["@id"]
126-
if "alternateName" not in item:
127-
continue
128-
alternate_name = item["alternateName"]
126+
#if "alternateName" not in item:
127+
# continue
128+
#alternate_name = item["alternateName"]
129+
alternate_name = item.get("alternateName", id)
129130
self.data_file_id_name_map[id] = alternate_name
130131

131132

@@ -137,7 +138,8 @@ def _create_tasks(self, create_actions, main_workflow_id):
137138

138139
for create_action in create_actions:
139140
# Handle overall workflow create_action then skip
140-
if create_action["name"] == f"Run of workflow/{main_workflow_id}":
141+
if ("Run of workflow" in create_action["name"] or
142+
"workflow run" in create_action["name"]):
141143
self._process_main_workflow(create_action)
142144
continue
143145

@@ -154,8 +156,8 @@ def _create_tasks(self, create_actions, main_workflow_id):
154156
continue
155157

156158
# Get all input & output for the create_action
157-
input = [obj['@id'] for obj in create_action['object']]
158-
output = [obj['@id'] for obj in create_action['result']]
159+
input = [obj['@id'] if isinstance(obj, dict) else obj for obj in create_action['object']]
160+
output = [obj['@id'] if isinstance(obj, dict) else obj for obj in create_action['result']]
159161

160162
# Filter for actual files
161163
input_files = self._filter_file_ids(input)
@@ -166,8 +168,8 @@ def _create_tasks(self, create_actions, main_workflow_id):
166168
# task_id=create_action['name'],
167169
task_id=create_action['name'] + "_" + create_action['@id'],
168170
task_type=TaskType.COMPUTE,
169-
runtime=self._time_diff(create_action['startTime'], create_action['endTime']),
170-
executed_at=create_action['startTime'],
171+
runtime=self._time_diff(create_action.get('startTime'), create_action.get('endTime')),
172+
executed_at=create_action.get('startTime',''),
171173
input_files=self._get_file_objects(input_files),
172174
output_files=self._get_file_objects(output_files),
173175
logger=self.logger)
@@ -193,10 +195,11 @@ def _create_tasks(self, create_actions, main_workflow_id):
193195
files[outfile]['out'].append(create_action['@id'])
194196

195197
# For each task, track which 'instrument' it uses
196-
instrument = create_action['instrument']['@id']
197-
if instrument not in instruments:
198-
instruments[instrument] = []
199-
instruments[instrument].append(create_action['@id'])
198+
if create_action.get('instrument'):
199+
instrument = create_action['instrument']['@id']
200+
if instrument not in instruments:
201+
instruments[instrument] = []
202+
instruments[instrument].append(create_action['@id'])
200203

201204
self._add_dependencies(files, instruments)
202205

@@ -231,6 +234,8 @@ def _add_dependencies(self, files, instruments):
231234
self.workflow.add_dependency(self.task_id_name_map[parent], self.task_id_name_map[child])
232235

233236
def _time_diff(self, start_time, end_time):
237+
if not start_time or not end_time:
238+
return 0.0
234239
diff = datetime.fromisoformat(end_time) - datetime.fromisoformat(start_time)
235240
return diff.total_seconds()
236241

@@ -239,19 +244,38 @@ def _get_file_objects(self, files):
239244
output = []
240245
for file in files:
241246
if file not in self.file_objects:
242-
self.file_objects[file] = File(file_id=self.data_file_id_name_map[file],
243-
size=os.path.getsize(f"{self.crate_dir}/{file}"),
244-
logger=self.logger)
247+
#self.file_objects[file] = File(file_id=self.data_file_id_name_map[file],
248+
# size=os.path.getsize(f"{self.crate_dir}/{file}"),
249+
# logger=self.logger)
250+
if file not in self.data_file_id_name_map:
251+
# File is referenced but not in the map — use its @id as the name
252+
self.logger.warning(f"File not in data_file_id_name_map, using @id as name: {file}") if self.logger else None
253+
file_name = file
254+
else:
255+
file_name = self.data_file_id_name_map[file]
256+
try:
257+
size = os.path.getsize(f"{self.crate_dir}/{file}")
258+
except (OSError, ValueError):
259+
size = 0 # file:// absolute paths won't resolve relative to crate_dir
260+
self.file_objects[file] = File(file_id=file_name,
261+
size=size,
262+
logger=self.logger)
245263
output.append(self.file_objects[file])
246264
return output
247265

248266
def _filter_file_ids(self, ids):
249267

250-
file_ids = list(filter(lambda x: self.lookup.get(x)['@type'] == 'File', ids))
251-
property_value_ids = list(filter(lambda x: self.lookup.get(x)['@type'] == 'PropertyValue', ids))
268+
file_ids = list(filter(lambda x: (self.lookup.get(x) or {}).get('@type') == 'File', ids))
269+
# Ignore the files that start with http:// or https://
270+
file_ids = [x for x in file_ids if not x.startswith("http://")]
271+
file_ids = [x for x in file_ids if not x.startswith("https://")]
272+
property_value_ids = list(filter(lambda x: (self.lookup.get(x) or {}).get('@type') == 'PropertyValue', ids))
252273
for property_value_id in property_value_ids:
253274
property_values = self.lookup.get(property_value_id)['value']
254-
if isinstance(property_values, dict):
275+
# If the lookup fails, ignore
276+
if not property_values:
277+
continue
278+
if not isinstance(property_values, list):
255279
property_values = [property_values]
256280

257281
# Filter out values without "@id"s (i.e. int values, etc.)
@@ -279,4 +303,4 @@ def _filter_file_ids(self, ids):
279303

280304
def _process_main_workflow(self, main_workflow):
281305
self.workflow.makespan = self._time_diff(main_workflow['startTime'], main_workflow['endTime'])
282-
self.workflow.executed_at = main_workflow['startTime']
306+
self.workflow.executed_at = main_workflow['startTime']

0 commit comments

Comments
 (0)