|
| 1 | +#!/usr/bin/env python3 |
| 2 | +import sys, re |
| 3 | +from datetime import datetime, timedelta |
| 4 | + |
| 5 | +def plot_job(job_id, file_path): |
| 6 | + # Parse data |
| 7 | + data = [] |
| 8 | + jobs_data = {} |
| 9 | + with open(file_path) as f: |
| 10 | + for i, line in enumerate(f): |
| 11 | + if i == 0 and line.startswith('job_id'): continue |
| 12 | + parts = line.strip().split('\t') |
| 13 | + if len(parts) >= 3: |
| 14 | + try: |
| 15 | + job_id_in_file = int(parts[0]) |
| 16 | + fraction = parts[2] |
| 17 | + if fraction not in ('NULL', '', '0'): |
| 18 | + ts = datetime.fromisoformat(re.sub(r'[+-]\d{2}$', '', parts[1])) |
| 19 | + pct = float(fraction) * 100 |
| 20 | + if job_id_in_file not in jobs_data: |
| 21 | + jobs_data[job_id_in_file] = [] |
| 22 | + jobs_data[job_id_in_file].append((ts, pct)) |
| 23 | + if job_id_in_file == job_id: |
| 24 | + ts = datetime.fromisoformat(re.sub(r'[+-]\d{2}$', '', parts[1])) |
| 25 | + pct = 0.0 if parts[2] in ('NULL', '') else float(parts[2]) * 100 |
| 26 | + data.append((ts, pct)) |
| 27 | + except: pass |
| 28 | + |
| 29 | + if not data: |
| 30 | + result = [f"No data for job {job_id}. Jobs with progress:"] |
| 31 | + for jid in sorted(jobs_data.keys()): |
| 32 | + job_points = sorted(jobs_data[jid]) |
| 33 | + duration = (job_points[-1][0] - job_points[0][0]).total_seconds() / 60 |
| 34 | + first_pct = job_points[0][1] |
| 35 | + last_pct = job_points[-1][1] |
| 36 | + result.append(f"{jid}: {duration:.1f}min {first_pct:.1f}% -> {last_pct:.1f}%") |
| 37 | + return "\n".join(result) |
| 38 | + data.sort() |
| 39 | + |
| 40 | + # Calculate dimensions |
| 41 | + start, end = data[0][0], data[-1][0] |
| 42 | + dur = (end - start).total_seconds() / 60 |
| 43 | + scale = 0.25 if dur < 60 else 0.5 if dur <= 120 else 3.0 |
| 44 | + width = min(120, max(1, int(dur / scale))) |
| 45 | + |
| 46 | + # Plot grid |
| 47 | + grid = [[' '] * width for _ in range(20)] |
| 48 | + for ts, pct in data: |
| 49 | + x = min(width-1, int((ts - start).total_seconds() / 60 / scale)) |
| 50 | + y = max(0, min(19, 19 - int(pct / 5))) |
| 51 | + grid[y][x] = '*' |
| 52 | + |
| 53 | + # Output |
| 54 | + final_pct, final_y = data[-1][1], max(0, min(19, 19 - int(data[-1][1] / 5))) |
| 55 | + lines = [] |
| 56 | + for i in range(20): |
| 57 | + val = (19 - i) * 5 |
| 58 | + if i == 0: val = 100 # Fix first line to show 100% |
| 59 | + left = f"{val:3d}% " if val % 10 == 0 else " " |
| 60 | + right = f"| {final_pct:.1f}%" if i == final_y else "|" |
| 61 | + lines.append(left + "|" + ''.join(grid[i]) + right) |
| 62 | + |
| 63 | + lines.append(" +" + "-" * width) |
| 64 | + time_labels = " " |
| 65 | + if width <= 30: |
| 66 | + # For short jobs, show only start and end |
| 67 | + start_label = start.strftime("%H:%M") |
| 68 | + end_label = end.strftime("%H:%M") |
| 69 | + time_labels += start_label + " " * (width - 10) + end_label |
| 70 | + else: |
| 71 | + for i in range(0, width, max(1, width // 8)): |
| 72 | + label = (start + timedelta(minutes=i * scale)).strftime("%H:%M") |
| 73 | + time_labels += label if i == 0 else " " * max(0, 6 + i - len(time_labels)) + label |
| 74 | + lines.append(time_labels) |
| 75 | + return "\n".join(lines) |
| 76 | + |
| 77 | +if __name__ == "__main__": |
| 78 | + if len(sys.argv) != 3: |
| 79 | + print("Usage: job-progress-plot <job-id> <file>", file=sys.stderr) |
| 80 | + sys.exit(1) |
| 81 | + try: |
| 82 | + result = plot_job(int(sys.argv[1]), sys.argv[2]) |
| 83 | + if result.startswith("No data for job"): |
| 84 | + print(result, file=sys.stderr) |
| 85 | + sys.exit(1) |
| 86 | + else: |
| 87 | + print(result) |
| 88 | + except Exception as e: |
| 89 | + print(f"Error: {e}", file=sys.stderr) |
| 90 | + sys.exit(1) |
0 commit comments