aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-09 22:50:31 +0200
committerblasty <blasty@local>2026-08-09 22:50:31 +0200
commit4d490052f7d4dc0fccb3e718c801ef99e8646e22 (patch)
tree5b4cb17c9ac59978f14fb7351b229a19fb60d0b4
parentDocs: re-check ALL nine upstream findings against 0.3.2 (diff)
downloadida-tui-4d490052f7d4dc0fccb3e718c801ef99e8646e22.tar.gz
ida-tui-4d490052f7d4dc0fccb3e718c801ef99e8646e22.tar.xz
ida-tui-4d490052f7d4dc0fccb3e718c801ef99e8646e22.zip
SPEED: replace the historical backend table with a real 0.3.1 vs 0.3.2 A/B
The worker-vs-Code-Mode table was measured before 0.3.2 and with both workarounds active, so it answered a question nobody asks any more. Replaced with three configurations measured on the same box, rolling both checkouts back and forward: A old client WITH workarounds on 0.3.1 -- what shipped B current client on 0.3.1 -- what the workarounds were for C current client on 0.3.2 -- now Headline: the real-world gain is ~1.4x geomean, NOT the 6.9x the empty round trip advertises, and the doc says so in those words -- because the tempting number to quote is the wrong one. The A->C vs B->C gap is the actual story: stock 0.3.1 was 5.4x slower, so the workarounds had already recovered nearly everything and upstream mostly bought us the right to delete them. Also records the three cost classes (payload- / round-trip- / IDA-dominated) so the next person optimising here knows which lever moves which op, and the ~10% run-to-run spread so a sub-1.2x 'regression' doesn't start a hunt. Old worker table kept below, labelled historical. experiments/bench_ops.py is the harness, with the copy-to-/tmp-before-checkout trick documented in it.
-rw-r--r--.fastfeedback/SPEED.md60
-rw-r--r--experiments/bench_ops.py103
2 files changed, 155 insertions, 8 deletions
diff --git a/.fastfeedback/SPEED.md b/.fastfeedback/SPEED.md
index 8791768..fb2a171 100644
--- a/.fastfeedback/SPEED.md
+++ b/.fastfeedback/SPEED.md
@@ -203,19 +203,66 @@ includes the real work each key triggers, since `press` settles), 2.9s settling,
2.1s waits, ~5s scenario bodies. The rest of the gate is dominated by per-suite
IDA boot — 7 processes, each opening its own database.
-### Backend performance (worker vs Code Mode), after the two transport fixes
+### Backend performance: ida-codemode 0.3.1 vs 0.3.2
-| op | worker | codemode | |
+Measured `targets/bash`, function `main` (`0x1ca40`), 20 reps, medians, same box,
+rolling **both** checkouts back and forward. Harness: `experiments/bench_ops.py`
+(copy it to /tmp first — a `git checkout` of an old commit would replace it).
+
+Three configurations, because comparing the wrong pair gives the wrong answer:
+
+- **A** = old client **with** its settrace+packing workarounds, on 0.3.1 — *what shipped*
+- **B** = current client (workarounds removed) on 0.3.1 — *what the workarounds were for*
+- **C** = current client on 0.3.2 — *now*
+
+| op | A shipped | C now | A→C | B stock 0.3.1 | B→C |
+|---|---|---|---|---|---|
+| empty round trip | 2.06ms | 0.30ms | **6.90x** | 2.11ms | 7.05x |
+| `list_funcs` 500 | 17.55ms | 15.46ms | 1.14x | 260.47ms | **16.85x** |
+| `heads` 200 (listing page) | 6.45ms | 4.38ms | 1.47x | 26.40ms | 6.02x |
+| `decompile` (warm) | 34.04ms | 32.09ms | 1.06x | 107.52ms | 3.35x |
+| `flowchart` (graph) | 8.13ms | 6.18ms | 1.32x | 38.22ms | 6.19x |
+| `xrefs_to` | 3.79ms | 1.73ms | **2.19x** | 3.74ms | 2.16x |
+
+**The real-world gain is ~1.4x** (geomean over the five non-synthetic ops; 1.17x on
+summed work) — NOT the 6.9x the empty round trip advertises. Quote 1.4x, not 6.9x.
+
+The gap between the A→C and B→C columns is the point: **we had already worked
+around it**. Column B is what 0.3.2 actually fixed — stock 0.3.1 was 5.4x slower.
+Upstream catching up bought us the right to *delete* ~40 lines of workaround, not
+a big new win.
+
+Three patterns worth knowing before optimising anything here:
+
+| class | example | why |
+|---|---|---|
+| payload-dominated | `list_funcs`, `heads` | `_PACK_EPILOGUE` already fixed these (260→17.6ms); 0.3.2 adds only 1.1–1.5x |
+| round-trip-dominated | `xrefs_to` (2.19x) | small payload, so the workarounds never helped — A≈B≈3.7ms. Its whole gain is the new floor |
+| IDA-work-dominated | `decompile` (1.06x) | 32ms of Hex-Rays inside IDA. Nothing upstream can touch it |
+
+**You cannot feel any of this.** A listing page went 6.45→4.38ms, a graph
+8.13→6.18ms — both far under a frame before and after. The win is maintenance.
+
+**Run-to-run spread is ~10% on a loaded box** (a re-run of column C gave 17.4 /
+4.68 / 33.5 / 6.31 / 1.79). Treat one-decimal differences as noise, and don't
+chase a "regression" under 1.2x without re-running on an idle box — `uptime`
+first. The A→C ratios above survive that spread; the 1.06x on `decompile` does
+not, and should be read as "unchanged".
+
+Historical, for reference — the old private worker vs Code Mode, taken with both
+workarounds active and before 0.3.2:
+
+| op | worker | codemode (0.3.1) | |
|---|---|---|---|
| `heads` 200 rows | 2.65ms | 5.85ms | 2.2x |
| `heads` expect-hit | 2.16ms | 4.61ms | 2.1x |
| `disasm` 200 | 9.03ms | 5.25ms | **0.6x** |
| `decompile` cold | 162.6ms | 30.7ms | **0.2x** |
| `decomp_map` | 45.8ms | 47.9ms | 1.0x |
-| empty round trip | ~0.07ms | **2.0ms** | the floor |
+| empty round trip | ~0.07ms | **2.0ms** | the floor, now 0.29ms |
-Two things dominated, and **both are now fixed UPSTREAM in ida-codemode 0.3.2**,
-so neither is our problem any more:
+Two things dominated that port, and **both are now fixed UPSTREAM in ida-codemode
+0.3.2**, so neither is our problem any more:
- `sys.settrace` — the runtime used to install a trace that returned itself, i.e.
LINE tracing in every frame, making `ida_bytes.get_flags` 52x slower than
@@ -229,9 +276,6 @@ and **0.97x** respectively — nothing. The settrace strip (and its
only to pin encoder settings. Re-measure with
`PYTHONPATH=. ~/ida-venv/bin/python experiments/bench_pack_trace.py`.
-**Numbers in the table above predate 0.3.2** and were taken with both workarounds
-active; treat them as historical.
-
**The 2ms round-trip floor is GONE as of ida-codemode 0.3.2** — it was never ours,
and upstream removed it. Same-box A/B (editable checkout rolled back to `4195f21`
and forward again, 200 iterations, `targets/echo`):
diff --git a/experiments/bench_ops.py b/experiments/bench_ops.py
new file mode 100644
index 0000000..6ed64e0
--- /dev/null
+++ b/experiments/bench_ops.py
@@ -0,0 +1,103 @@
+"""Time a realistic idatui operation mix against whatever ida-codemode is installed.
+
+The companion to `bench_pack_trace.py`: that one isolates a single workaround,
+this one answers "how much faster is the whole client, on real operations".
+
+**It deliberately does not import anything version-specific**, so the SAME file
+can measure an OLD idatui checkout (with its `sys.settrace` strip and packing
+workarounds) and the current one. To compare across versions, copy it somewhere
+outside the repo first -- `git checkout` of an older commit would otherwise
+replace or delete it::
+
+ cp experiments/bench_ops.py /tmp/
+ # C: current client, current library
+ PYTHONPATH=. ~/ida-venv/bin/python /tmp/bench_ops.py
+
+ # B: current client against the OLD library (shows what the workarounds were for)
+ git -C ~/dev/ida-codemode checkout 4195f21
+ PYTHONPATH=. ~/ida-venv/bin/python /tmp/bench_ops.py
+
+ # A: the client as it SHIPPED on the old library, workarounds and all
+ git checkout d74b6f5 # the commit before the workaround removal
+ PYTHONPATH=. ~/ida-venv/bin/python /tmp/bench_ops.py
+
+ git checkout master && git -C ~/dev/ida-codemode checkout main # ALWAYS restore
+
+ida-codemode is installed **editable** into both venvs, so checking that repo out
+swaps the backend under the TUI with no reinstall -- which is what makes this A/B
+cheap. Results for 0.3.1 vs 0.3.2 are in `.fastfeedback/SPEED.md`.
+"""
+from __future__ import annotations
+
+import argparse
+import os
+import statistics
+import time
+
+from idatui.codemode_client import CodeModeClient
+
+
+def bench(fn, reps: int) -> tuple[float, float]:
+ """Best-of and median wall time in ms; best-of resists co-tenant noise."""
+ samples = []
+ for _ in range(reps):
+ started = time.perf_counter()
+ fn()
+ samples.append((time.perf_counter() - started) * 1000.0)
+ return min(samples), statistics.median(samples)
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser()
+ ap.add_argument("target", nargs="?", default="targets/bash")
+ ap.add_argument("--reps", type=int, default=20)
+ args = ap.parse_args()
+
+ client = CodeModeClient(os.path.abspath(args.target))
+ client.connect()
+ handle = client._handle
+
+ # Work on the biggest function we can find, so the payload-heavy operations
+ # are actually payload-heavy.
+ index = client.invoke("list_funcs", queries=[{"offset": 0, "count": 60}])
+ funcs = (index.get("result") or [{}])[0].get("data") or []
+ if not funcs:
+ print("VERDICT: FAIL - no functions")
+ return 1
+ big = max(funcs, key=lambda f: f.get("size") or 0)
+ ea = big["addr"] if isinstance(big["addr"], str) else hex(big["addr"])
+
+ ops = [
+ # Synthetic: isolates the per-operation floor (execute_sync marshalling).
+ ("empty round trip", lambda: handle.execute_python("result = 1")),
+ # Payload-dominated: what _PACK_EPILOGUE was written for.
+ ("list_funcs 500", lambda: client.invoke(
+ "list_funcs", queries=[{"offset": 0, "count": 500}])),
+ ("heads 200 (listing page)", lambda: client.invoke(
+ "heads", addr=ea, count=200, annotate=True)),
+ # IDA-work-dominated: Hex-Rays, nothing upstream can move.
+ ("decompile (warm)", lambda: client.invoke("decompile", addr=ea)),
+ ("flowchart (graph)", lambda: client.invoke("flowchart", addr=ea)),
+ # Round-trip-dominated: small payload, so only the floor matters.
+ ("xrefs_to", lambda: client.invoke("xref_query", direction="to", addr=ea)),
+ ]
+
+ print(f"# target={os.path.basename(args.target)} func={ea} reps={args.reps} "
+ f"backend={client.backend}")
+ results = {}
+ for name, fn in ops:
+ try:
+ for _ in range(3): # warm caches; the first sample is always an outlier
+ fn()
+ best, med = bench(fn, args.reps)
+ results[name] = med
+ print(f"{name:28} best {best:8.3f}ms median {med:8.3f}ms")
+ except Exception as exc: # one broken op must not lose the other five
+ print(f"{name:28} FAILED: {type(exc).__name__}: {str(exc)[:60]}")
+ client.close()
+ print("RESULT " + ";".join(f"{k}={v:.3f}" for k, v in results.items()))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())