Skip to content

http2: add diagnostics channel 'http2.server.stream.start' #58449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
7 changes: 7 additions & 0 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,13 @@ closing the stream can be retrieved using the `stream.rstCode` property.

Emitted when a stream is created on the server.

`http2.server.stream.start`
Copy link
Member

@Flarna Flarna May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this one comes always together with http2.server.stream.created. In one case there is a nextTick in between.

Which use case is solved by this extra event?

If the timing between created and start is important we should test the timing stays as expected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The distinction between the two is that 'created' is for when the stream has just been created but 'start' is where the application starts using the stream. Here, 'start' gets called in the nextTick because that's where the pushStream callback gets invoked, where the user code can start using the push stream. I have added a test for the timings, PTAL @Flarna .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But they always come in pairs, sometimes with a nextTick inbetween, sometimes not.
But there is no case where a create is not immediately followed by a start right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right. It could be useful to debug any performance issues that might be stemming from the blocking code that runs after pushStream() get called but before the callback is fired.


* `stream` {ServerHttp2Stream}
* `headers` {HTTP/2 Headers Object}

Emitted when a stream is started on the server.

#### Modules

> Stability: 1 - Experimental
Expand Down
18 changes: 17 additions & 1 deletion lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ const onClientStreamErrorChannel = dc.channel('http2.client.stream.error');
const onClientStreamFinishChannel = dc.channel('http2.client.stream.finish');
const onClientStreamCloseChannel = dc.channel('http2.client.stream.close');
const onServerStreamCreatedChannel = dc.channel('http2.server.stream.created');
const onServerStreamStartChannel = dc.channel('http2.server.stream.start');

let debug = require('internal/util/debuglog').debuglog('http2', (fn) => {
debug = fn;
Expand Down Expand Up @@ -374,6 +375,12 @@ function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) {
headers: obj,
});
}
if (onServerStreamStartChannel.hasSubscribers) {
onServerStreamStartChannel.publish({
stream,
headers: obj,
});
}
if (endOfStream) {
stream.push(null);
}
Expand Down Expand Up @@ -2841,7 +2848,16 @@ class ServerHttp2Stream extends Http2Stream {
if (headRequest)
stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST;

process.nextTick(callback, null, stream, headers, 0);
process.nextTick(() => {
if (onServerStreamStartChannel.hasSubscribers) {
onServerStreamStartChannel.publish({
stream,
headers,
});
}

callback(null, stream, headers, 0);
});

if (onServerStreamCreatedChannel.hasSubscribers) {
onServerStreamCreatedChannel.publish({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

// This test ensures that the built-in HTTP/2 diagnostics channels are reporting
// the diagnostics messages for the 'http2.server.stream.start' channel only
// after the 'http2.server.stream.created' channel.

const Countdown = require('../common/countdown');
const assert = require('assert');
const dc = require('diagnostics_channel');
const http2 = require('http2');

const serverHttp2StreamCreationCount = 2;

const map = {};

dc.subscribe('http2.server.stream.created', common.mustCall(({ stream, headers }) => {
map[stream.id] = { ...map[stream.id], 'createdTime': process.hrtime.bigint() };
}, serverHttp2StreamCreationCount));

dc.subscribe('http2.server.stream.start', common.mustCall(({ stream, headers }) => {
map[stream.id] = { ...map[stream.id], 'startTime': process.hrtime.bigint() };
}, serverHttp2StreamCreationCount));

const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.respond();
stream.end();

stream.pushStream({}, common.mustSucceed((pushStream) => {
pushStream.respond();
pushStream.end();
}));
}));

server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);

const countdown = new Countdown(serverHttp2StreamCreationCount, () => {
client.close();
server.close();

const timings = Object.values(map);
assert.strictEqual(timings.length, serverHttp2StreamCreationCount);

for (const { createdTime, startTime } of timings) {
assert.ok(createdTime < startTime);
}
});

const stream = client.request({});
stream.on('response', common.mustCall(() => {
countdown.dec();
}));

client.on('stream', common.mustCall((pushStream) => {
pushStream.on('push', common.mustCall(() => {
countdown.dec();
}));
}));
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

// This test ensures that the built-in HTTP/2 diagnostics channels are reporting
// the diagnostics messages for the 'http2.server.stream.start' channel when
// ServerHttp2Streams are started by both:
// - in response to an incoming 'stream' event from the client
// - the server calling ServerHttp2Stream#pushStream()

const Countdown = require('../common/countdown');
const assert = require('assert');
const dc = require('diagnostics_channel');
const http2 = require('http2');
const { Duplex } = require('stream');

const serverHttp2StreamCreationCount = 2;

dc.subscribe('http2.server.stream.start', common.mustCall(({ stream, headers }) => {
// Since ServerHttp2Stream is not exported from any module, this just checks
// if the stream is an instance of Duplex and the constructor name is
// 'ServerHttp2Stream'.
assert.ok(stream instanceof Duplex);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that exposing internals makes it hard to ever move this out of experimental at some point in time in future.

Copy link
Member Author

@RaisinTen RaisinTen May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServerHttp2Stream is actually a part of the public API. It's just that this class is not exposed for direct use by application code, like new ServerHttp2Stream(), although users still can by accessing the .constructor property of one of these instances.

assert.strictEqual(stream.constructor.name, 'ServerHttp2Stream');
assert.ok(headers && !Array.isArray(headers) && typeof headers === 'object');
}, serverHttp2StreamCreationCount));

const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.respond();
stream.end();

stream.pushStream({}, common.mustSucceed((pushStream) => {
pushStream.respond();
pushStream.end();
}));
}));

server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);

const countdown = new Countdown(serverHttp2StreamCreationCount, () => {
client.close();
server.close();
});

const stream = client.request({});
stream.on('response', common.mustCall(() => {
countdown.dec();
}));

client.on('stream', common.mustCall((pushStream) => {
pushStream.on('push', common.mustCall(() => {
countdown.dec();
}));
}));
}));
Loading