feat(scaling): engine.io WS transport-level packing — lever 8 prototype#7772
feat(scaling): engine.io WS transport-level packing — lever 8 prototype#7772JohnMcLear wants to merge 1 commit into
Conversation
Issue: engine.io's WebSocket transport sends one WS frame per engine.io packet, even when the engine.io socket has multiple packets buffered (see #7767). The polling transport already coalesces — `transport.send(packets)` goes through `encodePayload(packets)` and writes one HTTP response containing the whole payload. At high emit rate the WS path is dominated by per-frame syscalls on the server and per-message callback overhead on the client; that's why dropping the polling fallback (lever 4) made things sharply worse. This patch monkey-patches engine.io's WebSocket transport prototype so `send(packets)` with N > 1 packets goes through `encodePayload` and emits ONE WS frame containing the multi-packet payload — the same wire bytes the polling transport already uses. Single-packet sends keep the legacy fast path including the pre-encoded-frame optimisation, so steady-state quiet-pad behaviour is identical to upstream. Gated by settings.enginePacking (default false). Receiving clients must recognise payload-encoded frames (split on `\x1e` and decodePayload). The etherpad-cli-client patch in ether/etherpad-load-test#TBD is forward-compatible; production deployments enabling the flag MUST also ship a forward-compatible browser bundle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Review Summary by QodoImplement engine.io WebSocket transport-level packet packing (lever 8)
WalkthroughsDescription• Implement engine.io WebSocket transport-level packet packing to reduce frame overhead • Monkey-patch WebSocket transport to coalesce multi-packet sends into single frames • Add enginePacking setting (default false) with forward-compatibility warnings • Maintain single-packet fast path for steady-state quiet-pad behavior Diagramflowchart LR
A["Multiple buffered<br/>engine.io packets"] -->|"enginePacking: true"| B["encodePayload<br/>coalescing"]
B -->|"Single WS frame"| C["Reduced per-frame<br/>syscall overhead"]
A -->|"Single packet or<br/>enginePacking: false"| D["Legacy fast path<br/>per-packet frames"]
D --> E["Unchanged behavior"]
File Changes1. src/node/utils/EnginePacking.ts
|
Code Review by Qodo
Context used 1. enginePacking undocumented in template
|
| /** | ||
| * Pack multiple engine.io packets into a single WebSocket frame | ||
| * (ether/etherpad#7756 lever 8). engine.io's WS transport otherwise | ||
| * sends one frame per packet, while the polling transport already | ||
| * batches via encodePayload. Enabling this matches the polling | ||
| * coalescing behaviour on the WS path; at high fan-out rates it cuts | ||
| * the WS frame count proportionally to packets-per-flush. | ||
| * | ||
| * WARNING: enabling this requires connected clients to recognise | ||
| * payload-encoded WS frames (split on the `\x1e` record separator | ||
| * and decodePayload). Clients that pass each frame straight to | ||
| * decodePacket will fail to parse a multi-packet frame and silently | ||
| * miss those messages. The etherpad browser bundle and | ||
| * etherpad-cli-client are forward-compatible (they detect the | ||
| * separator); third-party clients may not be. Coordinate rollouts. | ||
| */ | ||
| enginePacking: false, |
There was a problem hiding this comment.
1. enginepacking undocumented in template 📘 Rule violation ⚙ Maintainability
A new public config setting enginePacking is added and wired into server boot, but the user-facing configuration documentation/template is not updated to include it. This risks operators being unaware of the flag and its compatibility warning.
Agent Prompt
## Issue description
A new configuration option (`enginePacking`) was introduced and is used at runtime, but it is not documented in the repository’s configuration documentation, so users will not discover it or its rollout warnings.
## Issue Context
`enginePacking` is added to `SettingsType` and defaulted to `false` with detailed inline comments, and it gates a runtime behavior change in socket.io/engine.io startup.
## Fix Focus Areas
- settings.json.template[709-740]
- src/node/utils/Settings.ts[662-678]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Closing. Scored against the dive on run 25954316731: at step 350 (just below the cliff), engine-packing has p95=109ms and apply_mean=23.86ms vs baseline p95=76ms / apply_mean=16.15ms — neutral-to-slightly-worse, within noise. The patch is not the win we hoped for. Why — engine.io's For a real transport-level packing win we'd need to deliberately delay flush — for example, by deferring it to the next setImmediate or microtask boundary so multiple Secondary finding — this same run showed websocket-only as the best lever, contradicting every previous dive. The matrix-jobs-on-different-physical-runners issue (each GHA matrix entry runs on a separate machine) means our cross-lever comparisons are cross-hardware. Runner noise can flip lever conclusions. Real dive methodology going forward needs N≥3 runs per lever to filter out runner noise before drawing strong conclusions. Worth flagging on the dive doc when next updated. The harness-side forward-compat patch in ether/etherpad-load-test#106 (already merged) stays — it's cheap forward-compat in case a future server-side change uses payload-encoded frames intentionally. |
Summary
Prototype for the engine.io transport-level packing direction from #7767 — the meatiest untouched lever in the scaling-dive doc.
What changes. engine.io's WebSocket transport sends one WS frame per engine.io packet, even when the engine.io socket has multiple packets buffered. The polling transport doesn't — its `transport.send(packets)` runs through `encodePayload` and writes one HTTP response containing the whole payload. This PR monkey-patches engine.io's WS transport so multi-packet flushes go out as ONE frame containing the payload-encoded bundle. Single-packet sends keep the legacy fast path (including the pre-encoded-frame optimisation), so steady-state quiet-pad behaviour is identical to upstream.
Why. At high emit rate the WS path is dominated by per-frame syscalls on the server and per-message callback overhead on the client. The dive measured this directly via lever 4 (`socketTransportProtocols: ["websocket"]`): dropping the polling fallback at 200 authors quadrupled client p95 and nearly tripled `apply_mean`. The polling fallback was acting as a natural coalescer; this PR brings the same coalescing property to the WS path.
Implementation
Rollout warning
Enabling this on a server with clients that haven't been updated will silently break those clients: a multi-packet frame fed to engine.io-client's stock `decodePacket` returns garbage. The harness-side patch in ether/etherpad-load-test#TBD is forward-compatible (detects `\x1e` and routes through `decodePayload`). For production rollout the etherpad browser bundle would need the same forward-compat patch — out of scope for this prototype.
Scoring this
Once merged (or via `core_ref=feat/engine-io-ws-packing`):
```
gh workflow run "Scaling dive" --repo ether/etherpad-load-test --ref main \
-f core_ref=feat/engine-io-ws-packing \
-f sweep='authors=100..500:step=50:dwell=8s:warmup=2s'
```
The dive workflow includes a new `engine-packing` matrix entry that sets `enginePacking: true` in `settings.json`. Expectations:
Files touched
No tests yet — the patch is best validated end-to-end via the dive workflow. A unit test that constructs an in-memory WebSocket transport and asserts the framing is feasible but high effort for a prototype; happy to add it before merge if reviewers want.
🤖 Generated with Claude Code