From d7966c178047d190faba7100a40385eb00f5894b Mon Sep 17 00:00:00 2001 From: idatui Date: Sun, 9 Aug 2026 13:55:31 +0200 Subject: Graph: fix the triskel fallbacks, and say why when it still falls back "engine triskel -> native+triskel-failed" was unreadable and, worse, gave no reason: the cause went to a logger a TUI user never sees. Dumped all 400 functions of bin/ls (echo's 128 were not enough) and swept them at three zoom levels: 6 of 1200 layouts fell back, all of them my own _verify tripping over a detour that could not be placed. - the detour jumped to the nearest side of the FIRST box in the way, which in a dense layout is usually inside the next box along. It now collects every box the run passes and picks the nearest genuinely free line. - it skipped the first and last segments because they carry the port and the arrowhead. But that is exactly where the failures were: triskel is happy to park a block directly above its successor and drive the final approach straight through it. Those segments may now move ALONG their own box's border, which is free almost every time. - repairs are swept to a fixed point: moving one segment stretches its neighbours, which can push those into a box. 0/1200 fallbacks after that. Then the bigger corpus turned up a second, genuinely upstream defect: superimposing SESE regions can leave two blocks a couple of columns into each other (2 of ls's 400 functions, in float space, before rounding). Cosmetic in a PNG; here the boxes are made of text, so one block's disassembly overwrites another's. _verify now checks it and falls back, which is the right trade. Reporting, so this is never mute again: - stats["engine_error"] carries the reason, the status line shows it, and the label is "native (triskel failed)". - the corpus test asserts fallbacks are rare AND explained, rather than asserting they never happen. Also lowered AUTO_TRISKEL_MAX_BLOCKS 250 -> 180. Layout runs on every zoom keypress and triskel knees hard past ~175 blocks (174: 66ms, 233: 489ms, 329: 555ms). The old cap allowed a 489ms stall. The corpus timing check now measures only sizes `auto` can actually reach, plus a 5s ceiling so nothing blows up quadratically when forced. --- idatui/graph.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) (limited to 'idatui/graph.py') diff --git a/idatui/graph.py b/idatui/graph.py index fbaf87f..f40de07 100644 --- a/idatui/graph.py +++ b/idatui/graph.py @@ -702,11 +702,18 @@ def _native_engine(g: _Graph, root: int) -> tuple[list[Route], int]: #: Engine names accepted by ``layout(engine=...)`` and ``IDATUI_GRAPH_ENGINE``. ENGINES = ("auto", "native", "triskel") -#: Above this many blocks ``auto`` stays native: triskel's SESE decomposition -#: costs ~10x at 424 blocks (1.5s vs 145ms), and a layout that blocks the UI for -#: a second is worse than a layout with more crossings. Measured, see -#: docs/TRISKEL_EVAL.md. -AUTO_TRISKEL_MAX_BLOCKS = 250 +#: Above this many blocks ``auto`` stays native. Layout runs on every open and +#: every zoom keypress, so this is an interactivity budget, not a correctness +#: one. Triskel's cost knees hard (measured on `ls`, 400 functions): +#: +#: blocks 174 233 256 329 424 495 +#: native 25 72 23 93 144 203 ms +#: triskel 66 489 266 555 1501 1968 ms +#: +#: 180 keeps the worst auto-triskel layout in the tens of milliseconds. Raising +#: it buys prettier pictures of graphs nobody can read anyway -- the view +#: refuses to draw past 400 blocks at all. +AUTO_TRISKEL_MAX_BLOCKS = 180 def _pick_engine(engine: str | None, nblocks: int) -> str: @@ -735,6 +742,7 @@ def layout(blocks: list[Block], sizer, entry: int | None = None, g, root = _build(blocks, sizer, entry) layers = 0 + err = None if not g.nodes: routes = [] elif name == "triskel": @@ -744,8 +752,10 @@ def layout(blocks: list[Block], sizer, entry: int | None = None, except Exception as exc: # noqa: BLE001 # Native code with a history of throwing on degenerate CFGs. The # graph view is a convenience; losing it beats losing the session. + # Keep the REASON: a fallback the user can see but not explain is + # only marginally better than a crash. _LOG.warning("triskel layout failed (%s), falling back", exc) - name = "native+triskel-failed" + name, err = "native (triskel failed)", f"{type(exc).__name__}: {exc}" g, root = _build(blocks, sizer, entry) routes, layers = _native_engine(g, root) else: @@ -836,6 +846,7 @@ def layout(blocks: list[Block], sizer, entry: int | None = None, "edges": len(g.edges), "back": sum(1 for e in g.edges if e.back), "engine": name, + "engine_error": err, "ms": (time.perf_counter() - t0) * 1000, } return Layout(nodes=order, by_id={n.id: n for n in g.nodes.values()}, -- cgit v1.3.1-sl0p