# TODO — memory / allocation optimizations Background: the scanner runs millions of virtual threads (one per port probe today). Per-probe allocation churn, not peak RSS, is the main memory pressure source (GC load). Hot path: per host -> `ScanHostTask` -> per port -> `ScanPortTask` / `ScanUdpPortTask`. These items were reviewed and approved in this order. Nothing is implemented yet. Constraint (multi-scan): Scanner must remain able to run several parallel scans with different configurations and targets in one JVM. Anything static must be a true compile-time constant, never per-scan state. ## Phase 2 — approved small changes (do when green-lit, one commit each) - [ ] `ScanPortTask` / `ScanUdpPortTask`: make `waitInMillis` / `waitInNanos` `static final` (they are hardcoded 100 ms constants, not config — safe for multi-scan). Saves 16+ bytes on every port task. - [ ] `PortRangeIterator`: delete dead field `done` (never read). - [ ] `PortRangeIterator`: remember last `nextSetBit` result so `next()` does not rescan; `hasNext()` + `next()` currently both call `nextSetBit`. - [ ] Parse port range **once per scan** (in `Scanner`/context), not per host: remove `new PortRange(scan.getPorts())` from `ScanHostTask`; workers iterate the shared immutable BitSet with a plain int cursor. Saves per host: one string parse + up to two 8 KB BitSet copies. - [ ] `CliPrinter`: pre-compile the `stty size` regex as a `static final Pattern`. NOTE: the `getColumnWidth()` process spawn runs on purpose (terminal can be resized) — do NOT cache the width itself. - [ ] `CliPrinter.printErrors` / `printPortsResults` / `PortList.getPorts()`: replace `String.format` + `stream().boxed().toList()` with direct `StringBuilder` appends over the BitSet. ## Phase 2b — deferred (user: not implemented now, revisit later) - [ ] **`LinuxUdpSocketAPI` allocation churn per probe** (see `wrapNativeMethodCall`, `destination`, `sendEmptyDatagram`) Today every native call allocates: one errno call-state segment, one rebuilt `Object[] argv`, plus the `destination` segment per port (4–5 small allocations per probe). - Idea: allocate the errno state + argv arrays once per probe (or per worker) and pre-size the destination segment. - **Hard constraint (user):** the errno call-state must NOT be a single segment shared across all virtual threads. Any reuse must stay scoped to one worker thread / one probe at a time; otherwise keep status quo. - Also review the per-port `Arena.ofConfined()` + `new LinuxUdpSocketAPI` pair (confined arenas assume thread affinity which virtual threads do not guarantee across park/yield). ## Phase 3 — deferred (measure first) - [ ] **Replace "1 virtual thread + Callable + CompletionService per port" with a fixed worker pool per host** - Start N persistent virtual worker threads per host (N = min(maxWorkersPerHost, socketLimit), capped), pulling ports from a shared cursor and pushing `PortResult`s to a shared queue the consumer drains (today: `PortScanResultAccumulator` + `CompletionConsumer`). - Removes per port: the vthread, `ScanPortTask`, Integer boxes (×2), Runnable wrapper, `CompletableFuture`, `QueueElement`, `PollState`. - ~40–50 % fewer bytes per probe + far less GC + no per-port thread-create/park cycles. - Caveats: touches cancellation / "partial results on Ctrl-C" behavior (see `CompletionConsumer` contract). Keep `socketLimit` semaphore as the global throttle. Must stay compatible with parallel scanners. - Only do if Phase 2 numbers still show GC pressure dominated by per-port scaffolding. Measure with `-Xlog:gc*`, JFR `jdk.ObjectAllocationInNewTLAB`, wall-time on a fixed target set (e.g. `ips-for-testing.txt` × `1-1024`). ## Phase 4 — optional / measure first - [ ] **Reduce per-port atomic traffic in `Scan` counters**: `portStart()` / `portFinish()` do 3 CAS each per port. `socketCounter` increments in exact lockstep with `portCounter` in both tasks — consider dropping it or packing counters to cut ~1/3 of per-port CAS work. CPU-only win, profile before. ## Explicitly NOT doing (decisions) - `CliPrinter.getColumnWidth()`: keep the `stty size` process spawn per call (intentional: terminal can be resized). - `LinuxUdpSocketAPI`: do NOT share the errno call-state segment across worker threads (would be a data race). Keep per-call allocation. - Do NOT remove the default online check to "save" exception stack traces — it is a deliberate memory saver (prevents 1024 timed-out throws per offline host). Keep `PortResult` exceptions un-retained (accumulator already stores only `ExceptionInfo`).