Skip to content

Commit bfdae41

Browse files
committed
add metric value validation script
Signed-off-by: Harper, Jason M <[email protected]>
1 parent daa6d4d commit bfdae41

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed

scripts/compare_metrics.py

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2021-2025 Intel Corporation
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
5+
"""
6+
Compare metric values between two CSV files.
7+
Used to compare EMON system summary metrics with PerfSpect system summary metrics.
8+
"""
9+
10+
import sys
11+
import csv
12+
import argparse
13+
from typing import Dict, List, Tuple
14+
15+
16+
def read_csv(filepath: str) -> tuple[Dict[str, Dict[str, str]], List[str]]:
17+
"""Read CSV file and return dict of metric name -> metric data, and list of metric names in order."""
18+
metrics = {}
19+
metric_order = []
20+
with open(filepath, 'r') as f:
21+
reader = csv.DictReader(f)
22+
for row in reader:
23+
# Skip empty rows
24+
if not row:
25+
continue
26+
27+
# Try multiple column name patterns
28+
# Handle cases like "(Metric post processor 5.18.0) name (sample #1 - #304)"
29+
name = None
30+
for key in row.keys():
31+
if 'name' in key.lower() or 'metric' in key.lower():
32+
name = row[key]
33+
break
34+
35+
if not name:
36+
continue
37+
38+
# Remove 'metric_' prefix if present
39+
if name.startswith('metric_'):
40+
name = name[7:] # Remove 'metric_' prefix
41+
metrics[name] = row
42+
metric_order.append(name)
43+
return metrics, metric_order
44+
45+
46+
def parse_value(value_str: str) -> float:
47+
"""Parse a numeric value from string, handling scientific notation."""
48+
try:
49+
return float(value_str)
50+
except (ValueError, TypeError):
51+
return None
52+
53+
54+
def calculate_percent_difference(val1: float, val2: float) -> float:
55+
"""Calculate percent difference: ((val2 - val1) / val1) * 100."""
56+
if val1 == 0:
57+
if val2 == 0:
58+
return 0.0
59+
return float('inf')
60+
return ((val2 - val1) / val1) * 100
61+
62+
63+
def categorize_difference(pct_diff: float) -> Tuple[str, str]:
64+
"""Categorize the percent difference and return (status, symbol)."""
65+
abs_diff = abs(pct_diff)
66+
if abs_diff < 5:
67+
return ("Excellent", "✓")
68+
elif abs_diff < 10:
69+
return ("Good", "✓")
70+
elif abs_diff < 25:
71+
return ("Moderate", "")
72+
elif abs_diff < 50:
73+
return ("Large", "⚠️")
74+
else:
75+
return ("Critical", "⚠️")
76+
77+
78+
def print_header():
79+
"""Print the header section."""
80+
print("=" * 120)
81+
print("METRIC COMPARISON ANALYSIS")
82+
print("=" * 120)
83+
print()
84+
85+
86+
def print_comparison_table(comparisons: List[Tuple], file1_name: str, file2_name: str):
87+
"""Print detailed comparison table."""
88+
print(f"\n{'Metric':<60} {'EMON':>15} {'PerfSpect':>15} {'% Diff':>10} {'Status':>12}")
89+
print("-" * 120)
90+
91+
# Print in the order they appear in comparisons (preserves CSV order)
92+
for metric_name, val1, val2, pct_diff, status, symbol in comparisons:
93+
val1_str = f"{val1:.6g}" if val1 is not None else "N/A"
94+
val2_str = f"{val2:.6g}" if val2 is not None else "N/A"
95+
96+
if pct_diff == float('inf'):
97+
pct_str = "INF"
98+
elif pct_diff is not None:
99+
pct_str = f"{pct_diff:+.1f}%"
100+
else:
101+
pct_str = "N/A"
102+
103+
status_str = f"{symbol} {status}" if symbol else status
104+
print(f"{metric_name:<60} {val1_str:>15} {val2_str:>15} {pct_str:>10} {status_str:>12}")
105+
106+
107+
def print_summary_statistics(comparisons: List[Tuple]):
108+
"""Print summary statistics of the comparison."""
109+
print("\n" + "=" * 120)
110+
print("SUMMARY STATISTICS")
111+
print("=" * 120)
112+
113+
valid_diffs = [abs(pct) for _, _, _, pct, _, _ in comparisons if pct is not None and pct != float('inf')]
114+
115+
if not valid_diffs:
116+
print("No valid comparisons found.")
117+
return
118+
119+
# Count by category
120+
excellent = sum(1 for d in valid_diffs if d < 5)
121+
good = sum(1 for d in valid_diffs if 5 <= d < 10)
122+
moderate = sum(1 for d in valid_diffs if 10 <= d < 25)
123+
large = sum(1 for d in valid_diffs if 25 <= d < 50)
124+
critical = sum(1 for d in valid_diffs if d >= 50)
125+
126+
total = len(valid_diffs)
127+
128+
print(f"\nTotal metrics compared: {total}")
129+
print(f"\nAverage absolute difference: {sum(valid_diffs) / len(valid_diffs):.2f}%")
130+
print(f"Median absolute difference: {sorted(valid_diffs)[len(valid_diffs)//2]:.2f}%")
131+
print(f"Max absolute difference: {max(valid_diffs):.2f}%")
132+
print(f"Min absolute difference: {min(valid_diffs):.2f}%")
133+
134+
print("\nDistribution by category:")
135+
print(f" ✓ Excellent (<5%): {excellent:3d} ({100*excellent/total:5.1f}%)")
136+
print(f" ✓ Good (5-10%): {good:3d} ({100*good/total:5.1f}%)")
137+
print(f" Moderate (10-25%): {moderate:3d} ({100*moderate/total:5.1f}%)")
138+
print(f" ⚠️ Large (25-50%): {large:3d} ({100*large/total:5.1f}%)")
139+
print(f" ⚠️ Critical (>50%): {critical:3d} ({100*critical/total:5.1f}%)")
140+
141+
142+
def print_critical_discrepancies(comparisons: List[Tuple]):
143+
"""Print list of critical discrepancies."""
144+
critical = [(name, val1, val2, pct) for name, val1, val2, pct, status, _ in comparisons
145+
if pct is not None and pct != float('inf') and abs(pct) >= 50]
146+
147+
if not critical:
148+
print("\n✓ No critical discrepancies found (all metrics within 50%)")
149+
return
150+
151+
print("\n" + "=" * 120)
152+
print("CRITICAL DISCREPANCIES (>50% difference)")
153+
print("=" * 120)
154+
155+
for name, val1, val2, pct in sorted(critical, key=lambda x: abs(x[3]), reverse=True):
156+
print(f" • {name}")
157+
print(f" EMON: {val1:.6g} | PerfSpect: {val2:.6g} | Difference: {pct:+.1f}%")
158+
159+
160+
def check_tma_metrics(metrics1: Dict, metrics2: Dict, file1_name: str, file2_name: str, order2: List[str]):
161+
"""Check Top-down Microarchitecture Analysis metrics and their sum."""
162+
# Try different naming patterns
163+
tma_patterns = [
164+
["Frontend_Bound(%)", "Bad_Speculation(%)", "Backend_Bound(%)", "Retiring(%)"],
165+
["TMA_Frontend_Bound(%)", "TMA_Bad_Speculation(%)", "TMA_Backend_Bound(%)", "TMA_Retiring(%)"]
166+
]
167+
168+
tma_names = None
169+
for pattern in tma_patterns:
170+
if any(name in metrics1 or name in metrics2 for name in pattern):
171+
tma_names = pattern
172+
break
173+
174+
if not tma_names:
175+
return
176+
177+
# Order TMA metrics based on their appearance in file2 (summary CSV)
178+
tma_names_ordered = [name for name in order2 if name in tma_names]
179+
180+
print("\n" + "=" * 120)
181+
print("TOP-DOWN MICROARCHITECTURE ANALYSIS (TMA)")
182+
print("=" * 120)
183+
184+
sum1 = 0.0
185+
sum2 = 0.0
186+
comparisons = []
187+
188+
# Use ordered list if available, otherwise fall back to tma_names
189+
names_to_process = tma_names_ordered if tma_names_ordered else tma_names
190+
191+
for name in names_to_process:
192+
val1 = parse_value(metrics1.get(name, {}).get('aggregated', '')) if name in metrics1 else None
193+
val2 = parse_value(metrics2.get(name, {}).get('mean', '')) if name in metrics2 else None
194+
195+
if val1 is not None:
196+
sum1 += val1
197+
if val2 is not None:
198+
sum2 += val2
199+
200+
if val1 is not None and val2 is not None:
201+
pct_diff = calculate_percent_difference(val1, val2)
202+
status, symbol = categorize_difference(pct_diff)
203+
# Clean up metric name for display
204+
display_name = name.replace("TMA_", "").replace("(%)", "")
205+
comparisons.append((display_name, val1, val2, pct_diff, status, symbol))
206+
207+
if comparisons:
208+
print(f"\n{'TMA Metric':<40} {'EMON':>15} {'PerfSpect':>15} {'% Diff':>10} {'Status':>12}")
209+
print("-" * 120)
210+
211+
for name, val1, val2, pct_diff, status, symbol in comparisons:
212+
status_str = f"{symbol} {status}" if symbol else status
213+
print(f"{name:<40} {val1:>15.2f} {val2:>15.2f} {pct_diff:>+9.1f}% {status_str:>12}")
214+
215+
print("-" * 120)
216+
print(f"{'Sum':<40} {sum1:>15.2f} {sum2:>15.2f}")
217+
218+
if abs(sum1 - 100.0) > 0.1:
219+
print(f"\n⚠️ Warning: EMON TMA sum is {sum1:.2f}% (should be ~100%)")
220+
if abs(sum2 - 100.0) > 0.1:
221+
print(f"⚠️ Warning: PerfSpect TMA sum is {sum2:.2f}% (should be ~100%)")
222+
223+
224+
def main():
225+
parser = argparse.ArgumentParser(
226+
description='Compare metric values between two CSV files',
227+
formatter_class=argparse.RawDescriptionHelpFormatter,
228+
epilog='''
229+
Example:
230+
%(prog)s __mpp_system_view_summary.csv gnr_metrics_summary.csv
231+
'''
232+
)
233+
parser.add_argument('file1', help='EMON CSV file (e.g., __mpp_system_view_summary.csv)')
234+
parser.add_argument('file2', help='Perfspect CSV file (e.g., gnr_metrics_summary.csv)')
235+
236+
args = parser.parse_args()
237+
238+
try:
239+
metrics1, order1 = read_csv(args.file1)
240+
metrics2, order2 = read_csv(args.file2)
241+
except FileNotFoundError as e:
242+
print(f"Error: {e}", file=sys.stderr)
243+
return 1
244+
except Exception as e:
245+
print(f"Error reading CSV files: {e}", file=sys.stderr)
246+
return 1
247+
248+
# Find common metrics
249+
common_metrics = set(metrics1.keys()) & set(metrics2.keys())
250+
251+
if not common_metrics:
252+
print("Error: No common metrics found between the two files.", file=sys.stderr)
253+
return 1
254+
255+
# Determine which columns to use for comparison
256+
# Look for columns containing numeric values (aggregated, mean, avg, etc.)
257+
sample_row1 = list(metrics1.values())[0]
258+
sample_row2 = list(metrics2.values())[0]
259+
260+
# Find the value column in file1
261+
col1 = None
262+
for col_name in ['aggregated', 'mean', 'avg', 'average', 'value']:
263+
if col_name in sample_row1:
264+
col1 = col_name
265+
break
266+
if not col1:
267+
# Try to find any column with numeric data
268+
for key, val in sample_row1.items():
269+
if key.lower() not in ['name', 'metric', 'description', 'min', 'max', 'stddev', 'stdev', 'variation']:
270+
try:
271+
float(val)
272+
col1 = key
273+
break
274+
except (ValueError, TypeError):
275+
continue
276+
277+
# Find the value column in file2
278+
col2 = None
279+
for col_name in ['mean', 'aggregated', 'avg', 'average', 'value']:
280+
if col_name in sample_row2:
281+
col2 = col_name
282+
break
283+
if not col2:
284+
# Try to find any column with numeric data
285+
for key, val in sample_row2.items():
286+
if key.lower() not in ['name', 'metric', 'description', 'min', 'max', 'stddev', 'stdev', 'variation']:
287+
try:
288+
float(val)
289+
col2 = key
290+
break
291+
except (ValueError, TypeError):
292+
continue
293+
294+
if not col1 or not col2:
295+
print(f"Error: Could not determine value columns. File1 columns: {list(sample_row1.keys())}, File2 columns: {list(sample_row2.keys())}", file=sys.stderr)
296+
return 1
297+
298+
# Use file2's order (typically the summary file) to preserve metric ordering
299+
# This ensures metrics appear in the same order as in the summary CSV
300+
ordered_common_metrics = [m for m in order2 if m in common_metrics]
301+
302+
# Collect comparisons in the order they appear in file2
303+
comparisons = []
304+
for metric_name in ordered_common_metrics:
305+
val1 = parse_value(metrics1[metric_name].get(col1, ''))
306+
val2 = parse_value(metrics2[metric_name].get(col2, ''))
307+
308+
if val1 is not None and val2 is not None:
309+
pct_diff = calculate_percent_difference(val1, val2)
310+
status, symbol = categorize_difference(pct_diff)
311+
comparisons.append((metric_name, val1, val2, pct_diff, status, symbol))
312+
313+
# Print results
314+
print_header()
315+
print(f"File 1: {args.file1} (using '{col1}' column)")
316+
print(f"File 2: {args.file2} (using '{col2}' column)")
317+
print(f"Common metrics found: {len(comparisons)}")
318+
319+
print_comparison_table(comparisons, "EMON", "PerfSpect")
320+
321+
print_summary_statistics(comparisons)
322+
print_critical_discrepancies(comparisons)
323+
check_tma_metrics(metrics1, metrics2, "EMON", "PerfSpect", order2)
324+
325+
print("\n" + "=" * 120)
326+
327+
return 0
328+
329+
330+
if __name__ == '__main__':
331+
sys.exit(main())

0 commit comments

Comments
 (0)