Files
rocm-systems/projects/rocprofiler-compute/tools/config_management/generate_config_deltas.py
T
2025-10-22 15:17:43 -04:00

361 lines
13 KiB
Python

#!/usr/bin/env python3
##############################################################################
# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
##############################################################################
"""
Analysis Config Differentiation Script
Generates differences from curr arch directory to prev arch directory.
Output shows what needs to change in prev arch to match curr arch.
"""
from __future__ import annotations
import sys
from pathlib import Path
try:
from . import utils as cm_utils
except Exception:
repo_root = Path(__file__).resolve().parents[1]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
try:
import config_management.utils as cm_utils # type: ignore
except Exception:
import utils as cm_utils # type: ignore
AUTOGEN_TEXT = (
"# AUTOGENERATED FILE. Only edit for testing purposes, not for development. "
"Generated by tools/config_management/generate_config_deltas.py\n"
)
def get_metric_tables(data: dict) -> list[dict]:
"""Extract all metric tables from data source."""
tables: list[dict] = []
for item in data.get("Panel Config", {}).get("data source", []):
mt = item.get("metric_table")
if isinstance(mt, dict):
tables.append(mt)
return tables
def get_metric_descriptions(data: dict) -> dict:
"""Extract metric descriptions from panel config."""
return data.get("Panel Config", {}).get("metrics_description", {}) or {}
def compare_metrics(
prev_metrics: dict, curr_metrics: dict
) -> tuple[list[dict], list[dict], list[dict]]:
"""Compare metrics and return (additions, deletions, modifications)."""
prev_keys = set(prev_metrics.keys())
curr_keys = set(curr_metrics.keys())
additions = [{name: curr_metrics[name]} for name in sorted(curr_keys - prev_keys)]
deletions = [{name: prev_metrics[name]} for name in sorted(prev_keys - curr_keys)]
modifications: list[dict] = []
for name in sorted(prev_keys & curr_keys):
if prev_metrics[name] != curr_metrics[name]:
all_fields = set(prev_metrics[name].keys()) | set(curr_metrics[name].keys())
modified_fields = {
field: curr_metrics[name].get(field)
for field in all_fields
if prev_metrics[name].get(field) != curr_metrics[name].get(field)
}
if modified_fields:
modifications.append({name: modified_fields})
return additions, deletions, modifications
def compare_descriptions(
prev_descriptions: dict, curr_descriptions: dict
) -> tuple[dict, dict, dict]:
"""
Compare metric descriptions and return (additions, deletions, modifications).
Values are dicts with 'plain' and 'rst'.
"""
prev_keys = set(prev_descriptions.keys())
curr_keys = set(curr_descriptions.keys())
additions: dict = {}
deletions: dict = {}
modifications: dict = {}
for name in sorted(curr_keys - prev_keys):
desc = curr_descriptions[name]
additions[name] = (
desc if isinstance(desc, dict) else {"plain": desc, "rst": desc}
)
for name in sorted(prev_keys - curr_keys):
desc = prev_descriptions[name]
deletions[name] = (
desc if isinstance(desc, dict) else {"plain": desc, "rst": desc}
)
for name in sorted(prev_keys & curr_keys):
prev_desc = prev_descriptions[name]
curr_desc = curr_descriptions[name]
prev_plain = (
prev_desc if isinstance(prev_desc, str) else prev_desc.get("plain", "")
)
curr_plain = (
curr_desc if isinstance(curr_desc, str) else curr_desc.get("plain", "")
)
prev_rst = (
prev_desc
if isinstance(prev_desc, str)
else prev_desc.get("rst", prev_plain)
)
curr_rst = (
curr_desc
if isinstance(curr_desc, str)
else curr_desc.get("rst", curr_plain)
)
if prev_plain != curr_plain or prev_rst != curr_rst:
modifications[name] = {"plain": curr_plain, "rst": curr_rst}
return additions, deletions, modifications
def compare_tables(
prev_tables: list[dict], curr_tables: list[dict]
) -> tuple[list[dict], list[dict], list[dict]]:
"""Compare tables and return (additions, deletions, modifications)."""
prev_dict = {t["id"]: t for t in prev_tables}
curr_dict = {t["id"]: t for t in curr_tables}
prev_ids = set(prev_dict.keys())
curr_ids = set(curr_dict.keys())
additions: list[dict] = []
deletions: list[dict] = []
modifications: list[dict] = []
additions.extend(curr_dict[tid] for tid in sorted(curr_ids - prev_ids))
deletions.extend(prev_dict[tid] for tid in sorted(prev_ids - curr_ids))
for tid in sorted(prev_ids & curr_ids):
prev_metrics = prev_dict[tid].get("metric", {}) or {}
curr_metrics = curr_dict[tid].get("metric", {}) or {}
metric_adds, metric_dels, metric_mods = compare_metrics(
prev_metrics, curr_metrics
)
if metric_adds:
additions.append({
"id": tid,
"title": curr_dict[tid].get("title"),
"metrics": metric_adds,
})
if metric_dels:
deletions.append({
"id": tid,
"title": prev_dict[tid].get("title"),
"metrics": metric_dels,
})
if metric_mods:
modifications.append({
"id": tid,
"title": curr_dict[tid].get("title"),
"metrics": metric_mods,
})
return additions, deletions, modifications
def format_metric_fields(metric_data: dict) -> list[str]:
"""Format metric fields as YAML lines."""
lines: list[str] = []
for field_name, field_value in metric_data.items():
if isinstance(field_value, str) and (
"\n" in field_value or len(field_value) > 80
):
lines.append(f" {field_name}: |")
lines.extend(
f" {line}" for line in field_value.split("\n")
)
else:
lines.append(f" {field_name}: {field_value}")
return lines
def format_description_fields(desc_data: dict) -> list[str]:
"""Format description fields as YAML lines."""
lines: list[str] = []
for field_name, field_value in desc_data.items():
if isinstance(field_value, str) and (
"\n" in field_value or len(field_value) > 80
):
lines.append(f" {field_name}: |")
lines.extend(f" {line}" for line in field_value.split("\n"))
else:
lines.append(f" {field_name}: {field_value}")
return lines
def format_output(combined_diff: dict) -> str:
"""Format the diff dictionary into a YAML string."""
lines: list[str] = []
for category in ("Addition", "Deletion", "Modification"):
lines.append(f"{category}:")
if not combined_diff.get(category):
lines.append(" []")
lines.append("")
continue
for panel_item in combined_diff[category]:
pc = panel_item["panel_config"]
lines.extend([
" - Panel Config:",
f" id: {pc['id']}",
f" title: {pc['title']}",
])
if panel_item.get("metric_tables"):
lines.append(" metric_tables:")
for mt in panel_item["metric_tables"]:
lines.extend([
" - metric_table:",
f" id: {mt['id']}",
f" title: {mt['title']}",
" metrics:",
])
metrics_to_format = mt.get("metrics") or [
{name: data} for name, data in (mt.get("metric") or {}).items()
]
for metric in metrics_to_format:
for metric_name, metric_data in metric.items():
lines.append(f" - {metric_name}:")
lines.extend(format_metric_fields(metric_data))
if panel_item.get("metric_descriptions"):
lines.append(" metric_descriptions:")
for metric_name, desc_data in panel_item["metric_descriptions"].items():
lines.append(f" {metric_name}:")
lines.extend(format_description_fields(desc_data))
lines.append("")
return "\n".join(lines)
def main() -> None:
if len(sys.argv) != 3:
print("Usage: python generate_config_deltas.py <curr_arch_dir> <prev_arch_dir>")
sys.exit(1)
curr_arch_dir = Path(sys.argv[1])
prev_arch_dir = Path(sys.argv[2])
if not curr_arch_dir.is_dir() or not prev_arch_dir.is_dir():
print("Error: Both arguments must be directories")
sys.exit(1)
curr_files = {f.name for f in curr_arch_dir.glob("*.yaml")}
prev_files = {f.name for f in prev_arch_dir.glob("*.yaml")}
common_files = curr_files & prev_files
if not common_files:
print("Error: No common YAML files found")
sys.exit(1)
print(f"Comparing {len(common_files)} files...")
combined_diff = {"Addition": [], "Deletion": [], "Modification": []}
for filename in sorted(common_files):
curr_data = cm_utils.load_yaml(curr_arch_dir / filename)
prev_data = cm_utils.load_yaml(prev_arch_dir / filename)
curr_pc = curr_data.get("Panel Config", {}) or {}
prev_pc = prev_data.get("Panel Config", {}) or {}
curr_tables = get_metric_tables(curr_data)
prev_tables = get_metric_tables(prev_data)
curr_descriptions = get_metric_descriptions(curr_data)
prev_descriptions = get_metric_descriptions(prev_data)
table_adds, table_dels, table_mods = compare_tables(prev_tables, curr_tables)
desc_adds, desc_dels, desc_mods = compare_descriptions(
prev_descriptions, curr_descriptions
)
if table_adds or desc_adds:
entry = {
"panel_config": {"id": curr_pc.get("id"), "title": curr_pc.get("title")}
}
if table_adds:
entry["metric_tables"] = table_adds
if desc_adds:
entry["metric_descriptions"] = desc_adds
combined_diff["Addition"].append(entry)
if table_dels or desc_dels:
entry = {
"panel_config": {"id": prev_pc.get("id"), "title": prev_pc.get("title")}
}
if table_dels:
entry["metric_tables"] = table_dels
if desc_dels:
entry["metric_descriptions"] = desc_dels
combined_diff["Deletion"].append(entry)
if table_mods or desc_mods:
entry = {
"panel_config": {"id": curr_pc.get("id"), "title": curr_pc.get("title")}
}
if table_mods:
entry["metric_tables"] = table_mods
if desc_mods:
entry["metric_descriptions"] = desc_mods
combined_diff["Modification"].append(entry)
output = AUTOGEN_TEXT + format_output(combined_diff)
print("\n" + "=" * 80)
print("COMBINED DIFF OUTPUT:")
print("=" * 80)
print(output)
output_dir = prev_arch_dir / "config_delta"
output_dir.mkdir(exist_ok=True)
output_file = output_dir / f"{curr_arch_dir.name}_diff.yaml"
with open(output_file, "w") as f:
f.write(output)
print(f"\nDiff written to: {output_file}")
if __name__ == "__main__":
main()