| |
|
|
| import os |
| import re |
| import sys |
| from typing import Dict, List, Optional, Tuple |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| |
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
|
|
| TIMER_FILE_SUFFIX = "_timer_info.txt" |
|
|
|
|
| |
|
|
| def _read_text(file_path: str) -> str: |
| """Read text file robustly (tolerate weird encodings).""" |
| for enc in ("utf-8", "utf-16", "latin-1"): |
| try: |
| with open(file_path, "r", encoding=enc, errors="strict") as f: |
| return f.read() |
| except Exception: |
| continue |
| |
| with open(file_path, "r", encoding="utf-8", errors="ignore") as f: |
| return f.read() |
|
|
|
|
| def _safe_float(s: Optional[str]) -> float: |
| if s is None: |
| return np.nan |
| try: |
| return float(str(s).replace(",", "").strip()) |
| except Exception: |
| return np.nan |
|
|
|
|
| def _safe_int(s: Optional[str]) -> float: |
| if s is None: |
| return np.nan |
| try: |
| return int(str(s).strip()) |
| except Exception: |
| return np.nan |
|
|
|
|
| def _re1(pattern: str, text: str, flags=re.IGNORECASE) -> Optional[str]: |
| m = re.search(pattern, text, flags=flags) |
| return m.group(1) if m else None |
|
|
|
|
| def _re2(pattern: str, text: str, flags=re.IGNORECASE) -> Tuple[Optional[str], Optional[str]]: |
| m = re.search(pattern, text, flags=flags) |
| return (m.group(1), m.group(2)) if m else (None, None) |
|
|
|
|
| |
|
|
| def parse_timer_file(file_path: str, *, min_step_seconds: float = 1.0, debug: bool = False) -> Dict[str, float]: |
| """ |
| Parse a single *_timer_info.txt file. |
| - Never raises on missing fields; fills NaN instead. |
| - Extracts totals, nodes, JSON sizes, and step times (> min_step_seconds). |
| """ |
| content = _read_text(file_path) |
|
|
| if debug: |
| print(f"\n----- DEBUG: {os.path.basename(file_path)} -----") |
| print(repr(content)) |
|
|
| metrics: Dict[str, float] = {} |
| metrics["file_name"] = os.path.basename(file_path) |
|
|
| |
| w, h = _re2(r"Image Dimensions:\s*(\d+)\s*[xX]\s*(\d+)", content) |
| metrics["width"] = _safe_int(w) |
| metrics["height"] = _safe_int(h) |
|
|
| |
| tt = _re1(r"Total Time:\s*([\d.]+)\s*seconds", content) |
| if tt is None: |
| for line in content.splitlines(): |
| if "Total Time:" in line: |
| m = re.search(r"Total Time:\s*([\d.]+)", line, flags=re.IGNORECASE) |
| if m: |
| tt = m.group(1) |
| break |
| metrics["total_time"] = _safe_float(tt) |
|
|
| |
| metrics["nodes_before"] = _safe_int(_re1(r"Total graph nodes \(before pruning\):\s*(\d+)", content)) |
| metrics["nodes_after"] = _safe_int(_re1(r"Total graph nodes \(after pruning\):\s*(\d+)", content)) |
| metrics["json_size_before_kb"] = _safe_float(_re1(r"JSON File Size \(before pruning\):\s*([\d.,]+)\s*KB", content)) |
| metrics["json_size_after_kb"] = _safe_float(_re1(r"JSON File Size \(after pruning\):\s*([\d.,]+)\s*KB", content)) |
|
|
| |
| step_matches = re.findall(r"([A-Za-z &]+? check):\s*([\d.]+)\s*seconds", content, flags=re.IGNORECASE) |
| for step_name, secs in step_matches: |
| val = _safe_float(secs) |
| if not np.isnan(val) and val > min_step_seconds: |
| col = step_name.strip() |
| metrics[col] = val |
|
|
| return metrics |
|
|
|
|
| |
|
|
| def _plot_image_area_vs_total_time(df: pd.DataFrame, out_dir: str) -> None: |
| data = df.dropna(subset=["total_time", "width", "height"]).copy() |
| if data.empty: |
| return |
| data["image_area"] = data["width"] * data["height"] |
|
|
| plt.figure(figsize=(10, 6)) |
| plt.scatter(data["image_area"], data["total_time"], alpha=0.7) |
| plt.xlabel("Image Area (pixels)", fontsize=13) |
| plt.ylabel("Total Time (seconds)", fontsize=13) |
| plt.title("Image Area vs Total Time", fontsize=15) |
| plt.grid(True, linestyle="--", alpha=0.5) |
| plt.tight_layout() |
| plt.savefig(os.path.join(out_dir, "image_area_vs_total_time.png"), dpi=300) |
| plt.close() |
|
|
|
|
| def _plot_nodes_before_after(df: pd.DataFrame, out_dir: str) -> None: |
| data = df.dropna(subset=["nodes_before", "nodes_after"]).copy() |
| if data.empty: |
| return |
|
|
| plt.figure(figsize=(12, 7)) |
| colors = plt.cm.tab10(np.linspace(0, 1, len(data))) |
| for idx, (_, row) in enumerate(data.iterrows()): |
| c = colors[idx % len(colors)] |
| plt.scatter([idx], [row["nodes_before"]], c=[c], s=90, marker="o") |
| plt.scatter([idx], [row["nodes_after"]], c=[c], s=90, marker="D") |
| plt.xlabel("File Index", fontsize=13) |
| plt.ylabel("Number of Nodes", fontsize=13) |
| plt.title("Nodes Before and After Pruning", fontsize=15) |
| plt.legend( |
| handles=[ |
| plt.Line2D([0], [0], marker="o", color="w", label="Before Pruning", |
| markerfacecolor="black", markersize=9), |
| plt.Line2D([0], [0], marker="D", color="w", label="After Pruning", |
| markerfacecolor="black", markersize=9), |
| ], |
| fontsize=11, loc="upper right", |
| ) |
| plt.grid(True, linestyle="--", alpha=0.5) |
| plt.tight_layout() |
| plt.savefig(os.path.join(out_dir, "nodes_before_after_pruning.png"), dpi=300) |
| plt.close() |
|
|
|
|
| def _plot_json_size_vs_nodes(df: pd.DataFrame, out_dir: str) -> None: |
| data = df.dropna(subset=["nodes_before", "nodes_after", |
| "json_size_before_kb", "json_size_after_kb"]).copy() |
| if data.empty: |
| return |
|
|
| plt.figure(figsize=(12, 7)) |
| colors = plt.cm.tab10(np.linspace(0, 1, len(data))) |
| for idx, (_, row) in enumerate(data.iterrows()): |
| c = colors[idx % len(colors)] |
| plt.scatter(row["nodes_before"], row["json_size_before_kb"], c=[c], s=90, marker="o") |
| plt.scatter(row["nodes_after"], row["json_size_after_kb"], c=[c], s=90, marker="D") |
| plt.xlabel("Number of Nodes", fontsize=13) |
| plt.ylabel("JSON File Size (KB)", fontsize=13) |
| plt.title("JSON File Size vs Number of Nodes", fontsize=15) |
| plt.legend( |
| handles=[ |
| plt.Line2D([0], [0], marker="o", color="w", label="Before Pruning", |
| markerfacecolor="black", markersize=9), |
| plt.Line2D([0], [0], marker="D", color="w", label="After Pruning", |
| markerfacecolor="black", markersize=9), |
| ], |
| fontsize=11, loc="upper right", |
| ) |
| plt.grid(True, linestyle="--", alpha=0.5) |
| plt.tight_layout() |
| plt.savefig(os.path.join(out_dir, "json_size_vs_nodes.png"), dpi=300) |
| plt.close() |
|
|
|
|
| def _plot_average_step_times(df: pd.DataFrame, out_dir: str) -> None: |
| step_cols = [c for c in df.columns if c.lower().endswith("check")] |
| if not step_cols: |
| return |
| step_avg = df[step_cols].mean(numeric_only=True).dropna().sort_values() |
| if step_avg.empty: |
| return |
|
|
| plt.figure(figsize=(10, 6)) |
| bars = plt.bar(step_avg.index, step_avg.values, alpha=0.85) |
| plt.xlabel("Steps", fontsize=13) |
| plt.ylabel("Average Time (seconds)", fontsize=13) |
| plt.title("Average Time for Significant Steps", fontsize=15) |
| plt.xticks(rotation=45, ha="right", fontsize=11) |
| plt.grid(axis="y", linestyle="--", alpha=0.5) |
| for b in bars: |
| h = b.get_height() |
| plt.text(b.get_x() + b.get_width() / 2, h + 0.05, f"{h:.2f}", |
| ha="center", va="bottom", fontsize=9) |
| plt.tight_layout() |
| plt.savefig(os.path.join(out_dir, "average_step_times.png"), dpi=300) |
| plt.close() |
|
|
|
|
| def generate_plots(df: pd.DataFrame, output_dir: str) -> None: |
| """Generate plots when required columns are available.""" |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| if df.empty: |
| print("No data to plot.") |
| return |
|
|
| |
| df_plot = df.dropna(subset=["total_time"]) |
| if df_plot.empty: |
| print("No complete timer files with 'Total Time' found. Skipping time-based plots.") |
| else: |
| _plot_image_area_vs_total_time(df_plot, output_dir) |
| _plot_average_step_times(df_plot, output_dir) |
|
|
| |
| _plot_nodes_before_after(df, output_dir) |
| _plot_json_size_vs_nodes(df, output_dir) |
|
|
|
|
| |
|
|
| def analyze_timer_files( |
| time_dir_txt: str, |
| time_dir_plots: str, |
| *, |
| prefix: Optional[str] = None, |
| min_step_seconds: float = 1.0, |
| debug: bool = False, |
| save_csv: bool = True, |
| ) -> pd.DataFrame: |
| """ |
| Parse all *_timer_info.txt files under `time_dir_txt`, optionally filtering by filename prefix. |
| Generates plots into `time_dir_plots`, returns a DataFrame with all parsed metrics. |
| - Robust to partial/incomplete files. |
| - Will not raise on parse failures; files that fully fail are skipped. |
| """ |
| if not os.path.isdir(time_dir_txt): |
| raise FileNotFoundError(f"time_dir_txt not found: {time_dir_txt}") |
|
|
| os.makedirs(time_dir_plots, exist_ok=True) |
|
|
| |
| files = [ |
| os.path.join(time_dir_txt, f) |
| for f in os.listdir(time_dir_txt) |
| if f.endswith(TIMER_FILE_SUFFIX) and (prefix is None or f.startswith(prefix)) |
| ] |
| files.sort() |
|
|
| if not files: |
| print("No timer files found to analyze.") |
| return pd.DataFrame() |
|
|
| parsed: List[Dict[str, float]] = [] |
| skipped: List[str] = [] |
|
|
| for fp in files: |
| try: |
| metrics = parse_timer_file(fp, min_step_seconds=min_step_seconds, debug=debug) |
| parsed.append(metrics) |
| except Exception as e: |
| skipped.append(f"{os.path.basename(fp)} :: {e}") |
|
|
| if skipped: |
| |
| report_path = os.path.join(time_dir_plots, "skipped_files.txt") |
| with open(report_path, "w", encoding="utf-8") as r: |
| r.write("Files skipped due to parse errors:\n") |
| for line in skipped: |
| r.write(f"- {line}\n") |
| print(f"Some files were skipped. See: {report_path}") |
|
|
| if not parsed: |
| print("No files could be parsed.") |
| return pd.DataFrame() |
|
|
| df = pd.DataFrame(parsed) |
|
|
| |
| if "width" in df.columns and "height" in df.columns: |
| df["image_area"] = df["width"] * df["height"] |
|
|
| |
| if save_csv: |
| csv_path = os.path.join(time_dir_plots, "time_eval_summary.csv") |
| df.to_csv(csv_path, index=False) |
| print(f"Saved summary CSV: {csv_path}") |
|
|
| |
| generate_plots(df, time_dir_plots) |
| print("Plots saved in:", time_dir_plots) |
| return df |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| import argparse |
|
|
| p = argparse.ArgumentParser(description="Analyze timer logs and generate plots.") |
| p.add_argument("--time_dir_txt", required=True, help="Directory containing *_timer_info.txt files.") |
| p.add_argument("--time_dir_plots", required=True, help="Output directory for plots and CSV.") |
| p.add_argument("--prefix", default=None, help="Only analyze files starting with this prefix (e.g., image basename).") |
| p.add_argument("--min_step_seconds", type=float, default=1.0, help="Only record step times > this many seconds.") |
| p.add_argument("--debug", action="store_true", help="Print raw file contents while parsing.") |
| p.add_argument("--no_csv", action="store_true", help="Do not save the summary CSV.") |
| args = p.parse_args() |
|
|
| analyze_timer_files( |
| args.time_dir_txt, |
| args.time_dir_plots, |
| prefix=args.prefix, |
| min_step_seconds=args.min_step_seconds, |
| debug=args.debug, |
| save_csv=not args.no_csv, |
| ) |
|
|