blob: 179e7a309146673f96634d909e1d474480618117 [file]
#!/usr/bin/env python3
# Copyright (c) 2026 Valve Corporation
# Copyright (c) 2026 LunarG, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import csv
import sys
import argparse
import re
# Regular expression to remove ANSI escape sequences.
ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
def strip_ansi(s):
"""Remove ANSI escape sequences from a string."""
return ansi_escape.sub('', s)
def pad_ansi(s, width):
"""
Pads the string s (which may contain ANSI codes) with spaces on the right so that its
visible length (i.e. after stripping ANSI codes) is at least 'width'.
"""
visible = strip_ansi(s)
pad_count = width - len(visible)
return s + " " * pad_count
def read_overall_data(filename):
"""
Reads the CSV file and returns a dictionary mapping zone names to metrics.
Only rows whose "Zone Name" does not include a parenthesized suffix (e.g., " (top 5%)")
are considered (i.e. the overall row).
The metrics are parsed as:
- Count: int
- Avg (ms), Median (ms), Min (ms), Max (ms): float
"""
data = {}
try:
with open(filename, 'r', newline='') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
zone = row.get("Zone Name", "").strip()
try:
count = int(row.get("Count", "0"))
avg = float(row.get("Avg (ms)", "0"))
median_val = float(row.get("Median (ms)", "0"))
min_val = float(row.get("Min (ms)", "0"))
max_val = float(row.get("Max (ms)", "0"))
except Exception as e:
print(f"Error parsing metrics for zone '{zone}' in file '{filename}': {e}", file=sys.stderr)
continue
data[zone] = {
"Count": count,
"Avg (ms)": avg,
"Median (ms)": median_val,
"Min (ms)": min_val,
"Max (ms)": max_val
}
except FileNotFoundError:
print(f"Error: File '{filename}' not found.", file=sys.stderr)
sys.exit(1)
return data
def format_diff(ref_val, comp_val):
"""
Computes the difference between a reference value and a comparison value.
Returns a string formatted as a signed percentage along with the absolute difference
in milliseconds, e.g.: "+12.34% (+5.67 ms)".
A color gradient is applied so that:
- For positive differences, the text is tinted red, with brighter red for +100% and above.
- For negative differences, the text is tinted green, with brighter green for -100% and below.
- At 0% the text appears white.
If the reference value is zero, returns "N/A".
"""
if ref_val == 0:
return "N/A"
diff = comp_val - ref_val
perc_diff = diff / ref_val * 100.0
# Determine the color gradient.
if perc_diff >= 0:
# Clamp percentage to 100 if above 100.
p = min(perc_diff, 100)
R = 255
# Green and Blue go from 255 at 0% to 0 at 100%
GB = int(255 - (255 * p / 100))
G = GB
B = GB
else:
p = min(abs(perc_diff), 100)
G = 255
# Red and Blue go from 255 at 0% to 0 at -100%
RB = int(255 - (255 * p / 100))
R = RB
B = RB
# ANSI 24-bit color escape sequence.
color_code = f"\033[38;2;{R};{G};{B}m"
reset_code = "\033[0m"
diff_str = f"{perc_diff:+.2f}% ({diff:+.2f} ms)"
return f"{color_code}{diff_str}{reset_code}"
def main(reference_csv, comparison_csv):
# Read overall timing data from both CSV files.
ref_data = read_overall_data(reference_csv)
comp_data = read_overall_data(comparison_csv)
# Determine common zones and zones missing in one file.
common_zones = set(ref_data.keys()) & set(comp_data.keys())
missing_in_comp = set(ref_data.keys()) - common_zones
extra_in_comp = set(comp_data.keys()) - common_zones
# Build rows for common zones.
# Each row is: [Zone Name, Count Diff, Avg Diff, Median Diff, Min Diff, Max Diff]
rows = []
for zone in sorted(common_zones):
ref_metrics = ref_data[zone]
comp_metrics = comp_data[zone]
count_diff = comp_metrics["Count"] - ref_metrics["Count"]
avg_diff = format_diff(ref_metrics["Avg (ms)"], comp_metrics["Avg (ms)"])
median_diff = format_diff(ref_metrics["Median (ms)"], comp_metrics["Median (ms)"])
min_diff = format_diff(ref_metrics["Min (ms)"], comp_metrics["Min (ms)"])
max_diff = format_diff(ref_metrics["Max (ms)"], comp_metrics["Max (ms)"])
rows.append([zone, str(count_diff), avg_diff, median_diff, min_diff, max_diff])
# Define table headers.
headers = ["Zone Name", "Count Diff", "Avg Diff", "Median Diff", "Min Diff", "Max Diff"]
# Compute column widths (based on visible text only).
col_widths = [len(header) for header in headers]
for row in rows:
for i, cell in enumerate(row):
plain_cell = strip_ansi(cell)
col_widths[i] = max(col_widths[i], len(plain_cell))
# Build a header line using the custom pad function.
header_line = " | ".join(pad_ansi(header, col_widths[i]) for i, header in enumerate(headers))
sep_line = "-" * (sum(col_widths) + 3 * (len(col_widths) - 1))
# Print the table header.
print(f"\nZone Timing Comparison ({reference_csv} vs {comparison_csv}):")
print(header_line)
print(sep_line)
# Print each row, padding each cell based on its visible width.
for row in rows:
padded_cells = [pad_ansi(cell, col_widths[i]) for i, cell in enumerate(row)]
print(" | ".join(padded_cells))
# Report zones present in one CSV but not in the other.
if missing_in_comp:
print(f"\nZones present in {reference_csv} but missing in {comparison_csv}:")
for zone in sorted(missing_in_comp):
print(" -", zone)
if extra_in_comp:
print(f"\nZones present in {comparison_csv} but missing in {reference_csv}:")
for zone in sorted(extra_in_comp):
print(" -", zone)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Compare two CSV files of zone timings (overall rows only). The first CSV is used as the reference."
)
parser.add_argument("reference_csv", help="Reference CSV file")
parser.add_argument("comparison_csv", help="CSV file to compare")
args = parser.parse_args()
# Pass the parsed arguments to main as parameters.
main(args.reference_csv, args.comparison_csv)