aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.fastfeedback/SPEED.md98
1 files changed, 77 insertions, 21 deletions
diff --git a/.fastfeedback/SPEED.md b/.fastfeedback/SPEED.md
index e402ea1..996aefe 100644
--- a/.fastfeedback/SPEED.md
+++ b/.fastfeedback/SPEED.md
@@ -31,9 +31,16 @@ python3 tests/run.py graph -x # one suite by substring, fail-fa
**Narrow test — DEFAULT RUNG.** The pilot suite takes `--only <substr,...>`:
```bash
-~/ida-venv/bin/python tests/test_scenarios.py /tmp/scratch_bin --only rename # ~10s
+~/ida-venv/bin/python tests/test_scenarios.py /tmp/scratch_bin --only rename # ~2s
~/ida-venv/bin/python tests/test_scenarios.py --list # scenario names
+~/ida-venv/bin/python tests/test_scenarios.py targets/echo --profile # where the time went
```
+
+**`--profile` is how you find the next 20 seconds.** It reports, per scenario,
+seconds spent settling / in `wait()` / in keystrokes, and — the important one —
+any wait that **EXPIRED**, with its line number. An expired wait costs its whole
+timeout *and* means the check after it passed vacuously. Do not optimise this
+suite by guessing; run `--profile` and fix the top line.
`--only` matches substrings, so `--only listing` runs listing_view + listing_name_addr +
listing_make_string + listing_struct_expand. Name the scenario exactly to stay narrow.
@@ -92,6 +99,43 @@ run. Do not `kill -9` it — a hard-killed IDA wedges its database.
---
+## Simulated keypresses cost 85ms each unless you patch Textual
+
+The single biggest cost in this repo's UI suites was **Textual's own key path**,
+not our code. `Pilot.press` → `App._press_keys` calls `wait_for_idle` *twice per
+key*, and that helper sleeps in 20ms granules until *process* time stops
+advancing — a CPU-load heuristic for "the state is predictable now", which takes
+more granules the busier the box is. Measured on this box: **84ms per keypress**,
+and the pilot presses enough keys for that to be **23s of its 43s**.
+
+`tests/_fixtures.py: fast_keys()` replaces it (call it at import; every UI suite
+does). Two halves, and the second is the point:
+
+- `textual.app.wait_for_idle` → a bare `await asyncio.sleep(0)`.
+- `Pilot.press` → send the keys, then **`settle(app)`** — pump drained, workers
+ finished. Deleting the heuristic *without* this broke nine checks, so it was
+ doing a job, badly; `settle` is strictly stronger and ~2ms.
+
+**What `settle` still cannot see — and what to do instead:**
+
+| driven by | example | gate |
+|---|---|---|
+| a worker | decompile, navigation, index load | `settle(app, pred)` |
+| a **timer** | the function filter's `set_timer(0.08)` debounce | `wait(lambda: rows < full)` |
+| a **frame** | `widget.region` / `size` / a repaint trace | `wait(lambda: inp.region.height >= 1)` |
+
+A settled app has not necessarily been laid out or painted. Every site in this
+repo that needs a frame or a debounce is commented as such — if a check that
+reads geometry or a filtered row count starts flaking, that is the reason, and
+the fix is a wait on the effect, never a longer sleep.
+
+The frame-dependent ones found this way, as a shopping list of what to suspect:
+`si.region` after the search prompt opens, `gv._minimap_rect()` / `gv.size` after
+the graph opens, `gv.render_line()` scraping box glyphs, and a `render_line`
+trace asserting a repaint happened at a restored scroll. All four passed for
+years on the 85ms-per-key sleep and failed within three runs without it — they
+were always races, just paid-for ones.
+
## The four ways a test here wastes minutes
Every slow suite in this repo was slow for one of these, not for doing real work.
@@ -119,32 +163,45 @@ Check them before optimising anything else.
listing. → Give each phase its own temp copy (`fresh_copy`). Never `os.remove` an
`.i64` a suite is about to reopen.
+5. **A flat `pause(d)` where a gate belongs.** `Ctx.pause` in the pilot *is*
+ `settle` now (`d` is the upper bound, not the cost), which took 20.4s of
+ fixed sleeping down to 2.8s across ~140 call sites. `Ctx.sleep(d)` is the
+ escape hatch for the genuinely timer-driven; reach for it only after
+ checking the table above. In the other suites the same conversion took
+ `test_trace_ui` from 19.6s to 5.5s — it was 13.5s of `pilot.pause(1.0)`.
+
**`settle(app, pred, timeout=...)`** (`idatui/_sync.py`) is the one true gate: it drains
the message pump, waits for workers, and returns the moment `pred` holds. It is what the
live RPC layer uses, so tests and driver agree on what "done" means. A bare
`settle(app)` (no pred) means "the app finished reacting" — the right gate when the edit
may legitimately do nothing (e.g. carving random bytes).
-## Measured timings (2026-08-07, this box, warm)
+## Measured timings (2026-08-07, this box, warm; keypress work 2026-08-08)
| command | time | notes |
|---|---|---|
| `python3 tests/run.py --fast` | **0.6s** | 302 checks, all pure suites |
| `python3 tests/test_graph.py` | ~1s | 86 checks, pure layout engine |
| `python3 tests/test_codemode_client.py` | ~0.1s | 14 checks, fakes the DatabaseHandle |
-| `~/ida-venv/bin/python tests/test_rawimage_rpc.py` | ~60s | 21 checks, owns batch rename |
-| pilot boot (first scenario) | ~10-25s | opens the DB; seeded from `.pristine.i64` |
-| pilot `--only graph` | ~10s | 50 checks |
-| **pilot full (`test_scenarios.py`)** | **62.2s** | 56 scenarios, 301 checks (was 166s) |
-| `test_trace_ui.py` | 19.4s | 39 checks |
+| pilot boot (first scenario) | ~1.7s | opens the DB; seeded from `.pristine.i64` |
+| pilot `--only rename` | ~2s | 11 checks |
+| **pilot full (`test_scenarios.py`)** | **21.2s** | 57 scenarios, 313 checks (was 166s, then 62s) |
| `test_rawimage_rpc.py` | 7.5s | 21 checks, owns batch rename |
-| `test_project_ui.py` | 8.1s | 30 checks |
-| `test_thumb_ui.py` | **8.5s** | 20 checks — was 313s AND crashing |
-| `test_blob_ui.py` | **4.1s** | 30 checks — was 39.8s |
-| `tests/run.py` (everything) | **115s** | 788 checks. was ~9m20s. BACKGROUND ONLY |
+| `test_trace_ui.py` | **5.5s** | 39 checks — was 19.6s |
+| `test_trace_rpc.py` | 5.1s | 45 checks |
+| `test_project_ui.py` | **3.8s** | 30 checks — was 8.1s |
+| `test_thumb_ui.py` | **3.6s** | 20 checks — was 313s AND crashing |
+| `test_blob_ui.py` | **1.3s** | 30 checks — was 39.8s |
+| `tests/run.py` (everything) | **49s** | 800 checks. was ~9m20s, then 117s |
+
+IDA-suite total: ~9m21s → 117s (the four wastes below) → **49s** (keypresses and
+the pause→settle conversion). No check was removed to get there; the suite gained
+12 and the two runs behind these numbers were 49.2s and 48.9s, 800/800 both.
-IDA-suite total went from ~9m21s to ~2m16s (4.1x) by fixing the four wastes above —
-not by removing a single check.
+Where the pilot's remaining ~20s goes (`--profile`): 10.8s keystrokes (which now
+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
@@ -205,14 +262,13 @@ picks whichever client the checked-out tree has, so it runs on master too).
## Known-flaky
-- `test_scenarios.py --only listing_view` (**codemode backend only**). Passed 2 runs,
- failed the 3rd, on an unchanged tree and a fresh scratch DB. Symptom: `listing shows
- data heads (not just code)` reports `{'label','sep','funchdr','code'}` and
- `total=1500`, i.e. the model is read before its pages have materialised. Adding *any*
- delay (even a single `stderr.write`) makes it pass, which is the signature of a settle
- race, not a logic bug. The scenario waits on `c.lst.total > 0`, which becomes true
- before the rows exist. **Do not chase this as a regression without reproducing it
- 3+ times.** Real fix would be a wait on materialised rows, not on `total`.
+- _(fixed 2026-08-08)_ `test_scenarios.py --only listing_view` used to fail about one
+ run in three: `listing shows data heads (not just code)` reported
+ `{'label','sep','funchdr','code'}`, i.e. the model was read before its pages had
+ materialised. The scenario waited on `c.lst.total > 0`, which is computed from the
+ segment size and is true **before any row exists** — the textbook "wait on a signal
+ that is set too early". It now waits for a materialised data row. Nothing else is
+ known-flaky; if something starts flaking, check the timer/frame table above first.
---