Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,43 @@ on:
type: number
default: 30
description: How many times to repeat each benchmark
post-comment:
type: boolean
description: Post a comment linking to the run, with the aggregated result once known.
token:
type: string
description: A GitHub token to post comments cross repository (not recommended)

permissions:
contents: read

jobs:
post-comment:
if: inputs.post-comment
outputs:
body: ${{ steps.comment.outputs.COMMENT_BODY }}
url: ${{ steps.comment.outputs.COMMENT_URL }}
runs-on: ubuntu-slim
permissions:
pull-requests: write
steps:
- name: Mark token input as sensitive
if: inputs.token != ''
run: echo "::add-mask::${{ inputs.token }}"
- name: Add link to the current run
id: comment
run: |
FILTER_IF_SET=
[ -z "$FILTER" ] || FILTER_IF_SET=" / $FILTER"
COMMENT_BODY="Benchmark GHA (${CATEGORIES}${FILTER_IF_SET}): ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "COMMENT_BODY=$COMMENT_BODY" >> "$GITHUB_OUTPUT"
echo "COMMENT_URL=$(gh pr comment -R "${REPO:-$GITHUB_REPOSITORY}" "$PR_ID" --body "$COMMENT_BODY")" >> "$GITHUB_OUTPUT"
env:
CATEGORIES: ${{ inputs.category }}
FILTER: ${{ inputs.filter }}
GH_TOKEN: ${{ inputs.token || github.token }}
REPO: ${{ inputs.repo }}
PR_ID: ${{ inputs.pr_id }}
build:
strategy:
fail-fast: true
Expand Down Expand Up @@ -155,9 +187,13 @@ jobs:
path: ${{ matrix.system }}.csv

aggregate-results:
needs: build
needs: [build, post-comment]
if: ${{ always() && needs.build.result == 'success' && needs.post-comment.result == (inputs.post-comment && 'success' || 'skipped') }}
name: Aggregate benchmark results
runs-on: ubuntu-slim
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand Down Expand Up @@ -200,4 +236,30 @@ jobs:
echo "> [!WARNING] "
echo "> Do not take GHA benchmark results as face value, always confirm them"
echo "> using a dedicated machine, e.g. Jenkins CI."
' | tee /dev/stderr >> "$GITHUB_STEP_SUMMARY"
' | tee /dev/stderr ${{ inputs.post-comment && 'body.txt' || '' }} >> "$GITHUB_STEP_SUMMARY"
- name: Mark token input as sensitive
if: inputs.token != ''
run: echo "::add-mask::${{ inputs.token }}"
- name: Edit comment
if: inputs.post-comment
run: |
{
echo "$COMMENT_BODY"
echo
echo '<details><summary>Results</summary>'
echo
cat body.txt
echo
echo '</details>'
} | jq -Rrcs '{ body: . }' | gh api \
--method PATCH \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"/repos/${REPO:-$GITHUB_REPOSITORY}/issues/comments/${COMMENT_URL##*-}" \
--input -
env:
GH_TOKEN: ${{ inputs.token || github.token }}
REPO: ${{ inputs.repo }}
COMMENT_BODY: ${{ needs.post-comment.outputs.body }}
COMMENT_URL: ${{ needs.post-comment.outputs.url }}
PR_ID: ${{ inputs.pr_id }}
16 changes: 16 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -3582,6 +3582,22 @@ added: v18.1.0
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.

<a id="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>

### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`

An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
via a `worker_threads` `postMessage()` call while it was not in a transferable
state, for example because it had already started reading or had buffered data.

<a id="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>

### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`

An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
on a platform where moving the underlying handle between event loops is not
supported (currently Windows).

<a id="ERR_WORKER_INIT_FAILED"></a>

### `ERR_WORKER_INIT_FAILED`
Expand Down
42 changes: 42 additions & 0 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ added: v0.1.90

This class is used to create a TCP or [IPC][] server.

A listening TCP `net.Server` can be transferred to a worker thread by listing it
in the `transferList` of a [`worker_threads`][] `postMessage()` call. This moves
the underlying listening socket to the receiving thread, where it resumes
accepting connections. See [Transferring TCP handles to other threads][].

### `new net.Server([options][, connectionListener])`

* `options` {Object} See
Expand Down Expand Up @@ -751,6 +756,41 @@ is received. For example, it is passed to the listeners of a
[`'connection'`][] event emitted on a [`net.Server`][], so the user can use
it to interact with the client.

### Transferring TCP handles to other threads

A connected TCP `net.Socket` can be moved to another thread by listing it in the
`transferList` of a [`worker_threads`][] `postMessage()` call. After the
transfer, the source socket is destroyed on the sending thread (further use
fails with `ERR_STREAM_DESTROYED` rather than silently dropping data), and the
socket continues to work on the receiving thread. This makes it possible to
accept connections on one thread and distribute them across a pool of worker
threads, for example to build a `node:cluster`-like model on top of worker
threads.

The socket must be a freshly accepted or created TCP connection: it must still
be attached to a live handle, must not be connecting or destroyed, and must not
have started reading or have buffered data. Otherwise `postMessage()` throws
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported, and only
on Unix-like platforms; on Windows `postMessage()` throws
`ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`.

```cjs
const net = require('node:net');
const { Worker } = require('node:worker_threads');

// worker.js receives `{ socket }` messages and handles each connection.
const worker = new Worker('./worker.js');

const server = net.createServer((socket) => {
// Hand the freshly accepted connection off to the worker thread.
worker.postMessage({ socket }, [socket]);
});
server.listen(8000);
```

A listening [`net.Server`][] can be transferred the same way, which moves the
listening socket itself (and its pending accept queue) to the receiving thread.

### `new net.Socket([options])`

<!-- YAML
Expand Down Expand Up @@ -2194,6 +2234,7 @@ net.isIPv6('fhqwhgads'); // returns false
[Identifying paths for IPC connections]: #identifying-paths-for-ipc-connections
[RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt
[Readable Stream]: stream.md#class-streamreadable
[Transferring TCP handles to other threads]: #transferring-tcp-handles-to-other-threads
[`'close'`]: #event-close
[`'connect'`]: #event-connect
[`'connection'`]: #event-connection
Expand Down Expand Up @@ -2250,6 +2291,7 @@ net.isIPv6('fhqwhgads'); // returns false
[`socket.setTimeout()`]: #socketsettimeouttimeout-callback
[`socket.setTimeout(timeout)`]: #socketsettimeouttimeout-callback
[`stream.getDefaultHighWaterMark()`]: stream.md#streamgetdefaulthighwatermarkobjectmode
[`worker_threads`]: worker_threads.md
[`writable.destroy()`]: stream.md#writabledestroyerror
[`writable.destroyed`]: stream.md#writabledestroyed
[`writable.end()`]: stream.md#writableendchunk-encoding-callback
Expand Down
81 changes: 81 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -3478,6 +3478,10 @@ ordered events, emitted immediately as the tests execute.
| [`'test:fail'`][] | [`'test:complete'`][] (`details.passed` is `false`) |
| [`'test:plan'`][] | |
| [`'test:diagnostic'`][] | |
| | [`'test:log'`][] |

[`'test:log'`][] is deliberately execution ordered only: it is the live
counterpart of [`'test:diagnostic'`][]'s buffered reporting.

File scoped and global events are always emitted immediately, in execution
order.
Expand Down Expand Up @@ -3747,6 +3751,38 @@ When using process isolation (the default), the test name will be the file path
since the parent runner only knows about file-level tests. When using
`--test-isolation=none`, the actual test name is shown.

### Event: `'test:log'`

<!-- YAML
added: REPLACEME
-->

* `data` {Object}
* `column` {number|undefined} The column number where the test is defined, or
`undefined` if the test was run through the REPL.
* `data` {any} The structured payload passed to [`context.log`][], or
`undefined` if none was provided. The test runner does not interpret this
value.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
`undefined` if the test was run through the REPL.
* `message` {string} The log message.
* `name` {string} The test name.
* `nesting` {number} The nesting level of the test.
* `parentId` {number|undefined} The `testId` of the enclosing test, or
`undefined` for top-level tests.
* `testId` {number} A numeric identifier for the test instance that emitted
the log message.

Emitted when [`context.log`][] is called. Unlike [`'test:diagnostic'`][],
this event is emitted immediately, in the order that the tests execute,
making it suitable for reporters that render test output unbuffered.

### Event: `'test:pass'`

* `data` {Object}
Expand Down Expand Up @@ -4272,6 +4308,29 @@ test('top level test', (t) => {
});
```

### `context.log(message[, data])`

<!-- YAML
added: REPLACEME
-->

* `message` {string} Message to be reported.
* `data` {any} Optional structured payload attached to the message. The test
runner passes it through untouched. When tests run with process isolation,
this value must be compatible with the [HTML structured clone algorithm][].

This function is used to write a log message to the output. Unlike
[`context.diagnostic`][], the resulting [`'test:log'`][] event is emitted
immediately, in the order that the tests execute, rather than being buffered
until the test reports its results. This function does not return a value.

```js
test('top level test', (t) => {
t.log('fetched user', { userId: 42 });
t.log('retrying flaky endpoint', { attempt: 3 });
});
```

### `context.filePath`

<!-- YAML
Expand Down Expand Up @@ -4761,6 +4820,26 @@ test.describe('my suite', (suite) => {
});
```

### `context.log(message[, data])`

<!-- YAML
added: REPLACEME
-->

* `message` {string} Message to be reported.
* `data` {any} Optional structured payload attached to the message. The test
runner passes it through untouched.

Write a log message to the output. The resulting [`'test:log'`][] event is
emitted immediately, in the order that the tests execute.

```js
test.describe('my suite', (suite) => {
suite.log('Suite log message');
});
```

[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[TAP]: https://testanything.org/
[Test tags]: #test-tags
[`'test:complete'`]: #event-testcomplete
Expand All @@ -4770,6 +4849,7 @@ test.describe('my suite', (suite) => {
[`'test:enqueue'`]: #event-testenqueue
[`'test:fail'`]: #event-testfail
[`'test:interrupted'`]: #event-testinterrupted
[`'test:log'`]: #event-testlog
[`'test:pass'`]: #event-testpass
[`'test:plan'`]: #event-testplan
[`'test:start'`]: #event-teststart
Expand Down Expand Up @@ -4805,6 +4885,7 @@ test.describe('my suite', (suite) => {
[`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
[`assert.throws`]: assert.md#assertthrowsfn-error-message
[`context.diagnostic`]: #contextdiagnosticmessage
[`context.log`]: #contextlogmessage-data
[`context.skip`]: #contextskipmessage
[`context.tags`]: #contexttags
[`context.todo`]: #contexttodomessage
Expand Down
21 changes: 15 additions & 6 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,8 @@ In particular, the significant differences to `JSON` are:
* {KeyObject}s,
* {MessagePort}s,
* {net.BlockList}s,
* {net.Server}s (TCP only, when listed in `transferList`),
* {net.Socket}s (TCP only, when listed in `transferList`),
* {net.SocketAddress}es,
* {X509Certificate}s.

Expand Down Expand Up @@ -1235,12 +1237,20 @@ circularData.foo = circularData;
port2.postMessage(circularData);
```

`transferList` may be a list of {ArrayBuffer}, [`MessagePort`][], and
[`FileHandle`][] objects.
`transferList` may be a list of {ArrayBuffer}, [`MessagePort`][],
[`FileHandle`][], {net.Server}, and {net.Socket} objects.
After transferring, they are not usable on the sending side of the channel
anymore (even if they are not contained in `value`). Unlike with
[child processes][], transferring handles such as network sockets is currently
not supported.
anymore (even if they are not contained in `value`).

Transferring a {net.Server} moves its listening socket — together with any
pending connections in the accept queue — to the receiving thread's event loop.
Transferring a {net.Socket} moves a single connection; the socket must be a
freshly accepted or created TCP connection that has not yet started reading and
has no buffered data, otherwise `postMessage()` throws
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. This makes it possible to accept
connections on one thread and distribute them across a pool of worker threads.
Only TCP handles are supported, and only on Unix-like platforms; on Windows
`postMessage()` throws `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`.

If `value` contains {SharedArrayBuffer} instances, those are accessible
from either thread. They cannot be listed in `transferList`.
Expand Down Expand Up @@ -2276,7 +2286,6 @@ thread spawned will spawn another until the application crashes.
[async-resource-worker-pool]: async_context.md#using-asyncresource-for-a-worker-thread-pool
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
[child processes]: child_process.md
[contextified]: vm.md#what-does-it-mean-to-contextify-an-object
[locks.request()]: #locksrequestname-options-callback
[v8.serdes]: v8.md#serialization-api
Loading
Loading