Skip to content

Commit 8617871

Browse files
divybotlittledivy
andauthored
fix(ext/node): emit perf_hooks PerformanceEntry for http2 sessions and streams (#33618)
Enables `test-http2-perf_hooks` in node_compat suite. Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent 6566ce8 commit 8617871

3 files changed

Lines changed: 147 additions & 31 deletions

File tree

ext/node/ops/http2/session.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,7 @@ unsafe extern "C" fn on_frame_send_callback(
17361736
) -> i32 {
17371737
// SAFETY: data is the user_data pointer set during session creation
17381738
let session = unsafe { Session::from_user_data(data) };
1739+
session.frames_sent = session.frames_sent.saturating_add(1);
17391740
// SAFETY: frame is valid per nghttp2 callback contract
17401741
let f = unsafe { &*frame };
17411742
// SAFETY: union access of `hd` is always valid (every nghttp2 frame has a
@@ -1973,6 +1974,12 @@ pub struct Session {
19731974
pub max_invalid_frames: u32,
19741975
/// Running count of invalid frames received on this session.
19751976
pub invalid_frame_count: u32,
1977+
/// Total HTTP/2 frames received on this session, used for the
1978+
/// `framesReceived` field of perf_hooks `Http2Session` entries.
1979+
pub frames_received: u32,
1980+
/// Total HTTP/2 frames sent on this session, used for the
1981+
/// `framesSent` field of perf_hooks `Http2Session` entries.
1982+
pub frames_sent: u32,
19761983
/// Custom error code set by an nghttp2 callback when it returns a fatal
19771984
/// error so that `receive_data` can surface it to JS via
19781985
/// `session_internal_error_cb`. Mirrors Node's
@@ -2012,10 +2019,6 @@ pub struct Session {
20122019
/// (see `HandlePingFrame` in node_http2.cc): there is no legitimate
20132020
/// reason for a peer to send an unsolicited PING ACK.
20142021
pub pending_pings: u32,
2015-
/// Total number of HTTP/2 frames received on this session. Mirrors
2016-
/// Node's `Http2Session::statistics_.frame_count`, surfaced via the
2017-
/// PerformanceObserver `Http2Session` entry's `framesReceived` field.
2018-
pub frames_received: u32,
20192022
}
20202023

20212024
impl Session {
@@ -2553,13 +2556,14 @@ impl Http2Session {
25532556
outgoing_chunks: VecDeque::new(),
25542557
max_invalid_frames: 1000,
25552558
invalid_frame_count: 0,
2559+
frames_received: 0,
2560+
frames_sent: 0,
25562561
custom_recv_error_code: None,
25572562
sent_goaway_code: None,
25582563
local_custom_settings: Vec::new(),
25592564
remote_custom_settings: Vec::new(),
25602565
pending_settings_acks: VecDeque::new(),
25612566
pending_pings: 0,
2562-
frames_received: 0,
25632567
}));
25642568

25652569
// SAFETY: inner is valid (just allocated); callbacks and options are valid
@@ -2922,6 +2926,22 @@ impl Http2Session {
29222926
session.active_stream_count() as u32
29232927
}
29242928

2929+
#[fast]
2930+
#[smi]
2931+
fn frames_received(&self) -> u32 {
2932+
// SAFETY: self.inner was allocated by Box::into_raw and is valid
2933+
let session = unsafe { &*self.inner };
2934+
session.frames_received
2935+
}
2936+
2937+
#[fast]
2938+
#[smi]
2939+
fn frames_sent(&self) -> u32 {
2940+
// SAFETY: self.inner was allocated by Box::into_raw and is valid
2941+
let session = unsafe { &*self.inner };
2942+
session.frames_sent
2943+
}
2944+
29252945
#[fast]
29262946
fn has_pending_data(&self) -> bool {
29272947
// SAFETY: self.session is a valid nghttp2 session pointer
@@ -3111,13 +3131,6 @@ impl Http2Session {
31113131
session.outgoing_buffers.len() as u32
31123132
}
31133133

3114-
#[fast]
3115-
fn frames_received(&self) -> u32 {
3116-
// SAFETY: self.inner was allocated by Box::into_raw and is valid
3117-
let session = unsafe { &*self.inner };
3118-
session.frames_received
3119-
}
3120-
31213134
#[fast]
31223135
fn origin(&self, #[string] origins: &str, count: i32) -> i32 {
31233136
// Origins are concatenated and separated by NUL bytes (Node-compatible

ext/node/polyfills/http2.ts

Lines changed: 121 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ import {
5151
op_http2_callbacks,
5252
} from "ext:core/ops";
5353
import { enqueueNodePerformanceEntry } from "node:perf_hooks";
54+
const { performance: webPerformance } = core.loadExtScript(
55+
"ext:deno_web/15_performance.js",
56+
);
5457
import net from "node:net";
5558
import assert from "node:assert";
5659
import http from "node:http";
@@ -259,6 +262,89 @@ function getURLOrigin(urlStr) {
259262
}
260263
}
261264

265+
function perfNow() {
266+
return webPerformance.now();
267+
}
268+
269+
function emitSessionPerfEntry(session) {
270+
if (session[kPerfEmitted]) return;
271+
session[kPerfEmitted] = true;
272+
const stats = session[kPerfStats];
273+
if (!stats) return;
274+
275+
const startTime = stats.startTime;
276+
const duration = perfNow() - startTime;
277+
const handle = session[kHandle];
278+
const framesReceived = handle && typeof handle.framesReceived === "function"
279+
? handle.framesReceived()
280+
: 0;
281+
const framesSent = handle && typeof handle.framesSent === "function"
282+
? handle.framesSent()
283+
: 0;
284+
const streamCount = stats.streamCount;
285+
const streamAverageDuration = streamCount > 0
286+
? stats.streamTotalDuration / streamCount
287+
: 0;
288+
const type = session[kType] === NGHTTP2_SESSION_SERVER ? "server" : "client";
289+
const detail = {
290+
bytesRead: stats.bytesRead,
291+
bytesWritten: stats.bytesWritten,
292+
framesReceived,
293+
framesSent,
294+
maxConcurrentStreams: stats.maxConcurrentStreams,
295+
pingRTT: stats.pingRTT,
296+
streamAverageDuration,
297+
streamCount,
298+
type,
299+
};
300+
301+
enqueueNodePerformanceEntry({
302+
name: "Http2Session",
303+
entryType: "http2",
304+
startTime,
305+
duration,
306+
detail,
307+
});
308+
}
309+
310+
function emitStreamPerfEntry(stream) {
311+
if (stream[kPerfEmitted]) return;
312+
stream[kPerfEmitted] = true;
313+
const stats = stream[kPerfStats];
314+
if (!stats) return;
315+
316+
const startTime = stats.startTime;
317+
const duration = perfNow() - startTime;
318+
const detail = {
319+
bytesRead: stats.bytesRead,
320+
bytesWritten: stats.bytesWritten,
321+
timeToFirstByte: stats.firstByte > 0 ? stats.firstByte - startTime : 0,
322+
timeToFirstByteSent: stats.firstByteSent > 0
323+
? stats.firstByteSent - startTime
324+
: 0,
325+
timeToFirstHeader: stats.firstHeader > 0
326+
? stats.firstHeader - startTime
327+
: 0,
328+
};
329+
330+
enqueueNodePerformanceEntry({
331+
name: "Http2Stream",
332+
entryType: "http2",
333+
startTime,
334+
duration,
335+
detail,
336+
});
337+
338+
// Roll the stream's lifetime into the parent session's averageDuration.
339+
const session = stream[kSession];
340+
if (session) {
341+
const sstats = session[kPerfStats];
342+
if (sstats) {
343+
sstats.streamTotalDuration += duration;
344+
}
345+
}
346+
}
347+
262348
// Schedule a deferred sendPending() call on the session's native handle.
263349
// This is deferred via queueMicrotask to avoid re-entrancy: nghttp2's
264350
// send_pending_data can invoke callbacks that call back into JS ops.
@@ -399,6 +485,13 @@ const kType = Symbol("type");
399485
const kWriteGeneric = Symbol("write-generic");
400486
const kSessions = Symbol("sessions");
401487

488+
// Symbols for tracking perf_hooks `http2` performance entry stats. The
489+
// `pingRTT`, `streamCount`, etc. fields surfaced via `entry.detail` are
490+
// populated by Http2Session/Http2Stream methods when those events occur,
491+
// then read back when the entry is enqueued at session/stream destroy.
492+
const kPerfStats = Symbol("perf-stats");
493+
const kPerfEmitted = Symbol("perf-emitted");
494+
402495
const kMaxOutstandingSettings = Symbol("maxOutstandingSettings");
403496
const kMaxOutstandingPings = Symbol("maxOutstandingPings");
404497

@@ -1748,6 +1841,16 @@ class Http2Stream extends Duplex {
17481841
this[kRequest] = null;
17491842
this[kProxySocket] = null;
17501843

1844+
this[kPerfEmitted] = false;
1845+
this[kPerfStats] = {
1846+
startTime: perfNow(),
1847+
firstByte: 0,
1848+
firstByteSent: 0,
1849+
firstHeader: 0,
1850+
bytesRead: 0,
1851+
bytesWritten: 0,
1852+
};
1853+
17511854
this.on("pause", streamOnPause);
17521855

17531856
this.on("newListener", streamListenerAdded);
@@ -1774,6 +1877,11 @@ class Http2Stream extends Duplex {
17741877
session[kState].pendingStreams.delete(this);
17751878
session[kState].streams.set(id, this);
17761879

1880+
const sstats = session[kPerfStats];
1881+
if (sstats) {
1882+
sstats.streamCount++;
1883+
}
1884+
17771885
this[kID] = id;
17781886
//this[async_id_symbol] = handle.getAsyncId();
17791887
handle[kOwner] = this;
@@ -2315,6 +2423,8 @@ class Http2Stream extends Duplex {
23152423
err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code);
23162424
}
23172425

2426+
emitStreamPerfEntry(this);
2427+
23182428
this[kSession] = undefined;
23192429
this[kHandle] = undefined;
23202430

@@ -3424,25 +3534,7 @@ function cleanupSession(session) {
34243534
function finishSessionClose(session, error) {
34253535
debugSessionObj(session, "finishSessionClose");
34263536

3427-
// Emit a `Http2Session` PerformanceObserver entry with frame statistics
3428-
// from the native session before cleanupSession() drops the handle. Mirrors
3429-
// Node's `Http2Session::Close` reporting via PerformanceEntry. We surface
3430-
// only the fields the perf-hooks docs document and the in-tree tests
3431-
// observe.
3432-
const perfHandle = session[kHandle];
3433-
if (perfHandle && typeof perfHandle.framesReceived === "function") {
3434-
const framesReceived = perfHandle.framesReceived();
3435-
enqueueNodePerformanceEntry({
3436-
name: "Http2Session",
3437-
entryType: "http2",
3438-
startTime: 0,
3439-
duration: 0,
3440-
detail: {
3441-
type: session[kType] === NGHTTP2_SESSION_CLIENT ? "client" : "server",
3442-
framesReceived,
3443-
},
3444-
});
3445-
}
3537+
emitSessionPerfEntry(session);
34463538

34473539
const socket = session[kSocket];
34483540
cleanupSession(session);
@@ -3691,6 +3783,16 @@ class Http2Session extends EventEmitter {
36913783
2 ** 31 - 1,
36923784
) || 10;
36933785
this[kStrictSingleValueFields] = options.strictSingleValueFields !== false;
3786+
this[kPerfEmitted] = false;
3787+
this[kPerfStats] = {
3788+
startTime: perfNow(),
3789+
streamCount: 0,
3790+
streamTotalDuration: 0,
3791+
pingRTT: 0,
3792+
bytesRead: 0,
3793+
bytesWritten: 0,
3794+
maxConcurrentStreams: 0xffffffff,
3795+
};
36943796

36953797
// Do not use nagle's algorithm
36963798
if (typeof socket.setNoDelay === "function") {

tests/node_compat/config.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,7 @@
17881788
"parallel/test-http2-origin.js": {},
17891789
"parallel/test-http2-pack-end-stream-flag.js": {},
17901790
"parallel/test-http2-padding-aligned.js": {},
1791+
"parallel/test-http2-perf_hooks.js": {},
17911792
"parallel/test-http2-perform-server-handshake.js": {},
17921793
"parallel/test-http2-ping.js": {},
17931794
"parallel/test-http2-ping-settings-heapdump.js": {},

0 commit comments

Comments
 (0)