Skip to content

POC: collectJson, the body parsed by JSON::Parse, to a callback or a promise - #1348

Draft
nigrosimone wants to merge 1 commit into
uNetworking:masterfrom
nigrosimone:collect-json
Draft

nigrosimone wants to merge 1 commit into
uNetworking:masterfrom
nigrosimone:collect-json

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

POC, not to merge: a draft to see the cost and the impact on the existing code of the collectJson you suggested in #1347, and if this way is worth it.

  • collectJson(maxSize, cb) collects the body like collectBody and parses it in C++ with JSON::Parse. cb gets the value, or undefined when the body is over maxSize or not valid JSON.
  • collectJson(maxSize) returns a Promise. 413 and 400 come from uWS, on abort it never settles, and if there is no onAborted it attaches one that only marks res invalid. It resolves in a node::CallbackScope, so the code after the await is still corked: const user = await res.collectJson(1024 * 1024); res.end(JSON.stringify(user)); needs nothing else.

Impact: one new function in HttpResponseWrapper.h (about 100 lines) and the types, nothing existing changes. Tested locally on Node 22, 24 and 26, and with ASAN+UBSan: both forms, 600 KB in 40 pieces, utf-8, over maxSize, invalid, empty body, abort.

CPU per request against onData + Buffer.concat (the json-post scenario), WSL, server on one core, autocannon -c 100, 8 to 10 rounds. Two identical callback arms differ by 1 to 4%, that is the noise.

body collectBody + JSON.parse collectJson callback collectJson promise
10 B 1.00 to 1.02 1.05 to 1.06 1.00 to 1.06
1 KB 0.99 to 1.01 0.85 to 0.87 0.93
18 KB 0.99 0.94 to 0.98 0.97
312 KB 1.00 0.98 to 0.99 1.01

So it is faster from about 1 KB, the same on big bodies where parsing is all the time, and about 5% slower on a 10 B body, which I don't understand yet. The promise costs a few % over the callback.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Benchmark: 0df2c2b against 27c8955

Scenario Base req/s Head req/s Head / base Rounds Noise Busy CPU per request
http/hello-world 202087 202541 0.998x (-0.2%) 0.97 to 1.03 ±4.1% 100% / 100% 4.95 / 4.93 us (1.002x)
http/headers 173237 172926 1.004x (+0.4%) 0.95 to 1.02 ±4.2% 100% / 100% 5.77 / 5.78 us (0.996x)
http/json-post 155200 156551 1.006x (+0.6%) 0.99 to 1.03 ±4.6% 100% / 100% 6.44 / 6.38 us (0.993x)
http/cached 232841 229850 0.996x (-0.4%) 0.96 to 1.01 ±2.3% 100% / 100% 4.29 / 4.35 us (1.004x)
ws/echo-20b 210130 207745 0.991x (-0.9%) 0.96 to 1.00 ±4.3% 100% / 100% 4.76 / 4.81 us (1.009x)
ws/echo-4kb 183087 180531 0.987x (-1.3%) 0.97 to 1.00 ±3.5% 100% / 100% 5.46 / 5.54 us (1.013x)

4 rounds of 3s per scenario, each round loading base, head and a second process of each one, from a copy of its build, after the other, in an order that changes every round. "Head / base" is the median of the per-round ratios of req/s and "Rounds" their range. "Noise" is how far base/base and head/head, the same code on both sides, got from 1 in this same run: that is what the machine did, so a row is marked only when the median moved further than that, at least 2%, and every round moved the same way: 👀 slower, 🏆 faster. "Busy" is the cpu time of the server process over the round and "CPU per request" that time divided by the requests it answered, with the head/base ratio judged against its own same-code band and bold when it cleared it. When busy is well under 100% the load generator set the pace, the req/s are its and the cpu per request is the column to read. Only the ratios are comparable across runs, the absolute figures depend on the runner.

Node v26.10.0, Intel(R) Xeon(R) 6973P-C, 4 cores (server on cpu 0, load on 2,3), http_load_test and load_test with 100 connections.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant