WebSocket: pass the options as one struct, and check them apart from the dial
Behaviour change: none
Part of #44098. #44123 has the behaviour change and is stacked on this PR.
Problem
- The native
WebSocketchecks its input and dials in one step,connect()(src/jsc/bindings/webcore/WebSocket.cpp:413). ForfinishRequest, thewsclient must check first and dial later. - The options of
new WebSocket(url, options)are 7 arguments of 2create()overloads. An option that misses one argument list still compiles.
Fix
WebSocket::validate()holds the checks.connect()calls it first, always inlined.WebSocketOptionsholds the options.parse()reads them from script,create()takes the struct whole, anddial()alone applies them.ws.jsbuilds the options object of the native constructor in one function.- Verified: 26 new tests in
test/js/web/websocket/websocket.test.jspin the class, message and order of each error. They pass on main on purpose. Also the suites in Notes.
Background
create()makes the native socket and callsconnect(), which opens the TCP connection and sends the HTTP upgrade request.- The body of the option parser has no changed line. The declarations above it become fields of
WebSocketOptions: #41648 conflicts in that one hunk. - Considered a parser that returns the struct by value: 512 more bytes of text.
Downsides
- None found in native code. Release text is 870 bytes smaller. One
new WebSocket(url)runs 17,158 instructions before and 17,175 after, against a spread of up to 43 between runs. - A
wsclient that passes options runs about 12 more bytecode instructions per construction.
Notes
Measurements
linux-x64, release builds of main a4f1429148 and of this branch, same toolchain. Tools: llvm-size, llvm-nm, llvm-objdump, gdb. Not installed: perf, valgrind, strace, bloaty.
| What | main | this PR |
|---|---|---|
text bytes (llvm-size) | 80,686,610 | 80,685,740 |
| stripped binary bytes | 80,827,976 | 80,827,976 |
sizeof(WebCore::WebSocket) | 240 | 240 |
host functions in JSWebSocket.cpp | 7 | 7 |
bundled ws.js bytes | 34,840 | 34,994 |
Instructions of the main thread from the entry of the native constructor to its return, counted with gdb stepi on the third construction. Address randomization cannot be turned off in the test container, so the count does not repeat exactly. Each cell is the range of 2 or 3 runs.
| Call | main | this PR |
|---|---|---|
new WebSocket(url) | 17,158 to 17,165 | 17,175 |
new WebSocket(url, ["a", "b"]) | 20,605 to 20,648 | 20,637 to 20,668 |
new WebSocket(url, { headers }) | 25,929 to 25,936 | 25,918 to 25,925 |
new WebSocket(url, { protocols, headers, perMessageDeflate, tls }) | 44,455 to 44,462 | 44,454 to 44,461 |
Allocating calls into mimalloc in the same window (mi_malloc, mi_zalloc, mi_malloc_aligned, mi_realloc, counted with breakpoints): 17, 26, 30 and 59, the same on both. An earlier text of this PR gave 4, 7, 4 and 7. That count saw one of two copies of each allocator entry point.
Bytecode of a ws client (BUN_JSC_dumpGeneratedBytecodes=1): 180 of 182 code blocks are identical. #createWebSocket goes from 37 to 33 instructions, the new nativeOptions has 16, and the module body goes from 612 to 614.
The two unix socket checks
connect() had two exits with the same error for a ws+unix: URL: one for an empty path, and one for an empty socket path after the split at the first :. The second is true only when the path starts with :. validate() makes both checks in one condition, before the split.
Other open PRs on the same lines
- #41712 adds
useEnvProxyas one more argument ofcreate()andconnect(), and the samedispatchConnectFailure()helper with the same body. After this PR the flag is a field ofWebSocketOptions. - #41648 adds
tls.checkServerIdentityto the option parser. It is clean on main and conflicts with this PR in one hunk ofJSWebSocket.cpp: it declares a local,checkServerIdentity, in the block that this PR turns into the fields ofWebSocketOptions. Its other lines apply inside the parser body, which this PR does not change. - #37972 and #37973 do not touch these lines.
- #37638 makes
setupProxyreturn the error of an invalid proxy header. On main and in this PR,setupProxydrops that header and reports nothing.
Suites run with a debug build on linux-x64
test/js/web/websocket/: websocket.test.js, websocket-client.test.ts, websocket-custom-headers.test.ts, websocket-utf16-headers.test.ts, websocket-subprotocol-strict.test.ts, websocket-upgrade.test.ts, websocket-proxy.test.ts, websocket-unix.test.ts, websocket-close-connecting.test.ts, websocket.tls.ipv6.test.ts. Also test/js/first_party/ws/ and test/js/node/http/node-http-with-ws.test.ts.
Three tests of websocket.test.js connect to a public host and fail without internet access, on main too. On the test host (load average above 400) these tests end near their time limit on main and on this branch: instances should be finalized when GC'd, bounded RSS growth across throwing-option and connect-failure paths, and the two should unref tests of node-http-with-ws.test.ts. Interleaved runs of main and this branch give the same pass rate.
Not run: Windows, macOS.