aboutsummaryrefslogtreecommitdiffstats
path: root/docs/PAGING_FINDINGS.md
blob: bd6c38f81984d265a63b609a6f53c4b1df58f330 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# Paging & scale findings (the "millions of lines" problem)

Measured against a real target: `libcrypto.so.3` (5.7 MB, **10,092 functions**,
biggest function **52,120 instructions**). These constraints drive the domain /
paging layer. The measurements below came from the former ida-pro-mcp tool
backend. The Code Mode port preserves the adapter response shapes and conservative
page sizes, but executes enumeration through ida-domain; old server caps and RTT
numbers are historical rather than Code Mode constraints.

## Response shape (list_* / *_query tools)

A query tool payload is a *list of per-query results*:

```json
{ "result": [ { "data": [ ... ], "next_offset": <int|null> } ] }
```

## Per-call caps are silent and dangerous

Every `count` / `max_instructions` parameter is honored up to a cap, then
**silently collapses to a tiny default (10)** — it does NOT clamp to the max.

| tool        | param            | honored up to | above cap |
|-------------|------------------|---------------|-----------|
| `list_funcs`| `count`          | ~700          | → 10      |
| `disasm`    | `max_instructions`| ~500         | → 10      |

**Rule:** clamp page sizes client-side. Use `list` page ≤ **500**, disasm window
≤ **500**. Never pass a large count hoping for clamping.

## Pagination: advance by `len(data)`, NOT `next_offset`

`next_offset` is just `offset + count`. If `count` exceeds the cap you get 10
rows but `next_offset` still jumps by `count`, **skipping ~99% of the data**.
Correct enumeration:

```python
offset = 0
while True:
    data = call("list_funcs", queries=[{"offset": offset, "count": 500}])[...]
    yield from data
    if len(data) < 500:   # short page => end
        break
    offset += len(data)   # advance by what we actually got
```

Full 10,092-function enumeration = 21 pages, **~3.1 s** (0.31 ms/func). Do this
once in the background with a progress indicator; page lazily otherwise.

## disasm offset paging is **O(offset)** — the main scroll hazard

`disasm addr=F offset=N` re-walks from the function start, so latency grows with
depth (window = 60 instrs):

| offset | latency |
|--------|---------|
| 0      | ~6 ms   |
| 1,000  | ~10 ms  |
| 5,000  | ~23 ms  |
| 20,000 | ~75 ms  |
| 50,000 | ~180 ms |

There is **no resumable cursor**: the payload's `cursor: {"next": N}` is just
informational; `disasm` has no cursor input param. So O(offset) is unavoidable
via this API.

Implications for the domain/paging layer:
- **Cache fetched windows** by `(func, offset)`. Re-scrolling a deep window
  currently re-pays the full ~180 ms (no server-side cache); our cache makes
  revisits instant.
- **Prefetch ahead** in background threads (client is concurrency-safe) so the
  next window is warm before the user reaches it.
- **Over-fetch** up to the ~500 cap per call and slice locally for the viewport,
  amortizing both RTT and the O(offset) walk during fast scrolling.
- **This only bites pathological functions.** The two 52k-insn outliers are
  almost certainly mis-analyzed data/unrolled tables; the next largest real
  function is 2,748 instrs (deepest offset ≈ 10 ms). Typical scrolling is fine.

## `include_total` is expensive — fetch once, cache

Computing `total_instructions` scans the whole function: **~217 ms** on the 52k
monster (vs ~10 ms without). Fetch total once when a function view opens (to size
the scrollbar) and cache it; never request it per-scroll.

disasm totals are **top-level** fields, not under `asm`:
`total_instructions`, `instruction_count`, `cursor`.

## Decompile: can hard-fail on huge functions (soft error)

`decompile` on the 52k-insn function returns, in ~3 ms:
`{"code": null, "error": "Decompilation failed at 0x3349c0"}` with
`isError == false`. The client surfaces this as **data, not an exception**
(correct). The pseudocode view must handle "decompilation failed" gracefully —
fall back to the disassembly view or show an error panel.

Code Mode returns the complete execution result directly; ida-tui no longer
needs MCP structured-content/download-URL recovery for large pseudocode bodies.

## Code Mode lifecycle

`CodeModeClient` owns an authenticated SSE lease on a registered database:

* A matching GUI is preferred and remains open when the TUI exits.
* Otherwise Code Mode reuses or starts a shared managed idalib worker.
* Releasing one lease never terminates another client's session. A managed
  worker saves and exits after its final lease under Code Mode's grace policy.
* Lease loss surfaces as `IDAConnectionError`; reconnect performs discovery
  again and may bind a newly-created instance. It does not silently swap the
  handle underneath an operation.
* `--ttl` and the old keepalive flag are compatibility no-ops; the lease itself
  carries heartbeats.

## Writable path requirement (operational)

Attaching to a registered GUI does not require ida-tui to write beside the input.
Creating a managed database does require a writable output path. Multi-binary
projects provide one in their sidecar. ida-tui does not sweep IDA scratch files,
because another registered session may own them.