Skip to content

Commit 8221b7b

Browse files
authored
Merge pull request #148 from seqeralabs/remove-seqera-predicted-cost
fix(benchmark-report): drop task cost fallback
2 parents 57f5c99 + ed4654d commit 8221b7b

6 files changed

Lines changed: 305 additions & 18 deletions

File tree

modules/local/aggregate_benchmark_report_data/bin/benchmark_report_aggregate.py

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,37 @@ def _lookup_cost(
6262
return None
6363

6464

65-
def _cost_or_task(cost_row: dict[str, Any] | None, key: str, task_cost: float, default: float = 0.0) -> float:
65+
def _cost_or_task(cost_row: dict[str, Any] | None, key: str, default: float = 0.0) -> float:
6666
if not cost_row:
67-
return task_cost if key in {"cost", "used_cost"} else default
67+
return default
6868

6969
value = cost_row.get(key)
7070
if value is None:
71-
return task_cost if key in {"cost", "used_cost"} else default
71+
return default
7272

7373
return float(value)
7474

7575

76+
def _summarize_missing_processes(
77+
missing_process_counts: dict[str, int], preview_limit: int = 3
78+
) -> tuple[list[dict[str, Any]], str]:
79+
ordered = sorted(missing_process_counts.items(), key=lambda item: (-item[1], item[0]))
80+
preview = [
81+
{"process_short": process_short, "missing_tasks": missing_tasks}
82+
for process_short, missing_tasks in ordered[:preview_limit]
83+
]
84+
parts = [
85+
f"{item['process_short']} ({item['missing_tasks']})"
86+
if item["missing_tasks"] != 1
87+
else item["process_short"]
88+
for item in preview
89+
]
90+
hidden_count = max(len(ordered) - preview_limit, 0)
91+
if hidden_count:
92+
parts.append(f"+{hidden_count} more")
93+
return preview, ", ".join(parts)
94+
95+
7696
def _is_highlight_process(process: str) -> bool:
7797
process_lc = process.lower()
7898
return any(keyword in process_lc for keyword in _HIGHLIGHT_KEYWORDS)
@@ -240,9 +260,11 @@ def build_report_data(jsonl_dir: Path, include_failed_runs: bool = False) -> dic
240260
"unused_cost": 0.0,
241261
}
242262

263+
costs_jsonl_path = jsonl_dir / "costs.jsonl"
264+
cur_supplied = costs_jsonl_path.exists()
243265
costs_index: dict[tuple[str, str, str], dict[str, Any]] = {}
244266
has_cost_rows = False
245-
for c in _iter_jsonl(jsonl_dir / "costs.jsonl"):
267+
for c in _iter_jsonl(costs_jsonl_path):
246268
has_cost_rows = True
247269
run_id = str(c.get("run_id", ""))
248270
process = str(c.get("process", ""))
@@ -289,6 +311,10 @@ def build_report_data(jsonl_dir: Path, include_failed_runs: bool = False) -> dic
289311
task_run_acc: dict[str, dict[str, float]] = defaultdict(
290312
lambda: {"requested_cpu_h": 0.0, "requested_mem_gib_h": 0.0, "real_cpu_h": 0.0, "real_mem_gib_h": 0.0}
291313
)
314+
cost_coverage_runs: dict[tuple[str, str], dict[str, Any]] = {}
315+
total_cost_tasks = 0
316+
matched_cost_tasks = 0
317+
missing_cost_tasks = 0
292318

293319
for t in _iter_jsonl(jsonl_dir / "tasks.jsonl"):
294320
run_id = str(t.get("run_id", ""))
@@ -311,21 +337,40 @@ def build_report_data(jsonl_dir: Path, include_failed_runs: bool = False) -> dic
311337
}
312338

313339
cost_row = _lookup_cost(costs_index, run_id=run_id, process=process, process_short=process_short, hash_short=hash_short)
314-
task_cost = float(t.get("cost") or 0.0)
340+
341+
if cur_supplied:
342+
total_cost_tasks += 1
343+
coverage = cost_coverage_runs.setdefault(
344+
run_group_key,
345+
{
346+
"run_id": run_id,
347+
"group": group,
348+
"total_tasks": 0,
349+
"matched_tasks": 0,
350+
"missing_tasks": 0,
351+
"missing_process_counts": defaultdict(int),
352+
},
353+
)
354+
coverage["total_tasks"] += 1
355+
if cost_row:
356+
matched_cost_tasks += 1
357+
coverage["matched_tasks"] += 1
358+
else:
359+
missing_cost_tasks += 1
360+
coverage["missing_tasks"] += 1
361+
missing_process = process_short or process or "unknown"
362+
coverage["missing_process_counts"][missing_process] += 1
315363

316364
if cost_row:
317-
run_cost_acc[run_group_key]["cost"] += _cost_or_task(cost_row, "cost", task_cost)
318-
run_cost_acc[run_group_key]["used_cost"] += _cost_or_task(cost_row, "used_cost", task_cost)
319-
run_cost_acc[run_group_key]["unused_cost"] += _cost_or_task(cost_row, "unused_cost", task_cost, default=0.0)
320-
else:
321-
run_cost_acc[run_group_key]["cost"] += task_cost
322-
run_cost_acc[run_group_key]["used_cost"] += task_cost
365+
run_cost_acc[run_group_key]["cost"] += _cost_or_task(cost_row, "cost")
366+
run_cost_acc[run_group_key]["used_cost"] += _cost_or_task(cost_row, "used_cost")
367+
run_cost_acc[run_group_key]["unused_cost"] += _cost_or_task(cost_row, "unused_cost")
323368

324369
if has_cost_rows:
325370
overview_key = (group, process_short)
326-
cost_group_acc[overview_key]["total_cost"] += _cost_or_task(cost_row, "cost", task_cost)
327-
cost_group_acc[overview_key]["used_cost"] += _cost_or_task(cost_row, "used_cost", task_cost)
328-
cost_group_acc[overview_key]["unused_cost"] += _cost_or_task(cost_row, "unused_cost", task_cost, default=0.0)
371+
cost_group_acc[overview_key]["total_cost"] += _cost_or_task(cost_row, "cost")
372+
cost_group_acc[overview_key]["used_cost"] += _cost_or_task(cost_row, "used_cost")
373+
cost_group_acc[overview_key]["unused_cost"] += _cost_or_task(cost_row, "unused_cost")
329374
cost_group_acc[overview_key]["n_tasks"] += 1
330375

331376
status = t.get("status")
@@ -531,6 +576,34 @@ def build_report_data(jsonl_dir: Path, include_failed_runs: bool = False) -> dic
531576
]
532577
cost_overview.sort(key=lambda x: float(x.get("total_cost") or 0), reverse=True)
533578

579+
runs_with_missing_costs = []
580+
for row in cost_coverage_runs.values():
581+
if int(row["missing_tasks"]) <= 0:
582+
continue
583+
missing_processes, missing_process_summary = _summarize_missing_processes(row["missing_process_counts"])
584+
runs_with_missing_costs.append(
585+
{
586+
"run_id": row["run_id"],
587+
"group": row["group"],
588+
"total_tasks": int(row["total_tasks"]),
589+
"matched_tasks": int(row["matched_tasks"]),
590+
"missing_tasks": int(row["missing_tasks"]),
591+
"missing_processes": missing_processes,
592+
"missing_process_summary": missing_process_summary,
593+
}
594+
)
595+
runs_with_missing_costs.sort(key=lambda row: (-row["missing_tasks"], str(row["group"]), str(row["run_id"])))
596+
597+
cost_coverage = {
598+
"cur_supplied": cur_supplied,
599+
"has_any_cost_rows": has_cost_rows,
600+
"total_included_tasks": total_cost_tasks,
601+
"matched_task_count": matched_cost_tasks,
602+
"missing_task_count": missing_cost_tasks,
603+
"coverage_pct": _round((matched_cost_tasks / total_cost_tasks) * 100.0, 1) if total_cost_tasks else None,
604+
"runs_with_missing_costs": runs_with_missing_costs,
605+
}
606+
534607
combined_task_runtime = []
535608
for (pipeline, group), panel_acc in sorted(combined_runtime_acc.items(), key=lambda x: (x[0][0], x[0][1])):
536609
process_runtime_ms = panel_acc["process_runtime_ms"]
@@ -600,6 +673,7 @@ def build_report_data(jsonl_dir: Path, include_failed_runs: bool = False) -> dic
600673
"task_table": task_table,
601674
"task_scatter": task_scatter,
602675
"cost_overview": cost_overview,
676+
"cost_coverage": cost_coverage,
603677
}
604678

605679

modules/local/aggregate_benchmark_report_data/tests/test_aggregate.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,28 @@ def test_build_report_data_has_all_sections(tmp_path, make_run, flat_task, write
2828
"task_table",
2929
"task_scatter",
3030
"cost_overview",
31+
"cost_coverage",
3132
}
3233

3334

34-
def test_run_costs_without_cur_uses_task_cost(tmp_path, make_run, flat_task, write_run_json):
35+
def test_run_costs_without_cur_are_zero(tmp_path, make_run, flat_task, write_run_json):
3536
data_dir = tmp_path / "data"
3637
jsonl_dir = tmp_path / "jsonl_bundle"
3738
write_run_json(data_dir, [make_run(tasks=[flat_task(cost=4.2)])])
3839
normalize_jsonl(data_dir, jsonl_dir)
3940

4041
data = build_report_data(jsonl_dir)
41-
assert data["run_costs"][0]["cost"] == 4.2
42+
assert data["run_costs"][0]["cost"] == 0.0
4243
assert data["run_costs"][0]["used_cost"] is None
44+
assert data["cost_coverage"] == {
45+
"cur_supplied": False,
46+
"has_any_cost_rows": False,
47+
"total_included_tasks": 0,
48+
"matched_task_count": 0,
49+
"missing_task_count": 0,
50+
"coverage_pct": None,
51+
"runs_with_missing_costs": [],
52+
}
4353

4454

4555
def test_cur_zero_costs_do_not_fall_back_to_task_cost(tmp_path):
@@ -101,6 +111,107 @@ def test_cur_zero_costs_do_not_fall_back_to_task_cost(tmp_path):
101111
assert data["run_costs"][0]["unused_cost"] == 0.0
102112
assert data["cost_overview"][0]["total_cost"] == 0.0
103113
assert data["cost_overview"][0]["used_cost"] == 0.0
114+
assert data["cost_coverage"]["cur_supplied"] is True
115+
assert data["cost_coverage"]["coverage_pct"] == 100.0
116+
assert data["cost_coverage"]["missing_task_count"] == 0
117+
118+
119+
def test_partial_cur_coverage_is_reported_per_run_and_process(tmp_path):
120+
jsonl_dir = tmp_path / "jsonl_bundle"
121+
jsonl_dir.mkdir(parents=True)
122+
123+
runs = [
124+
{
125+
"run_id": "run1",
126+
"group": "cpu",
127+
"pipeline": "pipe",
128+
"username": "u",
129+
"pipeline_version": "main",
130+
"nextflow_version": "24.10.0",
131+
"platform_version": "x",
132+
"succeeded": 2,
133+
"failed": 0,
134+
"cached": 0,
135+
"executor": "awsbatch",
136+
"region": "us-east-1",
137+
"fusion_enabled": False,
138+
"wave_enabled": False,
139+
"container_engine": "docker",
140+
"duration_ms": 10,
141+
"cpu_time_ms": 1000,
142+
"cpu_efficiency": 50.0,
143+
"memory_efficiency": 50.0,
144+
"read_bytes": 0,
145+
"write_bytes": 0,
146+
}
147+
]
148+
tasks = [
149+
{
150+
"run_id": "run1",
151+
"group": "cpu",
152+
"hash": "ab/cdef12",
153+
"process": "foo:PROC_A",
154+
"process_short": "PROC_A",
155+
"name": "PROC_A",
156+
"status": "COMPLETED",
157+
"staging_ms": 0,
158+
"realtime_ms": 1000,
159+
"duration_ms": 1000,
160+
"cost": None,
161+
},
162+
{
163+
"run_id": "run1",
164+
"group": "cpu",
165+
"hash": "ab/cdef13",
166+
"process": "foo:PROC_B",
167+
"process_short": "PROC_B",
168+
"name": "PROC_B",
169+
"status": "COMPLETED",
170+
"staging_ms": 0,
171+
"realtime_ms": 1000,
172+
"duration_ms": 1000,
173+
"cost": None,
174+
},
175+
{
176+
"run_id": "run1",
177+
"group": "cpu",
178+
"hash": "ab/cdef14",
179+
"process": "foo:PROC_B",
180+
"process_short": "PROC_B",
181+
"name": "PROC_B_retry",
182+
"status": "CACHED",
183+
"staging_ms": 0,
184+
"realtime_ms": 1000,
185+
"duration_ms": 1000,
186+
"cost": None,
187+
},
188+
]
189+
costs = [
190+
{"run_id": "run1", "process": "foo:PROC_A", "hash": "abcdef12", "cost": 5.0, "used_cost": 4.0, "unused_cost": 1.0}
191+
]
192+
193+
(jsonl_dir / "runs.jsonl").write_text("".join(json.dumps(r) + "\n" for r in runs))
194+
(jsonl_dir / "tasks.jsonl").write_text("".join(json.dumps(t) + "\n" for t in tasks))
195+
(jsonl_dir / "costs.jsonl").write_text("".join(json.dumps(c) + "\n" for c in costs))
196+
197+
data = build_report_data(jsonl_dir)
198+
199+
assert data["run_costs"][0]["cost"] == 5.0
200+
assert data["cost_coverage"]["cur_supplied"] is True
201+
assert data["cost_coverage"]["has_any_cost_rows"] is True
202+
assert data["cost_coverage"]["total_included_tasks"] == 3
203+
assert data["cost_coverage"]["matched_task_count"] == 1
204+
assert data["cost_coverage"]["missing_task_count"] == 2
205+
assert data["cost_coverage"]["coverage_pct"] == 33.3
206+
207+
run_warning = data["cost_coverage"]["runs_with_missing_costs"][0]
208+
assert run_warning["run_id"] == "run1"
209+
assert run_warning["group"] == "cpu"
210+
assert run_warning["total_tasks"] == 3
211+
assert run_warning["matched_tasks"] == 1
212+
assert run_warning["missing_tasks"] == 2
213+
assert run_warning["missing_process_summary"] == "PROC_B (2)"
214+
assert run_warning["missing_processes"] == [{"process_short": "PROC_B", "missing_tasks": 2}]
104215

105216

106217
def test_task_table_includes_cached(tmp_path, make_run, flat_task, write_run_json):

modules/local/normalize_benchmark_jsonl/bin/benchmark_report_normalize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def extract_tasks(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
179179
"peak_rss": task.get("peakRss", 0),
180180
"read_bytes": task.get("readBytes", 0),
181181
"write_bytes": task.get("writeBytes", 0),
182-
"cost": task.get("cost"),
182+
"cost": None,
183183
"executor": task.get("executor", ""),
184184
"machine_type": task.get("machineType", ""),
185185
"cloud_zone": task.get("cloudZone", ""),

modules/local/normalize_benchmark_jsonl/tests/test_normalize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_cached_count_extracted(make_run, flat_task):
2424
def test_nested_tasks_unwrapped(make_run, nested_task):
2525
run = make_run(tasks=[nested_task(cost=2.0), nested_task(cost=3.0)])
2626
rows = extract_tasks([run])
27-
assert sum(r["cost"] for r in rows) == 5.0
27+
assert all(r["cost"] is None for r in rows)
2828

2929

3030
def test_failed_tasks_filtered(make_run, flat_task):

0 commit comments

Comments
 (0)