diff options
| author | blasty <peter@haxx.in> | 2026-08-06 15:11:54 +0200 |
|---|---|---|
| committer | blasty <peter@haxx.in> | 2026-08-06 15:11:54 +0200 |
| commit | 2506d350f4f2848bb69d430afaedd694c31b245a (patch) | |
| tree | 90a7594e0ecef5166fe8341c997e8244e7e81a1c | |
| parent | tests: graph scenarios (diff) | |
| download | ida-tui-2506d350f4f2848bb69d430afaedd694c31b245a.tar.gz ida-tui-2506d350f4f2848bb69d430afaedd694c31b245a.tar.xz ida-tui-2506d350f4f2848bb69d430afaedd694c31b245a.zip | |
graph: docs, and the offline layout tools
cfg_dump freezes real CFGs to JSON; graph_spike renders one or --stats a
whole corpus through the shipping engine; graph_smoke is the end-to-end
tool->domain->layout check; graph_shot renders the real view headless at a
chosen size, which is the only sane way to judge it (a tiled pane is far
too narrow and the minimap sits on top of the graph).
Diffstat (limited to '')
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | docs/GRAPH_VIEW.md | 145 | ||||
| -rw-r--r-- | experiments/cfg_dump.py | 119 | ||||
| -rw-r--r-- | experiments/graph_shot.py | 66 | ||||
| -rw-r--r-- | experiments/graph_smoke.py | 107 | ||||
| -rw-r--r-- | experiments/graph_spike.py | 149 |
6 files changed, 600 insertions, 0 deletions
@@ -49,6 +49,20 @@ don't file expectations. **Use at your own risk.** offset), rename (`n`), retype (`y`), comment (`;`), incremental search (`/` `?`), history (`back`), hex view (`\`), and `home`/`end`/`shift+home` line motions. +- **Literal formats** (`o`, IDA's own key): cycle how the number under the + cursor is displayed — hex → decimal → binary → character → offset → IDA's + own choice, `O` to go the other way. Only the stops that make sense for that + value are visited (no `char` unless it prints as one, no `offset` unless the + target is something you could name), so no press is a silent no-op. It works + in the pseudocode too, on Hex-Rays' separate number formats. The opcode-bytes + column, which used to own `o`, moved to `B`. + + A line usually holds more than one literal (`test byte ptr [rsi+rax*2+1], 20h` + has two), so **the one the cursor is on is marked** — that mark is what `o` + changes, and it keeps up as the text reflows (`0x30` ↔ `48` move everything + after them). Land on something with no format of its own — a register — and it + says so and names the operand that does, rather than quietly reformatting a + different one. - A **functions panel** (fuzzy symbol palette on `Ctrl+N`), a **strings browser** (`"`, filterable, Enter jumps to the literal), **hex viewer**, **struct editor**, and inline **make code/data/function/string** edits. diff --git a/docs/GRAPH_VIEW.md b/docs/GRAPH_VIEW.md new file mode 100644 index 0000000..ccddfe7 --- /dev/null +++ b/docs/GRAPH_VIEW.md @@ -0,0 +1,145 @@ +# Graph view — the function's control flow, in character cells + +**Space** swaps the code view for an IDA-style basic-block graph of the current +function. Space again returns to the text. It is opt-in, off by default, and +lives in its own module: with graph mode off nothing else in the app does any +extra work. + +``` + ┌─ sub_61D0 ──────────────────────┐ + │ 000061D0 sub_61D0 endbr64 │ ┌─ 9 blocks ──────┐ + │ 000061D4 push rbp │ │ █████████ │ + │ ... │ │ ·███████████ │ + │ 00006200 jmp short loc_622C │ │ ████·███████ │ + └─────────────────┬───────────────┘ └─────────────────┘ + │ + ┌─ loc_622C ───────▼─────────────────┐ + │ 0000622C loc_622C mov eax, [rcx] │ + │ 00006231 jbe short loc_6208 │ + └──────────┬─────────────┬───────────┘ + ╭─────────╯ ╰──────────╮ +``` + +## Keys + +| key | | +|---|---| +| `Space` | graph ⇄ text | +| `j` `k` | line up/down, crossing into the next/previous block | +| `h` `l` | column left / right | +| `J` `K` | follow an edge to a successor / predecessor block | +| `w` `b` | next / previous block in layout order | +| `0` | jump to the entry block | +| `z` | zoom: full → compact → collapsed | +| `m` | show / hide the minimap | +| `f` | centre on the current block | +| `Enter` | follow — stays in the graph when the target is a block of this function | +| `x` `n` `y` `;` | xrefs / rename / retype / comment, exactly as in the listing | +| `Tab` | leave for the pseudocode of the block you're on | +| mouse | drag to pan, click to place the cursor, double-click to follow | + +Graph mode is **sticky**: following a call from the graph lands in the callee's +graph rather than dumping you back in the listing. + +## Why the boxes are cheap + +A node's body is **the same `Head` rows the listing renders** — fetched with the +same `heads` tool, carrying IDA's own colour tags, names and operand text. That +is the whole reason this feature is a few hundred lines instead of a rewrite: +syntax highlighting, the word-under-cursor highlight, the execution trail and +every editing verb work inside a box because they are working on listing rows. +Growing a second disassembly renderer for graph mode would have been the real +cost. + +The backend adds exactly one tool, `flowchart(addr)` in +`server/patch_server.py`, which returns block ranges and typed edges — **not** +text. + +## Layout (`idatui/graph.py`) + +Pure python: no IDA, no Textual, no I/O, so it is unit-tested offline in +milliseconds (`tests/test_graph.py`, which needs no worker). Textbook Sugiyama, +the same shape IDA's own graph uses: + +| step | what | notes | +|---|---|---| +| 1 | break cycles | DFS gray-set; back edges reversed for layout only | +| 2 | layer | longest-path ranking | +| 3 | dummies | a k-layer edge becomes k−1 dummy nodes | +| 4 | order | median sweeps + adjacent transposition | +| 5 | x-coords | priority/median sweeps, variable node widths | +| 6 | route | ports per border, one lane-packed channel per layer gap | + +Step 3 is what makes step 6 tractable: because every long edge occupies real +horizontal space as dummy nodes, **no edge ever has to cross a box**. That is +measured, not hoped — `tests/test_graph.py` counts edge cells landing inside a +box across a 128-function corpus and requires 0. + +Edges are coloured by IDA's convention: green = branch taken, red = falls +through, blue = the block's only successor, purple = loops back, amber = one arm +of an n-way switch. The edges touching the block under the cursor are brightened. +A self-loop is a `↺` on the block's top border rather than an edge. + +### Two traps worth remembering + +- **Self-loops deadlock the Kahn ranking.** A block that jumps to itself never + drains its own in-degree, so ranking stalls and every downstream block stays at + rank 0 — the graph collapses into three layers and comes out 280 columns wide. + They are dropped from the layout graph and drawn as a marker. `_assign_ranks` + also force-releases the most-constrained survivor if the queue ever drains + early, so a residual cycle degrades instead of exploding. +- **Crossing minimisation is where the time goes.** The naive transposition pass + recounts crossings globally per candidate swap: O(n³), which made a 424-block + function take **20.4 seconds**. Counting inversions with a Fenwick tree and + computing only the local `O(deg(a)·deg(b))` delta per swap took the same + function to **152 ms**, and the whole 128-function corpus from 21 s to 224 ms. + +## Rendering + +Nothing is pre-painted. A 424-block function lays out to ~13M cells, so +`graph.Painting` is an *index* — per-row horizontal runs, a bucketed interval +index of vertical runs, and point marks — and `GraphView.render_line(y)` asks it +for one row at a time, exactly like `ListingView`. Cost per frame is proportional +to the viewport, not the graph. + +Three zoom levels (`z`) trade detail for shape: **full** (address gutter + +instructions), **compact** (instructions only), **collapsed** (one summary row +per block). On `main` (87 blocks) that is a 1378×518 canvas down to 545×289. + +The **minimap** (`m`) is a coarse occupancy grid of the whole graph with the +viewport marked, drawn top-right and inset two columns — a `ScrollView` paints +its scrollbar over the last column, which otherwise eats the minimap's border. + +## Limits + +Above **400 blocks** the graph is refused with a message and you stay in the +listing. A CFG that size is not a picture anyone can read — IDA's own is a +hairball there too (1853 crossings on the worst function in `targets/echo`). +This is a feature, not a shortcoming. + +Known cosmetic gap: a back edge leaves its tail's *top* border (`┴`) and arrows +up into the head's *bottom* (`▲`). Correct and readable, but IDA runs loop edges +around the side of the graph. + +## Driving it + +`graph` is an RPC verb (see `docs/RPC.md`), and it reports **structure**, not box +drawing characters — a driver wants blocks and edges, not glyphs: + +```bash +drive raw graph action=open # Space +drive raw graph action=show # blocks, edges, ranks, cursor +drive raw graph action=block target=0x6250 +drive raw graph action=succ # J +drive raw graph action=zoom +``` + +## Offline tools + +- `experiments/cfg_dump.py` — freeze real CFGs from a binary to JSON. +- `experiments/graph_spike.py` — lay out and render a corpus function to stdout, + or `--stats` the whole corpus. Uses `idatui.graph`, so it exercises the + shipping engine with no worker in the loop. +- `experiments/graph_smoke.py` — end-to-end: tool → domain → layout. +- `experiments/graph_shot.py` — render the real view headless at a chosen size + (the pane you are in is usually too narrow to judge it). diff --git a/experiments/cfg_dump.py b/experiments/cfg_dump.py new file mode 100644 index 0000000..30b6ba9 --- /dev/null +++ b/experiments/cfg_dump.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Dump real basic-block CFGs to JSON, so graph-layout work can be done offline. + +Run with a python that has ``idapro`` (i.e. the worker python, /usr/bin/python3): + + /usr/bin/python3 experiments/cfg_dump.py targets/echo -o /tmp/cfg-echo.json + +Copies the binary to a scratch dir first (never touches the tracked .i64), opens +it with idalib, and writes one record per function: + + {"name","ea","blocks":[{"id","start","end","lines":[str],"succs":[[id,kind]]}]} + +``kind`` is "fall" (falls through to the next address), "jump" (unconditional or +taken branch) or "switch" (n-way). That is exactly the input a graph view needs; +everything after this point (layering, ordering, routing, rendering) is pure +python and needs no IDA at all. +""" +from __future__ import annotations + +import argparse +import json +import os +import shutil +import sys +import tempfile + + +def dump(path: str, want: list[str], max_blocks: int) -> list[dict]: + import idapro + + scratch = tempfile.mkdtemp(prefix="cfgdump-") + local = os.path.join(scratch, os.path.basename(path)) + shutil.copy2(path, local) + if idapro.open_database(local, run_auto_analysis=True) != 0: + raise SystemExit(f"failed to open {local}") + + import ida_funcs + import ida_gdl + import ida_lines + import ida_bytes + import idautils + import idaapi + + out = [] + try: + for fea in idautils.Functions(): + fn = ida_funcs.get_func(fea) + if not fn: + continue + name = ida_funcs.get_func_name(fea) + if want and not any(w in name for w in want): + continue + fc = ida_gdl.FlowChart(fn, flags=ida_gdl.FC_PREDS) + blocks = [] + index = {} + for i, bb in enumerate(fc): + index[bb.start_ea] = i + for bb in fc: + lines = [] + ea = bb.start_ea + while ea < bb.end_ea and ea != idaapi.BADADDR: + txt = ida_lines.tag_remove( + ida_lines.generate_disasm_line(ea, 0) or "") + lines.append(txt.rstrip()) + nxt = ida_bytes.next_head(ea, bb.end_ea) + if nxt <= ea: + break + ea = nxt + succs = [] + sl = list(bb.succs()) + for s in sl: + if s.start_ea not in index: + continue + if len(sl) > 2: + kind = "switch" + elif s.start_ea == bb.end_ea: + kind = "fall" + else: + kind = "jump" + succs.append([index[s.start_ea], kind]) + blocks.append({ + "id": index[bb.start_ea], + "start": bb.start_ea, + "end": bb.end_ea, + "lines": lines, + "succs": succs, + }) + if max_blocks and len(blocks) > max_blocks: + continue + out.append({"name": name, "ea": fea, "blocks": blocks}) + finally: + idapro.close_database(save=False) + shutil.rmtree(scratch, ignore_errors=True) + return out + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("binary") + ap.add_argument("-o", "--out", default="/tmp/cfg.json") + ap.add_argument("-f", "--func", action="append", default=[], + help="only functions whose name contains this (repeatable)") + ap.add_argument("--max-blocks", type=int, default=0, + help="skip functions with more blocks than this") + args = ap.parse_args() + + recs = dump(os.path.abspath(args.binary), args.func, args.max_blocks) + recs.sort(key=lambda r: len(r["blocks"]), reverse=True) + with open(args.out, "w") as f: + json.dump(recs, f) + tot = sum(len(r["blocks"]) for r in recs) + print(f"{len(recs)} functions, {tot} blocks -> {args.out}") + for r in recs[:12]: + print(f" {len(r['blocks']):4d} blocks {r['name']}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/experiments/graph_shot.py b/experiments/graph_shot.py new file mode 100644 index 0000000..5f0d45a --- /dev/null +++ b/experiments/graph_shot.py @@ -0,0 +1,66 @@ +"""Render the graph view headless at a chosen size and print it. + + ~/ida-venv/bin/python experiments/graph_shot.py [func] [cols] [rows] [zoom] + +The pane a person runs this in is usually too small to judge the layout, and the +pilot lays out synchronously at whatever size you ask for -- so this is the way +to actually look at the thing. +""" +import asyncio +import os +import shutil +import sys + +REPO = os.path.expanduser("~/dev/ida-tui-maybe") +sys.path.insert(0, REPO) +os.chdir(REPO) + +func = sys.argv[1] if len(sys.argv) > 1 else "sub_2297" +cols = int(sys.argv[2]) if len(sys.argv) > 2 else 170 +rows = int(sys.argv[3]) if len(sys.argv) > 3 else 55 +zoom = int(sys.argv[4]) if len(sys.argv) > 4 else 0 + +src, tmp = f"{REPO}/targets/echo", "/tmp/echo_shot" +shutil.copy(src, tmp) +for e in ".i64 .id0 .id1 .id2 .nam .til".split(): + try: + os.remove(tmp + e) + except OSError: + pass + +from idatui.app import IdaTui, GraphView # noqa: E402 +from idatui._sync import wait_for # noqa: E402 +from idatui.rpc import screen_text # noqa: E402 + + +async def main() -> None: + app = IdaTui(tmp, keepalive=False) + async with app.run_test(size=(cols, rows)) as pilot: + await wait_for(lambda: app.program is not None and app._cur is not None, + pilot.pause, 120) + ea = app.program.resolve(func) + fn = app.program.function_of(ea) + app._open_function(fn.addr, fn.name) + await wait_for(lambda: app._cur is not None and app._cur.ea == fn.addr, + pilot.pause, 60) + await pilot.pause(0.2) + await pilot.press("space") + gv = app.query_one(GraphView) + ok = await wait_for(lambda: app._active == "graph" and gv.lay is not None, + pilot.pause, 90) + if not ok: + print("graph never opened:", app.query_one("#status").render()) + return + for _ in range(zoom): + await pilot.press("z") + await pilot.pause(0.2) + await pilot.pause(0.4) + print(screen_text(app)["text"]) + print() + print("status:", app.query_one("#status").render()) + print("stats :", gv.lay.stats, "canvas", + f"{gv.lay.width}x{gv.lay.height}", "zoom", gv.ZOOMS[gv._zoom]) + app._save_on_exit = False + + +asyncio.run(main()) diff --git a/experiments/graph_smoke.py b/experiments/graph_smoke.py new file mode 100644 index 0000000..1b6e21e --- /dev/null +++ b/experiments/graph_smoke.py @@ -0,0 +1,107 @@ +"""End-to-end smoke for the graph path: the `flowchart` tool -> domain -> +idatui.graph layout, through a real idalib worker. + + ~/ida-venv/bin/python experiments/graph_smoke.py [funcname] + +Wants: VERDICT: OK +""" +import os +import shutil +import sys +import time + +REPO = os.path.expanduser("~/dev/ida-tui-maybe") +sys.path.insert(0, REPO) +os.chdir(REPO) + +src, tmp = f"{REPO}/targets/echo", "/tmp/echo_graph" +shutil.copy(src, tmp) +for e in ".i64 .id0 .id1 .id2 .nam .til".split(): + try: + os.remove(tmp + e) + except OSError: + pass + +from idatui.worker_client import WorkerClient # noqa: E402 +from idatui.domain import Program # noqa: E402 +from idatui import graph as G # noqa: E402 + +want = sys.argv[1] if len(sys.argv) > 1 else "main" +print("spawning worker + opening echo\u2026", flush=True) +t = time.time() +cl = WorkerClient(tmp) +cl.connect(progress=lambda m: None) +print(f" worker ready in {time.time()-t:.2f}s", flush=True) +prog = Program(cl) + +ok = True +ea = prog.resolve(want) +print(f"resolve({want!r}) = {ea:#x}", flush=True) + +t = time.time() +fc = prog.flowchart(ea) +print(f"flowchart() -> {time.time()-t:.2f}s", flush=True) +if fc is None: + print("VERDICT: FAIL (no flowchart)") + raise SystemExit(1) + +print(f" {fc.name} @ {fc.func_ea:#x}: {len(fc.blocks)} blocks, entry={fc.entry}") +nrows = sum(len(b.rows) for b in fc.blocks) +print(f" {nrows} listing rows attached") +ok &= nrows > 0 +if not nrows: + print(" !! no rows attached to any block") + +empty = [b for b in fc.blocks if not b.rows] +if empty: + ok = False + print(f" !! {len(empty)} blocks have NO rows, e.g. " + f"{[hex(b.start) for b in empty[:4]]}") + +b0 = fc.blocks[fc.entry] +print(f" entry block {b0.start:#x}-{b0.end:#x}:") +for h in b0.rows[:5]: + tags = ",".join(k for k, _ in (h.spans or ()))[:40] + print(f" {h.ea:#010x} {h.kind:<7} {h.text[:42]!r} spans[{tags}]") +ok &= any(h.spans for b in fc.blocks for h in b.rows) +if not any(h.spans for b in fc.blocks for h in b.rows): + print(" !! no colour spans came through \u2014 highlighting would be dead") + +# rows must tile the block exactly, or boxes will have holes +for b in fc.blocks: + eas = [h.ea for h in b.rows] + if eas and (min(eas) < b.start or max(eas) >= b.end): + ok = False + print(f" !! block {b.start:#x} has rows outside its range") + +blocks = [G.Block(id=b.id, start=b.start, end=b.end, succs=list(b.succs)) + for b in fc.blocks] + + +def sizer(b): + src_b = fc.blocks[b.id] + w = max([len(f"loc_{b.start:X}")] + + [len(h.text) + 12 for h in src_b.rows]) + 4 + return (w, len(src_b.rows) + 3) + + +t = time.time() +lay = G.layout(blocks, sizer, entry=fc.entry) +print(f"layout() -> {(time.time()-t)*1000:.0f} ms {lay.stats}", flush=True) +print(f" canvas {lay.width}x{lay.height}") +ok &= len(lay.nodes) == len(blocks) + +# every block must be reachable in the drawing, and rows must index it +covered = {n.id for r in range(lay.height) for n in lay.nodes_at_row(r)} +ok &= covered == {b.id for b in blocks} +if covered != {b.id for b in blocks}: + print(f" !! row index misses {sorted({b.id for b in blocks} - covered)[:5]}") + +hits = sum(1 for r in range(min(lay.height, 400)) + if lay.painting.cells_at_row(r, 0, lay.width)) +print(f" {hits} of the first {min(lay.height,400)} rows carry edge cells") +ok &= hits > 0 + +cl.close() +print("VERDICT:", "OK" if ok else "FAIL") +raise SystemExit(0 if ok else 1) diff --git a/experiments/graph_spike.py b/experiments/graph_spike.py new file mode 100644 index 0000000..547c829 --- /dev/null +++ b/experiments/graph_spike.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Render a CFG with idatui.graph, offline, to stdout. + +The layout engine now lives in ``idatui/graph.py`` (this started life as the +spike that proved it). What survives here is the offline harness: feed it a +corpus from ``cfg_dump.py`` and look at a function, or lay out the whole corpus +and check the cost. No IDA, no Textual, no worker -- so it iterates in +milliseconds when you're changing layout heuristics. + + /usr/bin/python3 experiments/cfg_dump.py targets/echo -o /tmp/cfg-echo.json + python3 experiments/graph_spike.py /tmp/cfg-echo.json --func sub_61D0 + python3 experiments/graph_spike.py /tmp/cfg-echo.json --stats +""" +from __future__ import annotations + +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from idatui import graph as G # noqa: E402 + +COLOR = { + G.E_UNCOND: "\033[38;5;39m", G.E_TRUE: "\033[38;5;40m", + G.E_FALSE: "\033[38;5;203m", G.E_SWITCH: "\033[38;5;178m", + G.E_BACK: "\033[38;5;135m", +} +DIM, RESET = "\033[38;5;244m", "\033[0m" +PAD = 1 + + +def build(rec: dict, max_lines: int): + """(blocks, sizer, texts) for one cfg_dump record.""" + texts: dict[int, list[str]] = {} + for b in rec["blocks"]: + lines = list(b["lines"]) + if max_lines and len(lines) > max_lines: + lines = lines[:max_lines - 1] + [f"... {len(b['lines']) - max_lines + 1} more"] + texts[b["id"]] = lines + blocks = [G.Block(id=b["id"], start=b["start"], end=b["end"], + succs=[(d, k) for d, k in b["succs"]]) + for b in rec["blocks"]] + + def sizer(b: G.Block) -> tuple[int, int]: + lines = texts[b.id] + label = f"loc_{b.start:X}" + widest = max([len(label) + 4] + [len(l) for l in lines] or [4]) + return (widest + 2 * PAD + 2, max(len(lines), 1) + 2) + + return blocks, sizer, texts + + +def render(lay: G.Layout, texts: dict[int, list[str]], color: bool) -> str: + rows = [] + for row in range(lay.height): + cells: dict[int, tuple[str, str]] = {} + for col, (ch, kind, _eid) in lay.painting.cells_at_row( + row, 0, lay.width).items(): + cells[col] = (ch, COLOR.get(kind, "")) + for n in lay.nodes_at_row(row): + label = f"loc_{n.block.start:X}" if n.block else "" + if row == n.y: + cells[n.x] = (G.BOX["tl"], DIM) + for i in range(1, n.w - 1): + cells[n.x + i] = (G.BOX["h"], DIM) + cells[n.x + n.w - 1] = (G.BOX["tr"], DIM) + tag = f" {label} " + if len(tag) <= n.w - 4: + for k, c in enumerate(tag): + cells[n.x + 2 + k] = (c, DIM) + if n.block is not None and n.block.selfloop: + cells[n.x + n.w - 2] = ("\u21ba", COLOR[G.E_BACK]) + elif row == n.y + n.h - 1: + cells[n.x] = (G.BOX["bl"], DIM) + for i in range(1, n.w - 1): + cells[n.x + i] = (G.BOX["h"], DIM) + cells[n.x + n.w - 1] = (G.BOX["br"], DIM) + else: + cells[n.x] = (G.BOX["v"], DIM) + cells[n.x + n.w - 1] = (G.BOX["v"], DIM) + for i in range(1, n.w - 1): + cells[n.x + i] = (" ", "") + lines = texts.get(n.id, []) + i = row - n.y - 1 + if 0 <= i < len(lines): + for k, c in enumerate(lines[i]): + cells[n.x + 1 + PAD + k] = (c, "") + line, cur, last = [], "", -1 + for col in sorted(cells): + ch, st = cells[col] + line.append(" " * (col - last - 1)) + if color and st != cur: + line.append(st or RESET) + cur = st + line.append(ch) + last = col + if color and cur: + line.append(RESET) + rows.append("".join(line)) + return "\n".join(rows) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("corpus") + ap.add_argument("--func", help="function name (default: the smallest)") + ap.add_argument("--stats", action="store_true", help="lay out the whole corpus") + ap.add_argument("--max-lines", type=int, default=8, + help="collapse blocks longer than this (0 = never)") + ap.add_argument("--no-color", action="store_true") + args = ap.parse_args() + + recs = json.load(open(args.corpus)) + if args.stats: + print(f"{'blocks':>7} {'nodes':>6} {'dummy':>6} {'layer':>6} " + f"{'canvas':>12} {'ms':>8} name") + tot = 0.0 + for r in sorted(recs, key=lambda r: len(r["blocks"])): + blocks, sizer, _ = build(r, args.max_lines) + lay = G.layout(blocks, sizer) + s = lay.stats + tot += s["ms"] + print(f"{s['blocks']:>7} {s['nodes']:>6} {s['dummies']:>6} " + f"{s['layers']:>6} {lay.width:>5}x{lay.height:<6} " + f"{s['ms']:>8.1f} {r['name']}") + print(f"total {tot:.0f} ms over {len(recs)} functions") + return 0 + + if args.func: + rec = next((r for r in recs if r["name"] == args.func), None) + if rec is None: + print("no such function; have: " + f"{', '.join(r['name'] for r in recs[:20])}", file=sys.stderr) + return 1 + else: + rec = min(recs, key=lambda r: len(r["blocks"])) + + blocks, sizer, texts = build(rec, args.max_lines) + lay = G.layout(blocks, sizer) + print(render(lay, texts, color=not args.no_color)) + print(f"\n{rec['name']}: {lay.stats} canvas {lay.width}x{lay.height}", + file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |
