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. --- tests/test_graph.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) (limited to 'tests/test_graph.py') diff --git a/tests/test_graph.py b/tests/test_graph.py index d882f94..96d4f85 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -211,14 +211,38 @@ def t_corpus(path: str) -> None: print(f"\ncorpus: {len(recs)} functions from {path}") worst_ms = 0.0 worst_name = "" + fellback: list[tuple[str, str | None]] = [] + worst_any_ms = 0.0 + worst_any_name = "" t0 = time.perf_counter() for rec in recs: 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"]] lay = layout(blocks, sizer) - if lay.stats["ms"] > worst_ms: + # A SILENT fallback is the failure mode that matters here: the engine + # under test quietly stops being the engine under test, and every + # invariant below then passes for the wrong reason. `ls` main (329 + # blocks) used to fall back on all three zoom levels because a final + # approach was routed through the block above its target. + # + # Falling back is legitimate -- it is how an upstream layout defect is + # kept off the screen -- so this asserts it is rare and explained, + # not that it never happens. + if ENGINE != "auto" and lay.stats["engine"] != ENGINE: + fellback.append((rec["name"], lay.stats.get("engine_error"))) + check(bool(lay.stats.get("engine_error")), + f"corpus {rec['name']}: a fallback must record its reason") + # Time the engine only on the functions it would actually be ASKED for. + # `auto` hands anything over AUTO_TRISKEL_MAX_BLOCKS to native, and the + # view refuses to draw past 400 blocks at all, so a forced triskel run + # on a 495-block monster times a call the app cannot make. + reachable = (ENGINE != "triskel" + or len(blocks) <= G.AUTO_TRISKEL_MAX_BLOCKS) + if reachable and lay.stats["ms"] > worst_ms: worst_ms, worst_name = lay.stats["ms"], rec["name"] + if lay.stats["ms"] > worst_any_ms: + worst_any_ms, worst_any_name = lay.stats["ms"], rec["name"] check(no_box_overlap(lay), f"corpus {rec['name']}: boxes must not overlap") check(len(lay.nodes) == len(blocks), f"corpus {rec['name']}: every block is placed") @@ -230,7 +254,17 @@ def t_corpus(path: str) -> None: total = (time.perf_counter() - t0) * 1000 print(f" laid out {len(recs)} functions in {total:.0f} ms " f"(worst {worst_ms:.0f} ms: {worst_name})") - check(worst_ms < 2000, f"corpus: worst layout under 2s ({worst_ms:.0f} ms)") + if fellback: + print(f" {len(fellback)} fell back to native:") + for name, why in fellback: + print(f" {name}: {why}") + check(len(fellback) <= max(2, len(recs) // 20), + f"corpus: {ENGINE} fell back on {len(fellback)}/{len(recs)} functions") + check(worst_ms < 2000, f"corpus: worst REACHABLE layout under 2s " + f"({worst_ms:.0f} ms: {worst_name})") + # Nothing may blow up quadratically even when forced past its own limits. + check(worst_any_ms < 5000, f"corpus: worst layout at any size under 5s " + f"({worst_any_ms:.0f} ms: {worst_any_name})") def main() -> int: -- cgit v1.3.1-sl0p