summaryrefslogtreecommitdiff
path: root/TODO.md
blob: 5b1140dfa650987818a879b58882ec4ad128398a (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
# 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`.

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 — DONE (measured: ~31% less CPU per scan, ~4% faster wall, ~100 MB less peak RSS)

Workload: `java -jar target/jns-1.0-SNAPSHOT.jar -i - -p all`
(20 000 hosts × 65 535 ports), `/usr/bin/time -v`, single before-run + 4 stable after-runs.

- [x] `ScanPortTask` / `ScanUdpPortTask`: `waitInMillis` / `waitInNanos` are now
      `static final WAIT_IN_NANOS` (`8d355c7`). Saves 16+ bytes on every port task.
- [x] `PortRangeIterator`: dead field `done` deleted, last `nextSetBit` result
      cached so `next()` no longer rescans; `clear()` removed with it (`4772393`).
- [x] Port range parsed **once per scan** in `Scanner`, carried in
      `ScanExecutionContext`; `ScanHostTask` no longer re-parses per host;
      `PortRangeIterator` iterates the shared BitSet read-only (`5fb3b80`).
- [x] `CliPrinter`: `stty size` regex is `static final TTY_SIZE_PATTERN` (`3ee01d8`).
      (The `getColumnWidth()` process spawn is kept on purpose — terminal resize.)
- [x] `CliPrinter` output: `String.format` + `stream().boxed().toList()` replaced
      by direct `StringBuilder` appends over the BitSet;
      `PortList.nextSetBit(int)` accessor added (`1b79d8c`).

Benchmark (before vs after-4, `before-optimization.txt` / `after-optimization-4.txt`):
| Metric       | before  | after-4 | delta     |
|--------------|---------|---------|-----------|
| User CPU (s) | 167.22  | 111.22  | −33 %     |
| Sys CPU (s)  | 143.43  | 103.14  | −28 %     |
| Total CPU (s)| 310.65  | 214.36  | −31 %     |
| Wall (s)     | 2114.63 | 2026.19 | −4.2 %    |
| Max RSS (KB) | 1228920 | 1121484 | −8 %      |

Interpretation: biggest CPU saver is the iterator change (applied ×1.3 bn ports).
The scan is network-latency-bound, so wall dropped only ~4 % (CPU busy-fraction
14 % → 10 %, voluntary context switches −14 %). Remaining headroom is Phase 3.

## 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.
      - Gate: Phase 2 measured −31 % CPU; only do Phase 3 if further GC pressure
        dominates (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`).