perf: collect request bodies natively with uWS collectBody - #357
perf: collect request bodies natively with uWS collectBody#357nigrosimone wants to merge 2 commits into
Conversation
uWS can accumulate the whole body in C++ and hand it over in a single call, so the body parsers no longer need one JS callback per chunk plus a final Buffer.concat. The size limit is enforced by uWS itself, which yields null once the body exceeds it. Parsers that don't keep the buffer past their own call (json, text, urlencoded) now read straight from the memory uWS owns, removing the last copy on that path; raw and the verify option still get a copy since both outlive the callback. The old streaming path stays for inflated bodies, where the limit applies to the decompressed size and so cannot be delegated to uWS.
This comment was marked as outdated.
This comment was marked as outdated.
The only scenario that exercises the body parsers, body-urlencoded, posts 57 bytes. uWS hands a body that small over in a single chunk, so the suite never measures the work of collecting a body that arrives in pieces, and changes to that path cannot show up in the results. Post 512kb of JSON instead, which is above the ~512kb chunk size uWS delivers bodies in, so the parser has to put several chunks together.
|
The 512kb scenario has now run on both sides. Adding #358 on its own gave a baseline of the same scenario on
The large-body row moves from 0.99x to 1.10x, which looks like the change doing what it is supposed to. I do not think it can be claimed yet, though: the 57 byte control moved by the same 10% in the same pair of runs, and that scenario is far too small for this change to matter to it. Two rows moving together like that points at a difference between the runs rather than at the code, which also matches the absolute numbers (Express itself went from 21.26k to 15.50k req/sec on the control row between the two runs). So the honest read is still "no demonstrated win", now with a scenario that could show one if it were there. What would settle it is a few repeated runs of both branches; I cannot trigger reruns on this repo, and pushing empty commits to force them seemed worse than saying so. The offer from my earlier comment stands: happy to close this if you would rather not carry the extra branch in the parsers for something the suite cannot demonstrate. #358 and #359 are useful on their own either way. |
TL;DR: no demonstrated win
uWS can accumulate a whole request body in C++ and hand it over in a single call, which lets the body parsers drop the per-chunk bookkeeping they do today.
What changes
On the fast path (
!req.receivedData) the parsers usedres.onDataand rebuilt the body in JS: one callback per chunk,Buffer.from(buf)per chunk into an array, thenBuffer.concatat the end. That is two copies of the body plus one JS/C++ crossing per chunk.With
res.collectBody(limit, cb)uWS accumulates the chunks itself (reserving up front when it knows the remaining length) and calls back once. The size limit is enforced natively, and uWS yieldsnullwhen the body goes over it.Parsers that do not keep the buffer past their own call
json,text,urlencoded, all of which only read it throughtoString()- now read straight from the memory uWS owns, so the last copy goes away too.rawassigns the buffer toreq.body, and averifycallback may hold on to it as well, so both still get a copy.What stays on the old path
Inflated bodies.
options.limitis checked against the decompressed size, whilecollectBodycan only cap the compressed bytes it receives, so delegating the limit would weaken the protection against decompression bombs. Same for a non-finitelimit, which cannot be handed to the native side.Numbers
I could not get a trustworthy measurement on my machine: interleaved runs of the same comparison swung between 0.99x and 1.36x on a 60kb JSON body, and server-side CPU per request moved by ±30% between rounds, so I would rather not quote a figure. What the change removes is concrete though, one callback per chunk, one copy per chunk, one final
concat, and for the three text parsers the remaining copy, so the benchmark job here on Linux should be a better judge than anything I can produce on Windows.Worth noting for expectations: uWS already delivers bodies with a
content-lengthup to about 512kb in a single chunk, so for small payloads this mainly removes copies rather than callbacks. Bodies above that arrive in ~512kb chunks, which is where the old accumulate-and-concat path did the most work.