WebSocket: pass the options as one struct, and check them apart from the dial

Closed
RO
1mo ago
+225-79

Behaviour change: none

Part of #44098. #44123 has the behaviour change and is stacked on this PR.

Problem

  • The native WebSocket checks its input and dials in one step, connect() (src/jsc/bindings/webcore/WebSocket.cpp:413). For finishRequest, the ws client must check first and dial later.
  • The options of new WebSocket(url, options) are 7 arguments of 2 create() overloads. An option that misses one argument list still compiles.

Fix

  • WebSocket::validate() holds the checks. connect() calls it first, always inlined.
  • WebSocketOptions holds the options. parse() reads them from script, create() takes the struct whole, and dial() alone applies them.
  • ws.js builds the options object of the native constructor in one function.
  • Verified: 26 new tests in test/js/web/websocket/websocket.test.js pin 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 calls connect(), 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 ws client 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.

Whatmainthis PR
text bytes (llvm-size)80,686,61080,685,740
stripped binary bytes80,827,97680,827,976
sizeof(WebCore::WebSocket)240240
host functions in JSWebSocket.cpp77
bundled ws.js bytes34,84034,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.

Callmainthis PR
new WebSocket(url)17,158 to 17,16517,175
new WebSocket(url, ["a", "b"])20,605 to 20,64820,637 to 20,668
new WebSocket(url, { headers })25,929 to 25,93625,918 to 25,925
new WebSocket(url, { protocols, headers, perMessageDeflate, tls })44,455 to 44,46244,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 useEnvProxy as one more argument of create() and connect(), and the same dispatchConnectFailure() helper with the same body. After this PR the flag is a field of WebSocketOptions.
  • #41648 adds tls.checkServerIdentity to the option parser. It is clean on main and conflicts with this PR in one hunk of JSWebSocket.cpp: it declares a local, checkServerIdentity, in the block that this PR turns into the fields of WebSocketOptions. Its other lines apply inside the parser body, which this PR does not change.
  • #37972 and #37973 do not touch these lines.
  • #37638 makes setupProxy return the error of an invalid proxy header. On main and in this PR, setupProxy drops 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.