aboutsummaryrefslogtreecommitdiffstats
path: root/experiments/graph_compare.py
diff options
context:
space:
mode:
Diffstat (limited to 'experiments/graph_compare.py')
-rw-r--r--experiments/graph_compare.py158
1 files changed, 0 insertions, 158 deletions
diff --git a/experiments/graph_compare.py b/experiments/graph_compare.py
deleted file mode 100644
index 0714554..0000000
--- a/experiments/graph_compare.py
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/usr/bin/env python3
-"""Compare the layout engines on a corpus: crossings, canvas, ambiguity, cost.
-
- python3 experiments/graph_compare.py .auto/cfg-corpus.json
- python3 experiments/graph_compare.py .auto/cfg-corpus.json --max-blocks 40
-
-Every number comes out of the shipping pipeline (``idatui.graph``), not out of a
-side channel, so it measures what the view will actually draw.
-
-The columns that matter:
-
-``X`` proper segment crossings -- what the SESE decomposition is for.
-``amb`` cells claimed by more than one edge. In a pixel renderer overlapping
- lines are invisible; in a terminal one cell holds one character, so an
- ambiguous cell is an edge the user cannot follow and ``edge_at`` gets
- wrong under the cursor.
-"""
-from __future__ import annotations
-
-import argparse
-import json
-import os
-import sys
-import time
-from collections import defaultdict
-
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from idatui import graph as G # noqa: E402
-
-
-def sizer(b: G.Block) -> tuple[int, int]:
- return (len(f"loc_{b.start:X}") + 6, 4)
-
-
-def real_sizer(rec: dict, max_lines: int):
- """Box sizes from the actual disassembly text, like the view's own sizer.
-
- Box shape is not a detail here: it decides how much horizontal room a layer
- needs, and therefore how far edges travel sideways. Comparing engines on
- uniform 20-column stubs measures the wrong graph.
- """
- texts = {}
- 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
-
- def size(b: G.Block) -> tuple[int, int]:
- lines = texts[b.id]
- return max((len(line) for line in lines), default=8) + 4, len(lines) + 2
- return size
-
-
-def segments(lay: G.Layout) -> dict[int, list[tuple[int, int, int, int]]]:
- """Per-edge segments as (x0, y0, x1, y1), rebuilt from the painting."""
- segs: dict[int, list] = defaultdict(list)
- for row, runs in lay.painting.hruns.items():
- for lo, hi, _style, eid in runs:
- segs[eid].append((lo, row, hi, row))
- for lo, hi, col, _style, eid in lay.painting.vruns:
- segs[eid].append((col, lo, col, hi))
- return segs
-
-
-def crossings(segs: dict[int, list], cap: int = 400_000) -> int | None:
- def orient(px, py, qx, qy, rx, ry):
- v = (qx - px) * (ry - py) - (qy - py) * (rx - px)
- return (v > 0) - (v < 0)
-
- keys = list(segs)
- n = pairs = 0
- for i, a in enumerate(keys):
- for b in keys[i + 1:]:
- for ax, ay, bx, by in segs[a]:
- for cx, cy, dx, dy in segs[b]:
- pairs += 1
- if pairs > cap:
- return None
- o1 = orient(ax, ay, bx, by, cx, cy)
- o2 = orient(ax, ay, bx, by, dx, dy)
- o3 = orient(cx, cy, dx, dy, ax, ay)
- o4 = orient(cx, cy, dx, dy, bx, by)
- if o1 != o2 and o3 != o4:
- n += 1
- return n
-
-
-def ambiguous(lay: G.Layout) -> tuple[int, int]:
- """(cells claimed by >1 edge, total edge cells)."""
- owners: dict[tuple[int, int], set[int]] = defaultdict(set)
- for row, runs in lay.painting.hruns.items():
- for lo, hi, _s, eid in runs:
- for c in range(lo, hi + 1):
- owners[(row, c)].add(eid)
- for lo, hi, col, _s, eid in lay.painting.vruns:
- for r in range(lo, hi + 1):
- owners[(r, col)].add(eid)
- return sum(1 for v in owners.values() if len(v) > 1), len(owners)
-
-
-def main() -> int:
- ap = argparse.ArgumentParser()
- ap.add_argument("corpus")
- ap.add_argument("--max-blocks", type=int, default=10 ** 9)
- ap.add_argument("--min-blocks", type=int, default=6)
- ap.add_argument("--real-sizer", action="store_true",
- help="size boxes from the disassembly text, as the view does")
- ap.add_argument("--max-lines", type=int, default=8)
- args = ap.parse_args()
-
- from idatui import graph_triskel
- if not graph_triskel.available():
- print("pytriskel not importable; set IDATUI_TRISKEL_PATH", file=sys.stderr)
- return 2
-
- recs = json.load(open(args.corpus))
- recs = [r for r in recs
- if args.min_blocks <= len(r["blocks"]) <= args.max_blocks]
- recs.sort(key=lambda r: len(r["blocks"]))
-
- print(f"{'blk':>4} | {'native ms':>9} {'canvas':>11} {'X':>5} {'amb':>5} | "
- f"{'tk ms':>7} {'canvas':>11} {'X':>5} {'amb':>5} | name")
- tot = {"native": 0.0, "triskel": 0.0}
- won = tied = lost = 0
- amb_tot = {"native": 0, "triskel": 0}
- for rec in recs:
- row = {}
- for engine in ("native", "triskel"):
- 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"]]
- size = real_sizer(rec, args.max_lines) if args.real_sizer else sizer
- t0 = time.perf_counter()
- lay = G.layout(blocks, size, engine=engine)
- ms = (time.perf_counter() - t0) * 1000
- tot[engine] += ms
- amb, _cells = ambiguous(lay)
- amb_tot[engine] += amb
- row[engine] = (ms, lay.width, lay.height, crossings(segments(lay)), amb)
- (nm, nw, nh, nx, na) = row["native"]
- (tm, tw, th, tx, ta) = row["triskel"]
- if nx is not None and tx is not None:
- won += tx < nx
- tied += tx == nx
- lost += tx > nx
- print(f"{len(rec['blocks']):>4} | {nm:>9.1f} {f'{nw}x{nh}':>11} "
- f"{str(nx):>5} {na:>5} | {tm:>7.1f} {f'{tw}x{th}':>11} "
- f"{str(tx):>5} {ta:>5} | {rec['name']}")
- print(f"\ntotal: native {tot['native']:.0f} ms, triskel {tot['triskel']:.0f} ms")
- print(f"crossings: triskel better on {won}, equal on {tied}, worse on {lost}")
- print(f"ambiguous cells: native {amb_tot['native']}, triskel {amb_tot['triskel']}")
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())