diff --git a/batch_ssh/batch_ssh.py b/batch_ssh/batch_ssh.py index 927ddbf..6e29134 100755 --- a/batch_ssh/batch_ssh.py +++ b/batch_ssh/batch_ssh.py @@ -54,7 +54,7 @@ def load_config(path): def ssh(host: Host, command: str, timeout: int) -> Result: ssh_cmd = [ - "ssh", + "ssh", "-p", host.port, "-o", f"ConnectTimeout={timeout}", "-o", "StrictHostKeyChecking=no", @@ -113,19 +113,20 @@ def main(): max_prefix_len = max(max_prefix_len, len(prefix)) entry.append(prefix) - for i, entry in enumerate(res): - host, future, prefix = entry - + for host, future, prefix in res: future_res = future.result() output = future_res.output.strip() if future_res.returncode is None: assert("\n" not in output) output = f"exception: {output}" + suffix = "---" + else: + suffix = future_res.returncode output_lines = output.splitlines(keepends=True) # 处理缩进 - output = output_lines[0] + "".join(textwrap.indent("".join(output_lines[1:]), " " * (max_prefix_len + 13))) - print(f"[{prefix:<{max_prefix_len}}][ret: {future_res.returncode:>3}] {output}") + output = output_lines[0] + "".join(textwrap.indent("".join(output_lines[1:]), " " * (max_prefix_len + 8))) + print(f"[{prefix:<{max_prefix_len}}][{suffix:>3}] {output}") if __name__ == "__main__": main() diff --git a/gantt/big_input.png b/gantt/big_input.png new file mode 100644 index 0000000..594123f Binary files /dev/null and b/gantt/big_input.png differ diff --git a/gantt/big_input.txt b/gantt/big_input.txt new file mode 100644 index 0000000..1b2f457 --- /dev/null +++ b/gantt/big_input.txt @@ -0,0 +1,324 @@ +timestamp: 2026-09-08 19:38:07.431900 + +gantt results: +#, begin, duration, task, size +io-thread-00, 0.000, 0.000, sort, medium +io-thread-00, 0.000, 0.200, write, tiny +io-thread-00, 0.700, 0.005, dispatch, large +io-thread-00, 0.405, 0.000, sync, small +io-thread-00, 0.405, 8.000, poll, tiny +io-thread-00, 8.905, 0.200, merge, xlarge +io-thread-00, 9.205, 0.200, link, xlarge +io-thread-00, 9.405, 0.000, fsync, large +io-thread-00, 9.405, 0.500, write, small +io-thread-00, 9.905, 0.005, sync, large +worker-01, 0.000, 1.000, alloc, xlarge +worker-01, 1.000, 0.000, sort, large +worker-01, 1.500, 0.005, free, tiny +worker-01, 2.005, 0.500, flush, xlarge +worker-01, 2.505, 0.200, merge, tiny +worker-01, 2.405, 0.200, send, tiny +worker-01, 2.605, 0.005, free, medium +worker-01, 2.710, 1.000, fsync, medium +worker-01, 3.710, 0.200, checkpoint, medium +worker-01, 3.610, 0.050, serialize, small +worker-02, 0.000, 5.000, free, medium +worker-02, 5.500, 0.200, checkpoint, medium +worker-02, 5.400, 0.200, compute, medium +worker-02, 5.700, 0.500, sync, small +worker-02, 6.700, 1.000, kernel-A, large +worker-02, 7.800, 5.000, write, medium +worker-02, 12.800, 0.200, sort, xlarge +worker-02, 13.500, 0.500, sort, xlarge +worker-02, 14.100, 2.500, alloc, small +worker-02, 16.600, 8.000, optimize, tiny +io-thread-03, 0.000, 0.005, write, small +io-thread-03, 0.105, 0.005, free, large +io-thread-03, 0.610, 5.000, parse, medium +io-thread-03, 6.110, 0.000, checkpoint, tiny +io-thread-03, 6.610, 0.500, flush, medium +io-thread-03, 6.810, 0.500, compile, small +io-thread-03, 7.410, 0.000, sort, medium +io-thread-03, 7.910, 0.050, parse, tiny +io-thread-03, 7.960, 8.000, poll, small +io-thread-03, 15.960, 1.000, fsync, xlarge +worker-04, 0.500, 0.000, poll, medium +worker-04, 0.600, 0.000, read, medium +worker-04, 0.600, 0.200, compute, small +worker-04, 1.300, 0.005, sync, large +worker-04, 1.005, 8.000, write, small +worker-04, 9.105, 8.000, fsync, medium +worker-04, 17.605, 2.500, kernel-A, xlarge +worker-04, 20.105, 0.500, free, medium +worker-04, 20.705, 8.000, link, tiny +worker-04, 28.705, 0.200, sync, medium +worker-05, 0.000, 8.000, kernel-B, xlarge +worker-05, 8.000, 0.000, sync, tiny +worker-05, 8.000, 0.005, compute, medium +worker-05, 7.705, 8.000, kernel-B, medium +worker-05, 15.805, 0.200, serialize, small +worker-05, 16.505, 5.000, kernel-B, large +worker-05, 21.605, 0.200, read, tiny +worker-05, 21.905, 1.000, compile, large +worker-05, 23.005, 0.000, checkpoint, tiny +worker-05, 22.705, 2.500, sort, medium +io-thread-06, 0.000, 0.200, kernel-A, small +io-thread-06, 0.700, 5.000, write, large +io-thread-06, 5.700, 0.500, link, small +io-thread-06, 5.900, 5.000, serialize, tiny +io-thread-06, 10.600, 8.000, init, tiny +io-thread-06, 18.600, 0.050, compile, large +io-thread-06, 18.750, 0.200, free, tiny +io-thread-06, 18.950, 2.500, init, large +io-thread-06, 21.450, 5.000, send, large +io-thread-06, 26.950, 5.000, write, small +worker-07, 0.000, 0.200, compute, xlarge +worker-07, 0.700, 0.000, sort, medium +worker-07, 0.400, 0.000, dispatch, large +worker-07, 0.900, 8.000, fsync, tiny +worker-07, 9.400, 0.005, fsync, tiny +worker-07, 9.905, 0.005, checkpoint, small +worker-07, 10.010, 0.005, dispatch, small +worker-07, 10.515, 0.000, poll, tiny +worker-07, 10.615, 8.000, ping, medium +worker-07, 18.615, 1.000, kernel-B, medium +worker-08, 0.100, 0.050, checkpoint, medium +worker-08, 0.250, 1.000, sync, tiny +worker-08, 1.350, 0.005, sync, xlarge +worker-08, 1.355, 8.000, recv, small +worker-08, 9.355, 0.005, kernel-B, medium +worker-08, 9.360, 0.050, link, xlarge +worker-08, 9.410, 8.000, init, xlarge +worker-08, 17.410, 0.005, write, medium +worker-08, 17.115, 0.005, sort, xlarge +worker-08, 17.120, 0.500, send, xlarge +io-thread-09, 0.000, 1.000, kernel-A, medium +io-thread-09, 1.500, 5.000, recv, tiny +io-thread-09, 6.200, 2.500, recv, tiny +io-thread-09, 8.400, 1.000, write, medium +io-thread-09, 9.400, 5.000, serialize, large +io-thread-09, 14.900, 0.000, read, tiny +io-thread-09, 14.900, 8.000, compute, medium +io-thread-09, 23.400, 8.000, write, large +io-thread-09, 31.400, 0.000, send, medium +io-thread-09, 31.100, 1.000, kernel-A, small +worker-10, 0.000, 1.000, serialize, large +worker-10, 1.500, 0.050, kernel-B, small +worker-10, 1.550, 2.500, init, small +worker-10, 4.050, 2.500, checkpoint, small +worker-10, 6.550, 0.050, merge, tiny +worker-10, 6.700, 0.000, optimize, small +worker-10, 6.700, 5.000, alloc, medium +worker-10, 11.700, 0.200, init, small +worker-10, 12.000, 1.000, recv, tiny +worker-10, 13.000, 1.000, flush, xlarge +worker-11, 0.100, 8.000, ping, tiny +worker-11, 7.800, 0.500, fsync, xlarge +worker-11, 8.300, 0.000, read, xlarge +worker-11, 8.400, 1.000, sort, medium +worker-11, 9.500, 8.000, read, large +worker-11, 18.000, 0.200, recv, tiny +worker-11, 18.300, 0.000, parse, xlarge +worker-11, 18.300, 1.000, compile, tiny +worker-11, 19.300, 1.000, checkpoint, tiny +worker-11, 20.300, 8.000, send, large +io-thread-12, 0.000, 2.500, merge, medium +io-thread-12, 3.000, 0.050, kernel-A, large +io-thread-12, 3.150, 0.050, poll, xlarge +io-thread-12, 3.200, 2.500, serialize, tiny +io-thread-12, 5.700, 0.500, kernel-A, large +io-thread-12, 6.700, 1.000, link, large +io-thread-12, 7.800, 0.200, parse, large +io-thread-12, 8.000, 0.005, send, xlarge +io-thread-12, 8.505, 1.000, sync, small +io-thread-12, 9.505, 0.200, kernel-A, small +worker-13, 0.000, 0.000, kernel-B, large +worker-13, 0.500, 0.005, link, large +worker-13, 1.005, 0.200, merge, large +worker-13, 1.305, 2.500, kernel-B, small +worker-13, 3.505, 0.005, compile, small +worker-13, 3.510, 8.000, link, tiny +worker-13, 12.010, 0.200, read, large +worker-13, 12.210, 5.000, checkpoint, xlarge +worker-13, 17.710, 1.000, link, xlarge +worker-13, 19.210, 2.500, serialize, large +worker-14, 0.000, 5.000, link, medium +worker-14, 5.000, 0.500, parse, large +worker-14, 5.500, 0.500, link, tiny +worker-14, 6.000, 0.200, recv, medium +worker-14, 6.200, 8.000, sync, small +worker-14, 14.200, 0.200, free, small +worker-14, 14.400, 0.005, compile, large +worker-14, 14.405, 8.000, link, large +worker-14, 22.105, 0.200, compile, large +worker-14, 22.805, 0.000, dispatch, large +io-thread-15, 0.100, 0.000, alloc, medium +io-thread-15, 0.200, 2.500, serialize, xlarge +io-thread-15, 3.200, 0.200, optimize, small +io-thread-15, 3.400, 2.500, optimize, tiny +io-thread-15, 6.000, 1.000, checkpoint, large +io-thread-15, 7.000, 5.000, write, xlarge +io-thread-15, 12.500, 0.000, free, xlarge +io-thread-15, 13.000, 0.000, sync, large +io-thread-15, 13.000, 5.000, fsync, tiny +io-thread-15, 18.000, 2.500, ping, small +worker-16, 0.100, 1.000, ping, large +worker-16, 1.100, 2.500, recv, tiny +worker-16, 3.700, 0.000, sort, xlarge +worker-16, 3.400, 1.000, kernel-B, tiny +worker-16, 4.100, 0.000, kernel-B, small +worker-16, 3.800, 0.050, kernel-B, small +worker-16, 3.950, 0.005, dispatch, small +worker-16, 4.055, 0.500, alloc, small +worker-16, 5.055, 0.005, fsync, medium +worker-16, 4.760, 0.000, send, xlarge +worker-17, 0.100, 2.500, merge, small +worker-17, 2.300, 0.200, read, medium +worker-17, 3.000, 0.005, dispatch, tiny +worker-17, 3.005, 8.000, compile, medium +worker-17, 10.705, 8.000, flush, medium +worker-17, 18.405, 2.500, optimize, tiny +worker-17, 21.005, 1.000, flush, large +worker-17, 22.005, 2.500, fsync, xlarge +worker-17, 24.505, 8.000, optimize, large +worker-17, 32.605, 0.500, ping, small +io-thread-18, 0.000, 0.500, link, small +io-thread-18, 0.600, 2.500, ping, tiny +io-thread-18, 3.200, 1.000, fsync, large +io-thread-18, 4.200, 1.000, recv, medium +io-thread-18, 5.200, 0.500, serialize, tiny +io-thread-18, 6.200, 0.200, sync, small +io-thread-18, 6.500, 5.000, serialize, small +io-thread-18, 11.600, 5.000, link, tiny +io-thread-18, 16.300, 0.500, kernel-B, large +io-thread-18, 16.800, 0.500, checkpoint, xlarge +worker-19, 0.000, 5.000, serialize, xlarge +worker-19, 5.000, 2.500, sort, xlarge +worker-19, 7.500, 1.000, merge, large +worker-19, 8.500, 0.500, recv, small +worker-19, 8.700, 0.200, ping, tiny +worker-19, 9.400, 0.050, kernel-A, small +worker-19, 9.550, 0.500, sort, xlarge +worker-19, 10.550, 0.500, read, small +worker-19, 11.050, 0.200, alloc, small +worker-19, 11.250, 0.000, merge, xlarge +worker-20, 0.000, 0.500, compute, tiny +worker-20, 1.000, 0.500, merge, small +worker-20, 1.600, 0.005, init, xlarge +worker-20, 1.605, 5.000, optimize, large +worker-20, 6.605, 0.050, compute, medium +worker-20, 6.755, 0.005, sync, large +worker-20, 6.860, 0.005, dispatch, tiny +worker-20, 6.865, 0.050, dispatch, medium +worker-20, 6.615, 0.200, read, xlarge +worker-20, 6.915, 0.200, parse, large +io-thread-21, 0.100, 5.000, send, xlarge +io-thread-21, 5.200, 0.500, dispatch, xlarge +io-thread-21, 5.400, 0.005, kernel-A, small +io-thread-21, 5.405, 0.005, fsync, small +io-thread-21, 5.410, 8.000, sync, small +io-thread-21, 13.110, 2.500, link, xlarge +io-thread-21, 15.710, 0.500, compute, small +io-thread-21, 16.210, 0.500, merge, large +io-thread-21, 16.410, 0.200, recv, xlarge +io-thread-21, 16.610, 2.500, read, xlarge +worker-22, 0.000, 0.050, recv, small +worker-22, 0.000, 0.000, fsync, medium +worker-22, 0.500, 0.500, link, tiny +worker-22, 1.100, 0.500, merge, large +worker-22, 1.600, 8.000, serialize, large +worker-22, 9.700, 0.005, poll, tiny +worker-22, 9.805, 1.000, poll, medium +worker-22, 10.505, 0.005, kernel-B, xlarge +worker-22, 11.010, 0.000, checkpoint, medium +worker-22, 11.510, 0.000, fsync, large +worker-23, 0.500, 5.000, recv, small +worker-23, 6.000, 2.500, flush, large +worker-23, 8.200, 5.000, alloc, large +worker-23, 13.200, 1.000, checkpoint, tiny +worker-23, 14.200, 1.000, compile, large +worker-23, 15.200, 2.500, serialize, tiny +worker-23, 17.800, 0.005, ping, medium +worker-23, 17.805, 0.005, free, xlarge +worker-23, 17.510, 8.000, link, large +worker-23, 25.210, 0.200, parse, medium +io-thread-24, 0.500, 5.000, flush, large +io-thread-24, 5.200, 0.200, recv, xlarge +io-thread-24, 5.400, 0.500, link, large +io-thread-24, 5.600, 0.000, flush, xlarge +io-thread-24, 5.600, 0.050, send, xlarge +io-thread-24, 5.350, 8.000, compile, tiny +io-thread-24, 13.350, 0.005, link, tiny +io-thread-24, 13.355, 5.000, merge, medium +io-thread-24, 18.855, 0.500, compile, large +io-thread-24, 19.455, 0.200, link, xlarge +worker-25, 0.000, 2.500, kernel-A, xlarge +worker-25, 3.000, 0.050, sync, medium +worker-25, 3.150, 1.000, parse, medium +worker-25, 3.850, 0.500, sort, medium +worker-25, 4.850, 5.000, write, large +worker-25, 10.350, 5.000, alloc, medium +worker-25, 15.850, 8.000, free, large +worker-25, 23.850, 0.200, merge, small +worker-25, 24.550, 2.500, kernel-B, large +worker-25, 26.750, 1.000, sort, large +worker-26, 0.100, 2.500, checkpoint, small +worker-26, 2.700, 0.000, write, xlarge +worker-26, 3.200, 1.000, read, large +worker-26, 3.900, 8.000, link, tiny +worker-26, 11.900, 2.500, flush, small +worker-26, 14.100, 5.000, recv, medium +worker-26, 19.600, 2.500, flush, tiny +worker-26, 22.100, 8.000, free, medium +worker-26, 30.200, 8.000, compute, xlarge +worker-26, 37.900, 0.200, flush, medium +io-thread-27, 0.000, 0.005, compile, tiny +io-thread-27, 0.000, 5.000, fsync, medium +io-thread-27, 4.700, 0.000, ping, tiny +io-thread-27, 4.700, 1.000, alloc, large +io-thread-27, 5.700, 0.200, parse, large +io-thread-27, 6.400, 0.050, fsync, small +io-thread-27, 6.150, 2.500, poll, small +io-thread-27, 8.750, 0.050, kernel-B, large +io-thread-27, 8.800, 5.000, recv, tiny +io-thread-27, 13.900, 0.500, checkpoint, xlarge +worker-28, 0.000, 0.005, link, medium +worker-28, 0.505, 0.500, flush, large +worker-28, 1.005, 5.000, send, small +worker-28, 6.105, 5.000, read, small +worker-28, 11.205, 1.000, dispatch, medium +worker-28, 12.205, 0.000, checkpoint, large +worker-28, 12.205, 0.000, dispatch, tiny +worker-28, 12.705, 5.000, send, small +worker-28, 18.205, 1.000, kernel-B, small +worker-28, 19.705, 0.500, checkpoint, small +worker-29, 0.000, 0.000, send, large +worker-29, 0.000, 1.000, sort, small +worker-29, 0.700, 0.500, ping, large +worker-29, 1.200, 0.200, write, xlarge +worker-29, 1.400, 8.000, parse, medium +worker-29, 9.400, 0.500, optimize, medium +worker-29, 9.900, 0.005, link, tiny +worker-29, 9.905, 0.200, checkpoint, large +worker-29, 10.605, 1.000, sync, large +worker-29, 11.305, 0.500, serialize, tiny +io-thread-30, 0.100, 1.000, checkpoint, medium +io-thread-30, 1.600, 2.500, flush, medium +io-thread-30, 3.800, 0.200, optimize, tiny +io-thread-30, 4.500, 8.000, ping, xlarge +io-thread-30, 12.500, 0.005, flush, large +io-thread-30, 12.505, 2.500, read, small +io-thread-30, 14.705, 0.000, send, large +io-thread-30, 14.405, 0.005, kernel-B, xlarge +io-thread-30, 14.410, 2.500, link, medium +io-thread-30, 17.410, 2.500, dispatch, small +worker-31, 0.100, 0.005, optimize, xlarge +worker-31, 0.205, 0.500, compute, medium +worker-31, 0.705, 5.000, link, small +worker-31, 5.705, 0.005, checkpoint, medium +worker-31, 6.210, 1.000, compute, large +worker-31, 7.210, 0.200, read, large +worker-31, 7.110, 0.200, flush, xlarge +worker-31, 7.010, 0.000, ping, small +worker-31, 7.010, 0.200, sync, xlarge +worker-31, 7.210, 0.200, kernel-B, medium diff --git a/gantt/gantt.py b/gantt/gantt.py new file mode 100755 index 0000000..71bca9e --- /dev/null +++ b/gantt/gantt.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""gantt.py — Gantt chart for program task timing analysis + +Input format (see input.txt in the same directory): + timestamp: + + gantt results: + #, begin, duration, , , ... + , , , , , ... + + - worker (the # column) is a string deciding which row the task is drawn on; + - begin / duration are seconds relative to timestamp (float); + - remaining columns are custom tags, rendered onto bars via --label format string. + +Usage examples: + python3 gantt.py input.txt + python3 gantt.py input.txt -o result.png --label '{task} ({duration}s)' + python3 gantt.py input.txt --color-by task --label '{task}' + +Placeholders available in the --label format string: + {tag_name} tag names defined in the header, e.g. {task}, {size} + {c} / {N} raw CSV column index (0=worker, 1=begin, 2=duration, tags from 3) + {worker} {begin} {duration} {end} built-in fields +""" +import argparse +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +import matplotlib +# 使用Anti-Grain Geometry图像渲染后端,避免在无GUI环境运行时出现错误 +matplotlib.use("Agg") +import matplotlib.colors +import matplotlib.pyplot as plt + +# 全局配置 +BASE_COLORMAP = "Set3" # 基础配色方案(matplotlib定性色条) +ALT_LIGHTEN = 0.35 # 交替明度提升参数,控制同一行相邻任务深浅差异 +FIG_W = 16.0 # 输出图像宽度,单位:英寸 +FIG_ASPECT = 9 / 16 # 默认高度:宽度 +BASE_ROWS = 32 # 基准行数:不超过时维持默认比例,超过后保持每行高度按比例增高 + +# 由BASE_COLORMAP锚点插值构建连续渐变COLORMAP +COLORMAP = matplotlib.colors.LinearSegmentedColormap.from_list( + "gradient", matplotlib.colormaps[BASE_COLORMAP].colors) + + +# 公共函数 +# 从COLORMAP上均匀采样n个颜色 +def sample_colors(n): + if n <= 1: + return [COLORMAP(0.5)] + return [COLORMAP(i / (n - 1)) for i in range(n)] + + +# 提升颜色rgb明度,amount表示向白色混合的百分比,取值为0-1 +def lighten(rgb, amount): + """向白色混合 amount,略微提高明度。""" + return tuple(c + (1 - c) * amount for c in rgb[:3]) + + +# 自然排序:划分数字和文本段,每段逐次比较,数字段优先文本段,数字段按数值排序,文本段按字典序排序 +def natural_key(s): + return [(0, int(p)) if p.isdigit() else (1, p) for p in re.split(r"(\d+)", s) if p] + + +@dataclass(frozen=True) +class Task: + worker: str + begin: float + duration: float + tags: tuple + + +@dataclass(frozen=True) +class GanttData: + """输入文件解析结果。 + + meta: 头部 key: value(如 timestamp) + columns: tag 列名(无表头时为 c3/c4/... 形式的兜底命名) + tasks: 解析出的任务列表 + """ + meta: dict + columns: tuple + tasks: tuple + + +# ---------------------------------------------------------------- 解析输入 + +def _warn(lineno, reason, raw): + print(f"warning: line {lineno} {reason}, skipped: {raw!r}", file=sys.stderr) + + +def _parse_row(fields, lineno, raw): + """解析一条数据行为 Task,字段不足或数值非法时告警并返回 None。""" + if len(fields) < 3: + _warn(lineno, "has too few fields", raw) + return None + try: + begin, duration = float(fields[1]), float(fields[2]) + except ValueError: + _warn(lineno, "begin/duration not numeric", raw) + return None + return Task(fields[0], begin, duration, tuple(fields[3:])) + + +def parse_input(path): + """Parse the input file, returning a GanttData.""" + meta, columns, tasks = {}, [], [] + in_results = False + for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + line = raw.strip() + if not line: + continue + if not in_results: + if line == "gantt results:": + in_results = True + else: + key, sep, value = line.partition(":") + if sep: + meta[key.strip()] = value.strip() + continue + fields = [f.strip() for f in line.split(",")] + if not columns: + # 表头行:第二列为 begin 则认为是表头,否则首行即数据,tag 按列号命名 + if len(fields) >= 3 and fields[1].lower() == "begin": + columns = fields[3:] + continue + columns = [f"c{3 + i}" for i in range(max(len(fields) - 3, 0))] + task = _parse_row(fields, lineno, raw) + if task: + tasks.append(task) + # 统一 tag 数量与列数一致 + if columns: + tasks = [Task(t.worker, t.begin, t.duration, + (t.tags + ("",) * len(columns))[: len(columns)]) for t in tasks] + return GanttData(meta, tuple(columns), tuple(tasks)) + + +def fmt_num(value): + return f"{value:.6g}" + + +def make_label(fmt, task, columns): + """Render the --label format string onto a bar. Unknown placeholders become empty.""" + ctx = {f"c{i + 3}": tag for i, tag in enumerate(task.tags)} + for i, name in enumerate(columns): + ctx[name] = task.tags[i] if i < len(task.tags) else "" + ctx.update( + c0=task.worker, c1=fmt_num(task.begin), c2=fmt_num(task.duration), + worker=task.worker, begin=fmt_num(task.begin), + duration=fmt_num(task.duration), end=fmt_num(task.begin + task.duration), + ) + + class _Ctx(dict): + def __missing__(self, key): + return "" + + # {N} 等价于 {cN} + return re.sub(r"\{(\d+)\}", r"{c\1}", fmt).format_map(_Ctx(ctx)) + + +def resolve_column(name, columns): + """Resolve the --color-by argument to a raw CSV column index; None if unresolvable.""" + if name in columns: + return 3 + columns.index(name) + if name in ("worker", "begin", "duration"): + return ("worker", "begin", "duration").index(name) + m = re.fullmatch(r"c?(\d+)", name) + return int(m.group(1)) if m else None + + +def field_at(task, col_idx): + if col_idx == 0: + return task.worker + if col_idx == 1: + return fmt_num(task.begin) + if col_idx == 2: + return fmt_num(task.duration) + return task.tags[col_idx - 3] if col_idx - 3 < len(task.tags) else "" + + +# ---------------------------------------------------------------- 绘图 + +def render(data, args): + meta, columns, tasks = data.meta, data.columns, data.tasks + # 行排序方式:natural / lex / appearance + if args.sort == "lex": + workers = sorted({t.worker for t in tasks}) + elif args.sort == "appearance": + workers = list(dict.fromkeys(t.worker for t in tasks)) + else: + workers = sorted({t.worker for t in tasks}, key=natural_key) + row_of = {w: i for i, w in enumerate(workers)} + + # --color-by:按列值从彩虹 colorbar 均匀采样分类色 + # 默认按 worker 着色,同一 worker 内相邻任务交替深浅,避免连在一起的段看不清 + has_legend = False + legend_items = [] + if args.color_by: + col_idx = resolve_column(args.color_by, columns) + if col_idx is None: + print(f"error: cannot resolve --color-by column '{args.color_by}', " + f"available: {', '.join(columns) or '(no tag columns)'}", file=sys.stderr) + sys.exit(1) + values = list(dict.fromkeys(field_at(t, col_idx) for t in tasks)) + value_color = dict(zip(values, sample_colors(len(values)))) + task_color = {id(t): value_color[field_at(t, col_idx)] for t in tasks} + legend_items = list(value_color.items()) + has_legend = len(legend_items) >= 2 + else: + worker_color = dict(zip(workers, sample_colors(len(workers)))) + seq = {} + task_color = {} + for t in tasks: + i = seq.get(t.worker, 0) + task_color[id(t)] = (worker_color[t.worker] if i % 2 == 0 + else lighten(worker_color[t.worker], ALT_LIGHTEN)) + # 宽度接近零的任务不参与同行颜色 toggle + if t.duration > 0: + seq[t.worker] = i + 1 + + # 默认 16:9;行数超过 BASE_ROWS 时保持每行高度不变,按比例增大图高 + fig_h = FIG_W * FIG_ASPECT * max(1.0, len(workers) / BASE_ROWS) + fig, ax = plt.subplots(figsize=(FIG_W, fig_h)) + labels = [] + for t in tasks: + y = row_of[t.worker] + ax.broken_barh([(t.begin, max(t.duration, 1e-9))], (y - 0.5, 1.0), + facecolors=task_color[id(t)], edgecolor="none") + if args.label: + artist = ax.text(t.begin + t.duration / 2, y, + make_label(args.label, t, columns), + ha="center", va="center", fontsize=8) + labels.append((artist, t.begin, t.begin + t.duration)) + + # label 文字宽度超过任务 bar 宽度则不显示 + if labels: + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + for artist, x0, x1 in labels: + bar_px = abs(ax.transData.transform((x1, 0))[0] + - ax.transData.transform((x0, 0))[0]) + if artist.get_window_extent(renderer).width > bar_px: + artist.remove() + + ax.set_ylim(len(workers) - 0.5, -0.5) # 首个 worker 在最上方 + ax.margins(x=0) # 横坐标左右两端不留空隙 + ax.set_yticks(range(len(workers))) + ax.set_yticklabels(workers) + ax.tick_params(axis="y", length=0) + ax.grid(axis="x", linewidth=0.8, alpha=0.5) + ax.set_axisbelow(True) + ax.set_xlabel("time (s)") + + title = args.title or "Gantt Chart" + sub = meta.get("timestamp") + if sub: + ax.set_title(f"{title}\n{sub}", fontsize=12) + else: + ax.set_title(title, fontsize=12) + if has_legend: + ax.legend(handles=[plt.Rectangle((0, 0), 1, 1, color=c, label=v) + for v, c in legend_items], + loc="lower left", bbox_to_anchor=(0, 1.02), ncol=8, + frameon=False, fontsize=9) + + fig.savefig(args.output, bbox_inches="tight") + + +# ---------------------------------------------------------------- CLI + +def main(): + parser = argparse.ArgumentParser( + description="Draw a Gantt chart from program task timing data " + "(input format see input.txt)") + parser.add_argument("input", help="input file path") + parser.add_argument("-o", "--output", help="output image path, default .png") + parser.add_argument("--label", + help="format string for bar labels, e.g. '{task} ({duration}s)', " + "see module docstring") + parser.add_argument("--color-by", + help="color bars by this column (tag name or index like c3), " + "default single color") + parser.add_argument("--title", help="custom title") + parser.add_argument("--sort", choices=["natural", "lex", "appearance"], + default="natural", + help="worker row order: natural (Windows-style, default), " + "lex (lexicographic), appearance (first-seen order)") + args = parser.parse_args() + + path = Path(args.input) + if not path.is_file(): + print(f"error: input path '{args.input}' does not exist or is not a file", + file=sys.stderr) + sys.exit(1) + + data = parse_input(path) + + if not data.tasks: + print("error: no valid tasks parsed (missing 'gantt results:' section?)", + file=sys.stderr) + sys.exit(1) + + args.output = args.output or str(path.with_suffix(".png")) + render(data, args) + print(f"gantt saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/gantt/input.txt b/gantt/input.txt new file mode 100644 index 0000000..88c23c3 --- /dev/null +++ b/gantt/input.txt @@ -0,0 +1,7 @@ +timestamp: 2026-09-08 19:38:07.431900 + +gantt results: +#, begin, duration, , +... + +此后每行,#打印worker_id,可以是string,表示任务在甘特图的哪个行上;begin表示任务开始时间距离timestamp的时间,浮点型,单位s;duration表示任务长度,浮点型,单位s;后面的tag则是任务的自定义字段,脚本支持输入一个包含tag的格式化字符串,将字符串内容输入到对应任务的甘特图bar上 \ No newline at end of file diff --git a/gantt/test_input.png b/gantt/test_input.png new file mode 100644 index 0000000..11bc29f Binary files /dev/null and b/gantt/test_input.png differ diff --git a/gantt/test_input.txt b/gantt/test_input.txt new file mode 100644 index 0000000..897b2fc --- /dev/null +++ b/gantt/test_input.txt @@ -0,0 +1,16 @@ +timestamp: 2026-09-08 19:38:07.431900 + +gantt results: +#, begin, duration, task, size +main, 0.0, 0.8, init, small +main, 0.8, 12.5, compute, large +main, 13.3, 0, sync, small +main, 13.3, 1.2, cleanup, small +io-worker, 1.0, 5.5, read, medium +io-worker, 7.0, 4.0, write, medium +io-worker, 11.5, 0.02, fsync, small +gpu, 1.5, 3.25, kernel-A, large +gpu, 5.0, 6.75, kernel-B, large +net, 2.0, 2.5, recv, medium +net, 9.0, 3.0, send, medium +net, 14.5, 0.001, ping, small