22
33# Add Flow-partitioned stream client implementation
44
5- Status: ready-for-agent
5+ Status: done
66Type: AFK
77Category: performance
88
@@ -35,8 +35,8 @@ final shard boundary.
3535
3636Flow and GenStage already provide the lower-level shape we want:
3737
38- - ` Flow.partition/2 ` materializes partitioned producers with
39- ` GenStage.PartitionDispatcher ` ;
38+ - ` Flow.into_stages/3 ` can use ` GenStage.PartitionDispatcher ` as the
39+ consumer-facing dispatcher ;
4040- ` GenStage.PartitionDispatcher ` routes events to named partitions;
4141- each consumer subscribes with ` partition: shard_index ` ;
4242- each partition consumer can own shard-local stream sender state and receive
@@ -65,7 +65,7 @@ The implementation should keep the benchmark model unchanged:
6565
6666The expected data path is:
6767
68- ` Flow source -> Flow.partition/2 -> one GenStage shard sink per partition `
68+ ` Flow source -> GenStage.PartitionDispatcher -> one GenStage shard sink per partition `
6969
7070Partitioning should be deterministic and diagnosable. Prefer explicit stream
7171index modulo shard count over opaque stream-handle hashing:
@@ -94,24 +94,24 @@ timed function returns.
9494
9595## Acceptance criteria
9696
97- - [ ] ` bench/moqxprobe ` contains a new named stream implementation,
97+ - [x ] ` bench/moqxprobe ` contains a new named stream implementation,
9898 ` flow_partitions ` , registered alongside the existing stream
9999 implementations.
100- - [ ] The implementation uses ` Flow.partition/2 ` or an equivalent
100+ - [x ] The implementation uses ` Flow.partition/2 ` or an equivalent
101101 GenStage-native partitioned producer path instead of manually routing
102102 payloads through ` send(worker, event) ` .
103- - [ ] The implementation remains Flow-fed and uses the same payload event shape
103+ - [x ] The implementation remains Flow-fed and uses the same payload event shape
104104 as the other stream implementations.
105- - [ ] Each shard owns its stream sender state and handles send completions in
105+ - [x ] Each shard owns its stream sender state and handles send completions in
106106 its own OTP process.
107- - [ ] The implementation reports configured shard count, active shard count,
107+ - [x ] The implementation reports configured shard count, active shard count,
108108 routed/received payload count, completion count, queue/window pressure,
109109 and timing diagnostics comparable to ` sender_shards ` .
110- - [ ] Fake-target tests prove the implementation completes the same workload
110+ - [x ] Fake-target tests prove the implementation completes the same workload
111111 and receiver evidence as the existing implementations.
112- - [ ] A fake/local Benchee comparison records ` context_owner ` , ` stream_owner ` ,
112+ - [x ] A fake/local Benchee comparison records ` context_owner ` , ` stream_owner ` ,
113113 ` sender_shards ` , and ` flow_partitions ` with the same input shape.
114- - [ ] Promote ` flow_partitions ` to current best only if it is cleaner and at
114+ - [x ] Promote ` flow_partitions ` to current best only if it is cleaner and at
115115 least performance-neutral against ` sender_shards ` ; otherwise keep it as a
116116 historical/rejected architecture with the evidence recorded.
117117
@@ -138,3 +138,153 @@ cleaner OTP/process ownership model:
138138
139139If the experiment fails, record why and keep it in the registry as historical
140140or rejected evidence rather than deleting the trace.
141+
142+ ## Comments
143+
144+ ### 2026-06-19 implementation
145+
146+ Implemented the first ` flow_partitions ` candidate.
147+
148+ Important design correction: ` Flow.partition/2 ` partitions internal Flow
149+ stages, but ` Flow.into_stages/3 ` still defaults to a demand dispatcher for the
150+ external consumers. The final consumer boundary must use
151+ ` GenStage.PartitionDispatcher ` directly, so ` MOQXProbe.Traffic ` now provides
152+ ` start_partitioned_payloads/3 ` that starts a Flow source and attaches sinks
153+ with:
154+
155+ ``` elixir
156+ dispatcher: {GenStage .PartitionDispatcher , partitions: 0 .. (n - 1 ), hash: hash}
157+ ```
158+
159+ Added ` MOQXProbe.Traffic.StreamPartitionSink ` :
160+
161+ - one GenStage consumer per partition;
162+ - explicit partition subscription;
163+ - shard-local stream sender state;
164+ - per-stream queue and send window ownership;
165+ - backend send-completion handling in ` handle_info/2 ` ;
166+ - final snapshot emission before normal stop.
167+
168+ Termination is explicit. The benchmark driver appends one
169+ ` %{control: :source_eof, partition: partition} ` event per partition after the
170+ payload workload. A partition sink treats source EOF as "no more payload events
171+ for this partition", not as QUIC FIN. The sink stops only when source EOF has
172+ arrived, local queues are empty, in-flight sends are zero, and expected send
173+ completions have been observed.
174+
175+ Initial local tests cover:
176+
177+ - direct partition sink lifecycle through Flow source EOF and send-completion
178+ drain;
179+ - fake-target ` flow_partitions ` benchmark completion through
180+ ` bench/stream_clients.exs ` ;
181+ - CLI/registry inclusion as a ` candidate ` implementation.
182+
183+ Validation:
184+
185+ - ` cd bench/moqxprobe && mix format --check-formatted && mix test && mix credo --strict ` :
186+ 54 tests passed, Credo strict found no issues.
187+ - Single-candidate smoke:
188+ ` mix run bench/stream_clients.exs -- --target fake --input flow-generated --implementation flow_partitions --stream-count 8 --payload-count 20 --payload-size 256 --stream-send-window 4 --sender-shard-count 2 --benchee-warmup 0 --benchee-time 1 --benchee-memory-time 0 --benchee-reduction-time 0 --git-sha test-smoke `
189+ reached about ` 1.55 K ips ` (` 646.35 us ` average).
190+ - Four-way fake comparison on the same tiny shape:
191+ ` stream_owner ` about ` 1.89 K ips ` , ` sender_shards ` about ` 1.76 K ips ` ,
192+ ` flow_partitions ` about ` 1.65 K ips ` , and ` context_owner ` about ` 0.81 K ips ` .
193+
194+ Initial decision: keep ` sender_shards ` as ` current_best ` . ` flow_partitions `
195+ was cleaner in ownership and termination semantics, but the first small local
196+ smoke was not performance-neutral against ` sender_shards ` , so it remained a
197+ ` candidate ` architecture trace.
198+
199+ ### 2026-06-19 tuning pass
200+
201+ Tuned the fake-target comparison across shard/partition count and GenStage
202+ demand/backlog.
203+
204+ Important correctness boundary:
205+
206+ - ` --flow-stages 2 ` is unsafe for the current ordered stream workload. It can
207+ reorder payload events for the same stream and deliver a non-final payload
208+ after the ` finish?: true ` payload, causing ` :send_side_finished ` . The CLI now
209+ rejects ` --flow-stages > 1 ` for this stream script until a source architecture
210+ can preserve per-stream order with parallel source stages.
211+
212+ Small shape, ` 32 x 100 x 1180 ` , window ` 16 ` , fake target:
213+
214+ - shard/partition ` 2 ` , demand ` 64 ` , queue ` 256 ` : ` sender_shards ` ` 134.42 ips ` ,
215+ ` flow_partitions ` ` 125.75 ips ` ;
216+ - shard/partition ` 4 ` , demand ` 64 ` , queue ` 256 ` : ` flow_partitions `
217+ ` 145.25 ips ` , ` sender_shards ` ` 137.64 ips ` ;
218+ - shard/partition ` 8 ` , demand ` 64 ` , queue ` 256 ` : ` flow_partitions `
219+ ` 134.34 ips ` , ` sender_shards ` ` 129.71 ips ` ;
220+ - shard/partition ` 4 ` , demand ` 128 ` , queue ` 512 ` : ` flow_partitions `
221+ ` 322.71 ips ` , ` sender_shards ` ` 267.83 ips ` ;
222+ - shard/partition ` 4 ` , demand ` 256 ` , queue ` 1024 ` : ` flow_partitions `
223+ ` 337.18 ips ` , ` sender_shards ` ` 279.93 ips ` ;
224+ - shard/partition ` 8 ` , demand ` 256 ` , queue ` 1024 ` : ` flow_partitions `
225+ ` 422.29 ips ` , ` sender_shards ` ` 262.85 ips ` ;
226+ - shard/partition ` 16 ` , demand ` 256 ` , queue ` 1024 ` : ` flow_partitions `
227+ ` 409.07 ips ` , ` sender_shards ` ` 210.56 ips ` .
228+
229+ Four-way tuned small-shape matrix with shard/partition ` 8 ` , demand ` 256 ` ,
230+ queue ` 1024 ` :
231+
232+ - ` flow_partitions ` : ` 410.49 ips ` ;
233+ - ` sender_shards ` : ` 264.25 ips ` ;
234+ - ` stream_owner ` : ` 181.28 ips ` ;
235+ - ` context_owner ` : ` 43.52 ips ` .
236+
237+ Large fake shape, ` 128 x 1000 x 1180 ` , window ` 16 ` , demand ` 256 ` ,
238+ queue ` 1024 ` :
239+
240+ - shard/partition ` 8 ` : ` flow_partitions ` ` 13.64 ips ` (` 73.34 ms ` average),
241+ ` sender_shards ` ` 8.85 ips ` (` 113.01 ms ` average);
242+ - shard/partition ` 16 ` : ` flow_partitions ` ` 13.50 ips ` (` 74.10 ms ` average),
243+ ` sender_shards ` ` 6.68 ips ` (` 149.65 ms ` average).
244+
245+ Decision: promote ` flow_partitions ` to ` current_best ` for fake/local
246+ process-model work. Keep ` sender_shards ` as a historical comparison trace.
247+ Best observed local fake settings for now:
248+
249+ ` --sender-shard-count 8 --flow-stages 1 --min-demand 128 --max-demand 256 --max-queue-depth 1024 `
250+
251+ ### 2026-06-19 local quicprobe calibration
252+
253+ Ran the tuned ` flow_partitions ` setup against a same-host ` quicprobe ` target
254+ with local ` iperf3 ` preflights and receiver evidence enabled.
255+
256+ Target setup:
257+
258+ - ` iperf3 --server --bind 127.0.0.1 --port 55201 `
259+ - ` quicprobe server --addr 127.0.0.1:55433 --alpn moqx-test --datagram-semantics drain --evidence-http-addr 127.0.0.1:55434 `
260+ - TLS used the repo integration CA/cert under ` .tmp/integration-certs ` .
261+
262+ Preflight:
263+
264+ - TCP loopback: about ` 61.64 Gbps ` , zero retransmits.
265+ - UDP loopback at ` 100 Mbps ` : about ` 0.014 ms ` jitter, ` 0.61% ` loss.
266+
267+ Small real-QUIC shape, ` 32 x 100 x 1180 ` , window ` 16 ` , shard/partition ` 8 ` ,
268+ demand ` 128..256 ` , queue ` 1024 ` :
269+
270+ - ` sender_shards ` : ` 36.55 ips ` , ` 27.36 ms ` average;
271+ - ` flow_partitions ` : ` 35.33 ips ` , ` 28.30 ms ` average;
272+ - receiver evidence: ` 38/38 ` valid, each sample delivered ` 32 ` completed
273+ streams and ` 3,776,000 ` bytes.
274+
275+ Heavier real-QUIC shape, ` 64 x 1000 x 1180 ` , window ` 16 ` , shard/partition ` 8 ` ,
276+ demand ` 128..256 ` , queue ` 1024 ` :
277+
278+ - ` sender_shards ` : ` 2.46 ips ` , ` 406.54 ms ` average, about ` 1.49 Gbps `
279+ workload goodput;
280+ - ` flow_partitions ` : ` 2.31 ips ` , ` 432.83 ms ` average, about ` 1.40 Gbps `
281+ workload goodput;
282+ - receiver evidence: ` 6/6 ` valid, each sample delivered ` 64 ` completed
283+ streams and ` 75,520,000 ` bytes.
284+
285+ Conclusion: the fake-target process-model win is real, but local quicprobe
286+ does not yet confirm ` flow_partitions ` as the better end-to-end QUIC sender.
287+ Keep this result as calibration only. The next performance loop should compare
288+ fake, same-host quicprobe, and remote quicprobe explicitly so we can see where
289+ the bottleneck moves from BEAM process ownership into transport/NIF/QUIC-stack
290+ interaction.
0 commit comments