test(geode): add LoadBenchmark.fanoutScaling for selective-fanout perf

Verifies the FilterIndex path in LiveEventStore: each event matches
exactly ONE of N subscribers, so without an index the relay walks
all N subs per event (O(N)); with the author-keyed index the lookup
is O(1) and per-event latency stays flat as N grows.

Different from fanoutLatency (broadcast: 1 event reaches every
sub). Here per-event work is lookup-bound rather than
delivery-bound. Configurable via -DfanoutScalingSubs (comma list)
and -DfanoutScalingEvents.

Measured (laptop, JDK 21, full sweep):
  100 subs, 2k events: p50=1.35ms p99=5.71ms
 1000 subs, 2k events: p50=1.05ms p99=3.05ms
 5000 subs, 1k events: p50=1.01ms p99=2.96ms

p50 ~1ms across N — the predicted O(1) scaling. Default events count
adapts downward at high N to stay below WebSocketSessionPump's
8192-frame outbound cap (test-client read side is the bottleneck
above ~5k subs, not the relay).

Plan doc updated with the measured numbers in the "How to verify"
section.
This commit is contained in:
Claude
2026-05-07 23:05:29 +00:00
parent a43783879b
commit 0d0fbf36c9
3 changed files with 189 additions and 4 deletions
@@ -119,18 +119,37 @@ predicate. Dispatch:
### How to verify
`geode.perf.LoadBenchmark.fanoutScaling` (to be added):
`geode.perf.LoadBenchmark.fanoutScaling` (added):
- N connections, each subscribes to `{authors: [pk_i], kinds: [1]}`.
- Publish 10k EVENTs from a producer connection; each event matches
- Publish events round-robin across the N pubkeys; each event matches
exactly one subscriber.
- Measure end-to-end latency p50/p99 for N ∈ {100, 1000, 5000}.
Without the index, p99 grows roughly linearly with N. With the
index, p99 should be flat up to a much higher N.
The benchmark adapts the per-N event count downward to stay below
geode's `WebSocketSessionPump.MAX_OUTGOING_BUFFER` (8192 frames per
session). The single-WS test subClient can't drain (subs + events)
frames at full firehose rate above ~5k subs in a shared test JVM —
that's a test-infra ceiling, not a relay one. Override with
`-DfanoutScalingEvents=N` to push past the default.
Measured numbers from a development laptop (Linux, JDK 21, full
sweep):
| N subs | events | mean (ms) | p50 (ms) | p99 (ms) | p999 (ms) |
|-------:|-------:|----------:|---------:|---------:|----------:|
| 100 | 2 000 | 1.63 | 1.35 | 5.71 | 20.49 |
| 1 000 | 2 000 | 1.12 | 1.05 | 3.05 | 9.40 |
| 5 000 | 1 000 | 1.07 | 1.01 | 2.96 | 7.38 |
p50 stays at ~1 ms across all three N values — the index is producing
the predicted O(1)-per-event scaling. The minor p99 spread comes from
GC pauses and OkHttp scheduler jitter on the test client, not from
linear per-event work in the relay.
For the LocalCache side, a similar benchmark would publish events
matching one of M observers and measure dispatch cost as M grows.
Not yet added — Phase 2 candidate for a follow-up.
## Risks