diff --git a/doc/api/http2.md b/doc/api/http2.md index 8b3e33723b4c7d..2706f4cff3f5e2 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -3022,23 +3022,35 @@ The `name` property of the `PerformanceEntry` will be equal to either If `name` is equal to `Http2Stream`, the `PerformanceEntry` will contain the following additional properties: +* `bytesRead` {number} The number of DATA frame bytes received for this + `Http2Stream`. +* `bytesWritten` {number} The number of DATA frame bytes sent for this + `Http2Stream`. +* `id` {number} The identifier of the associated `Http2Stream` * `timeToFirstByte` {number} The number of milliseconds elapsed between the `PerformanceEntry` `startTime` and the reception of the first `DATA` frame. +* `timeToFirstByteSent` {number} The number of milliseconds elapsed between + the `PerformanceEntry` `startTime` and sending of the first `DATA` frame. * `timeToFirstHeader` {number} The number of milliseconds elapsed between the `PerformanceEntry` `startTime` and the reception of the first header. If `name` is equal to `Http2Session`, the `PerformanceEntry` will contain the following additional properties: +* `bytesRead` {number} The number of bytes received for this `Http2Session`. +* `bytesWritten` {number} The number of bytes sent for this `Http2Session`. +* `framesReceived` {number} The number of HTTP/2 frames received by the + `Http2Session`. +* `framesSent` {number} The number of HTTP/2 frames sent by the `Http2Session`. +* `maxConcurrentStreams` {number} The maximum number of streams concurrently + open during the lifetime of the `Http2Session`. * `pingRTT` {number} The number of milliseconds elapsed since the transmission of a `PING` frame and the reception of its acknowledgment. Only present if a `PING` frame has been sent on the `Http2Session`. -* `streamCount` {number} The number of `Http2Stream` instances processed by - the `Http2Session`. * `streamAverageDuration` {number} The average duration (in milliseconds) for all `Http2Stream` instances. -* `framesReceived` {number} The number of HTTP/2 frames received by the - `Http2Session`. +* `streamCount` {number} The number of `Http2Stream` instances processed by + the `Http2Session`. * `type` {string} Either `'server'` or `'client'` to identify the type of `Http2Session`. diff --git a/lib/perf_hooks.js b/lib/perf_hooks.js index 7e1d085b525b44..4a05f7ccba7bc8 100644 --- a/lib/perf_hooks.js +++ b/lib/perf_hooks.js @@ -66,6 +66,70 @@ const observerableTypes = [ 'http2' ]; +const IDX_STREAM_STATS_ID = 0; +const IDX_STREAM_STATS_TIMETOFIRSTBYTE = 1; +const IDX_STREAM_STATS_TIMETOFIRSTHEADER = 2; +const IDX_STREAM_STATS_TIMETOFIRSTBYTESENT = 3; +const IDX_STREAM_STATS_SENTBYTES = 4; +const IDX_STREAM_STATS_RECEIVEDBYTES = 5; + +const IDX_SESSION_STATS_TYPE = 0; +const IDX_SESSION_STATS_PINGRTT = 1; +const IDX_SESSION_STATS_FRAMESRECEIVED = 2; +const IDX_SESSION_STATS_FRAMESSENT = 3; +const IDX_SESSION_STATS_STREAMCOUNT = 4; +const IDX_SESSION_STATS_STREAMAVERAGEDURATION = 5; +const IDX_SESSION_STATS_DATA_SENT = 6; +const IDX_SESSION_STATS_DATA_RECEIVED = 7; +const IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS = 8; + +let sessionStats; +let streamStats; + +function collectHttp2Stats(entry) { + switch (entry.name) { + case 'Http2Stream': + if (streamStats === undefined) + streamStats = process.binding('http2').streamStats; + entry.id = + streamStats[IDX_STREAM_STATS_ID] >>> 0; + entry.timeToFirstByte = + streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTE]; + entry.timeToFirstHeader = + streamStats[IDX_STREAM_STATS_TIMETOFIRSTHEADER]; + entry.timeToFirstByteSent = + streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT]; + entry.bytesWritten = + streamStats[IDX_STREAM_STATS_SENTBYTES]; + entry.bytesRead = + streamStats[IDX_STREAM_STATS_RECEIVEDBYTES]; + break; + case 'Http2Session': + if (sessionStats === undefined) + sessionStats = process.binding('http2').sessionStats; + entry.type = + sessionStats[IDX_SESSION_STATS_TYPE] >>> 0 === 0 ? 'server' : 'client'; + entry.pingRTT = + sessionStats[IDX_SESSION_STATS_PINGRTT]; + entry.framesReceived = + sessionStats[IDX_SESSION_STATS_FRAMESRECEIVED]; + entry.framesSent = + sessionStats[IDX_SESSION_STATS_FRAMESSENT]; + entry.streamCount = + sessionStats[IDX_SESSION_STATS_STREAMCOUNT]; + entry.streamAverageDuration = + sessionStats[IDX_SESSION_STATS_STREAMAVERAGEDURATION]; + entry.bytesWritten = + sessionStats[IDX_SESSION_STATS_DATA_SENT]; + entry.bytesRead = + sessionStats[IDX_SESSION_STATS_DATA_RECEIVED]; + entry.maxConcurrentStreams = + sessionStats[IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS]; + break; + } +} + + let errors; function lazyErrors() { if (errors === undefined) @@ -467,6 +531,10 @@ function doNotify() { // Set up the callback used to receive PerformanceObserver notifications function observersCallback(entry) { const type = mapTypes(entry.entryType); + + if (type === NODE_PERFORMANCE_ENTRY_TYPE_HTTP2) + collectHttp2Stats(entry); + performance[kInsertEntry](entry); const list = getObserversList(type); diff --git a/src/node_http2.cc b/src/node_http2.cc index 844421fa35c1c3..2bcf7ccfa0554e 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -471,6 +471,8 @@ Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { callbacks, OnSendData); nghttp2_session_callbacks_set_on_invalid_frame_recv_callback( callbacks, OnInvalidFrame); + nghttp2_session_callbacks_set_on_frame_send_callback( + callbacks, OnFrameSent); if (kHasGetPaddingCallback) { nghttp2_session_callbacks_set_select_padding_callback( @@ -560,28 +562,35 @@ inline void Http2Stream::EmitStatistics() { if (!HasHttp2Observer(env())) return; Http2StreamPerformanceEntry* entry = - new Http2StreamPerformanceEntry(env(), statistics_); + new Http2StreamPerformanceEntry(env(), id_, statistics_); env()->SetImmediate([](Environment* env, void* data) { - Local context = env->context(); Http2StreamPerformanceEntry* entry = static_cast(data); if (HasHttp2Observer(env)) { - Local obj = entry->ToObject(); - v8::PropertyAttribute attr = - static_cast(v8::ReadOnly | v8::DontDelete); - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstByte"), - Number::New(env->isolate(), - (entry->first_byte() - entry->startTimeNano()) / 1e6), - attr).FromJust(); - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstHeader"), - Number::New(env->isolate(), - (entry->first_header() - entry->startTimeNano()) / 1e6), - attr).FromJust(); - entry->Notify(obj); + AliasedBuffer& buffer = + env->http2_state()->stream_stats_buffer; + buffer[IDX_STREAM_STATS_ID] = entry->id(); + if (entry->first_byte() != 0) { + buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTE] = + (entry->first_byte() - entry->startTimeNano()) / 1e6; + } else { + buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTE] = 0; + } + if (entry->first_header() != 0) { + buffer[IDX_STREAM_STATS_TIMETOFIRSTHEADER] = + (entry->first_header() - entry->startTimeNano()) / 1e6; + } else { + buffer[IDX_STREAM_STATS_TIMETOFIRSTHEADER] = 0; + } + if (entry->first_byte_sent() != 0) { + buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT] = + (entry->first_byte_sent() - entry->startTimeNano()) / 1e6; + } else { + buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT] = 0; + } + buffer[IDX_STREAM_STATS_SENTBYTES] = entry->sent_bytes(); + buffer[IDX_STREAM_STATS_RECEIVEDBYTES] = entry->received_bytes(); + entry->Notify(entry->ToObject()); } delete entry; }, static_cast(entry)); @@ -591,45 +600,25 @@ inline void Http2Session::EmitStatistics() { if (!HasHttp2Observer(env())) return; Http2SessionPerformanceEntry* entry = - new Http2SessionPerformanceEntry(env(), statistics_, TypeName()); + new Http2SessionPerformanceEntry(env(), statistics_, session_type_); env()->SetImmediate([](Environment* env, void* data) { - Local context = env->context(); Http2SessionPerformanceEntry* entry = static_cast(data); if (HasHttp2Observer(env)) { - Local obj = entry->ToObject(); - v8::PropertyAttribute attr = - static_cast(v8::ReadOnly | v8::DontDelete); - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "type"), - String::NewFromUtf8(env->isolate(), - entry->typeName(), - v8::NewStringType::kInternalized) - .ToLocalChecked(), attr).FromJust(); - if (entry->ping_rtt() != 0) { - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "pingRTT"), - Number::New(env->isolate(), entry->ping_rtt() / 1e6), - attr).FromJust(); - } - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "framesReceived"), - Integer::NewFromUnsigned(env->isolate(), entry->frame_count()), - attr).FromJust(); - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "streamCount"), - Integer::New(env->isolate(), entry->stream_count()), - attr).FromJust(); - obj->DefineOwnProperty( - context, - FIXED_ONE_BYTE_STRING(env->isolate(), "streamAverageDuration"), - Number::New(env->isolate(), entry->stream_average_duration()), - attr).FromJust(); - entry->Notify(obj); + AliasedBuffer& buffer = + env->http2_state()->session_stats_buffer; + buffer[IDX_SESSION_STATS_TYPE] = entry->type(); + buffer[IDX_SESSION_STATS_PINGRTT] = entry->ping_rtt() / 1e6; + buffer[IDX_SESSION_STATS_FRAMESRECEIVED] = entry->frame_count(); + buffer[IDX_SESSION_STATS_FRAMESSENT] = entry->frame_sent(); + buffer[IDX_SESSION_STATS_STREAMCOUNT] = entry->stream_count(); + buffer[IDX_SESSION_STATS_STREAMAVERAGEDURATION] = + entry->stream_average_duration(); + buffer[IDX_SESSION_STATS_DATA_SENT] = entry->data_sent(); + buffer[IDX_SESSION_STATS_DATA_RECEIVED] = entry->data_received(); + buffer[IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS] = + entry->max_concurrent_streams(); + entry->Notify(entry->ToObject()); } delete entry; }, static_cast(entry)); @@ -695,6 +684,9 @@ inline bool Http2Session::CanAddStream() { inline void Http2Session::AddStream(Http2Stream* stream) { CHECK_GE(++statistics_.stream_count, 0); streams_[stream->id()] = stream; + size_t size = streams_.size(); + if (size > statistics_.max_concurrent_streams) + statistics_.max_concurrent_streams = size; IncrementCurrentSessionMemory(stream->self_size()); } @@ -963,6 +955,14 @@ inline int Http2Session::OnFrameNotSent(nghttp2_session* handle, return 0; } +inline int Http2Session::OnFrameSent(nghttp2_session* handle, + const nghttp2_frame* frame, + void* user_data) { + Http2Session* session = static_cast(user_data); + session->statistics_.frame_sent += 1; + return 0; +} + // Called by nghttp2 when a stream closes. inline int Http2Session::OnStreamClose(nghttp2_session* handle, int32_t id, @@ -1040,6 +1040,7 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle, // If the stream has been destroyed, ignore this chunk if (stream->IsDestroyed()) return 0; + stream->statistics_.received_bytes += len; stream->AddChunk(data, len); } return 0; @@ -1494,6 +1495,7 @@ void Http2Session::SendPendingData() { size_t offset = 0; size_t i = 0; for (const nghttp2_stream_write& write : outgoing_buffers_) { + statistics_.data_sent += write.buf.len; if (write.buf.base == nullptr) { bufs[i++] = uv_buf_init( reinterpret_cast(outgoing_storage_.data() + offset), @@ -1643,6 +1645,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread, if (bufs->len > 0) { // Only pass data on if nread > 0 uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; + session->statistics_.data_received += nread; ssize_t ret = session->Write(buf, 1); // Note: if ssize_t is not defined (e.g. on Win32), nghttp2 will typedef @@ -2142,6 +2145,8 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle, void* user_data) { Http2Session* session = static_cast(user_data); Http2Stream* stream = session->FindStream(id); + if (stream->statistics_.first_byte_sent == 0) + stream->statistics_.first_byte_sent = uv_hrtime(); DEBUG_HTTP2SESSION2(session, "reading outbound file data for stream %d", id); CHECK_EQ(id, stream->id()); @@ -2192,6 +2197,7 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle, return NGHTTP2_ERR_CALLBACK_FAILURE; } + stream->statistics_.sent_bytes += numchars; return numchars; } @@ -2217,6 +2223,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle, Http2Session* session = static_cast(user_data); DEBUG_HTTP2SESSION2(session, "reading outbound data for stream %d", id); Http2Stream* stream = GetStream(session, id, source); + if (stream->statistics_.first_byte_sent == 0) + stream->statistics_.first_byte_sent = uv_hrtime(); CHECK_EQ(id, stream->id()); size_t amount = 0; // amount of data being sent in this data frame. @@ -2250,6 +2258,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle, if (session->IsDestroyed()) return NGHTTP2_ERR_CALLBACK_FAILURE; } + + stream->statistics_.sent_bytes += amount; return amount; } @@ -2863,6 +2873,10 @@ void Initialize(Local target, "settingsBuffer", state->settings_buffer.GetJSArray()); SET_STATE_TYPEDARRAY( "optionsBuffer", state->options_buffer.GetJSArray()); + SET_STATE_TYPEDARRAY( + "streamStats", state->stream_stats_buffer.GetJSArray()); + SET_STATE_TYPEDARRAY( + "sessionStats", state->session_stats_buffer.GetJSArray()); #undef SET_STATE_TYPEDARRAY env->set_http2_state(std::move(state)); diff --git a/src/node_http2.h b/src/node_http2.h index 5b5f7e5f52e084..a7938a5f800854 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -715,8 +715,11 @@ class Http2Stream : public AsyncWrap, struct Statistics { uint64_t start_time; uint64_t end_time; - uint64_t first_header; // Time first header was received - uint64_t first_byte; // Time first data frame byte was received + uint64_t first_header; // Time first header was received + uint64_t first_byte; // Time first DATA frame byte was received + uint64_t first_byte_sent; // Time first DATA frame byte was sent + uint64_t sent_bytes; + uint64_t received_bytes; }; Statistics statistics_ = {}; @@ -949,8 +952,12 @@ class Http2Session : public AsyncWrap { uint64_t start_time; uint64_t end_time; uint64_t ping_rtt; + uint64_t data_sent; + uint64_t data_received; uint32_t frame_count; + uint32_t frame_sent; int32_t stream_count; + size_t max_concurrent_streams; double stream_average_duration; }; @@ -995,6 +1002,10 @@ class Http2Session : public AsyncWrap { const nghttp2_frame* frame, int error_code, void* user_data); + static inline int OnFrameSent( + nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data); static inline int OnStreamClose( nghttp2_session* session, int32_t id, @@ -1115,21 +1126,29 @@ class Http2SessionPerformanceEntry : public PerformanceEntry { Http2SessionPerformanceEntry( Environment* env, const Http2Session::Statistics& stats, - const char* kind) : + nghttp2_session_type type) : PerformanceEntry(env, "Http2Session", "http2", stats.start_time, stats.end_time), ping_rtt_(stats.ping_rtt), + data_sent_(stats.data_sent), + data_received_(stats.data_received), frame_count_(stats.frame_count), + frame_sent_(stats.frame_sent), stream_count_(stats.stream_count), + max_concurrent_streams_(stats.max_concurrent_streams), stream_average_duration_(stats.stream_average_duration), - kind_(kind) { } + session_type_(type) { } uint64_t ping_rtt() const { return ping_rtt_; } + uint64_t data_sent() const { return data_sent_; } + uint64_t data_received() const { return data_received_; } uint32_t frame_count() const { return frame_count_; } + uint32_t frame_sent() const { return frame_sent_; } int32_t stream_count() const { return stream_count_; } + size_t max_concurrent_streams() const { return max_concurrent_streams_; } double stream_average_duration() const { return stream_average_duration_; } - const char* typeName() const { return kind_; } + nghttp2_session_type type() const { return session_type_; } void Notify(Local obj) { PerformanceEntry::Notify(env(), kind(), obj); @@ -1137,33 +1156,50 @@ class Http2SessionPerformanceEntry : public PerformanceEntry { private: uint64_t ping_rtt_; + uint64_t data_sent_; + uint64_t data_received_; uint32_t frame_count_; + uint32_t frame_sent_; int32_t stream_count_; + size_t max_concurrent_streams_; double stream_average_duration_; - const char* kind_; + nghttp2_session_type session_type_; }; class Http2StreamPerformanceEntry : public PerformanceEntry { public: Http2StreamPerformanceEntry( Environment* env, + int32_t id, const Http2Stream::Statistics& stats) : PerformanceEntry(env, "Http2Stream", "http2", stats.start_time, stats.end_time), + id_(id), first_header_(stats.first_header), - first_byte_(stats.first_byte) { } + first_byte_(stats.first_byte), + first_byte_sent_(stats.first_byte_sent), + sent_bytes_(stats.sent_bytes), + received_bytes_(stats.received_bytes) { } + int32_t id() const { return id_; } uint64_t first_header() const { return first_header_; } uint64_t first_byte() const { return first_byte_; } + uint64_t first_byte_sent() const { return first_byte_sent_; } + uint64_t sent_bytes() const { return sent_bytes_; } + uint64_t received_bytes() const { return received_bytes_; } void Notify(Local obj) { PerformanceEntry::Notify(env(), kind(), obj); } private: + int32_t id_; uint64_t first_header_; uint64_t first_byte_; + uint64_t first_byte_sent_; + uint64_t sent_bytes_; + uint64_t received_bytes_; }; class Http2Session::Http2Ping : public AsyncWrap { diff --git a/src/node_http2_state.h b/src/node_http2_state.h index af0740c994e765..ed88f068a04b16 100644 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -61,6 +61,29 @@ namespace http2 { PADDING_BUF_FIELD_COUNT }; + enum Http2StreamStatisticsIndex { + IDX_STREAM_STATS_ID, + IDX_STREAM_STATS_TIMETOFIRSTBYTE, + IDX_STREAM_STATS_TIMETOFIRSTHEADER, + IDX_STREAM_STATS_TIMETOFIRSTBYTESENT, + IDX_STREAM_STATS_SENTBYTES, + IDX_STREAM_STATS_RECEIVEDBYTES, + IDX_STREAM_STATS_COUNT + }; + + enum Http2SessionStatisticsIndex { + IDX_SESSION_STATS_TYPE, + IDX_SESSION_STATS_PINGRTT, + IDX_SESSION_STATS_FRAMESRECEIVED, + IDX_SESSION_STATS_FRAMESSENT, + IDX_SESSION_STATS_STREAMCOUNT, + IDX_SESSION_STATS_STREAMAVERAGEDURATION, + IDX_SESSION_STATS_DATA_SENT, + IDX_SESSION_STATS_DATA_RECEIVED, + IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS, + IDX_SESSION_STATS_COUNT + }; + class http2_state { public: explicit http2_state(v8::Isolate* isolate) : @@ -77,6 +100,16 @@ class http2_state { offsetof(http2_state_internal, stream_state_buffer), IDX_STREAM_STATE_COUNT, root_buffer), + stream_stats_buffer( + isolate, + offsetof(http2_state_internal, stream_stats_buffer), + IDX_STREAM_STATS_COUNT, + root_buffer), + session_stats_buffer( + isolate, + offsetof(http2_state_internal, session_stats_buffer), + IDX_SESSION_STATS_COUNT, + root_buffer), padding_buffer( isolate, offsetof(http2_state_internal, padding_buffer), @@ -97,6 +130,8 @@ class http2_state { AliasedBuffer root_buffer; AliasedBuffer session_state_buffer; AliasedBuffer stream_state_buffer; + AliasedBuffer stream_stats_buffer; + AliasedBuffer session_stats_buffer; AliasedBuffer padding_buffer; AliasedBuffer options_buffer; AliasedBuffer settings_buffer; @@ -106,6 +141,8 @@ class http2_state { // doubles first so that they are always sizeof(double)-aligned double session_state_buffer[IDX_SESSION_STATE_COUNT]; double stream_state_buffer[IDX_STREAM_STATE_COUNT]; + double stream_stats_buffer[IDX_STREAM_STATS_COUNT]; + double session_stats_buffer[IDX_SESSION_STATS_COUNT]; uint32_t padding_buffer[PADDING_BUF_FIELD_COUNT]; uint32_t options_buffer[IDX_OPTIONS_FLAGS + 1]; uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1]; diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js index f2ef29cec25e06..07d9c55ed7e0d2 100644 --- a/test/parallel/test-http2-perf_hooks.js +++ b/test/parallel/test-http2-perf_hooks.js @@ -8,7 +8,7 @@ const h2 = require('http2'); const { PerformanceObserver } = require('perf_hooks'); -const obs = new PerformanceObserver((items) => { +const obs = new PerformanceObserver(common.mustCall((items) => { const entry = items.getEntries()[0]; assert.strictEqual(entry.entryType, 'http2'); assert.strictEqual(typeof entry.startTime, 'number'); @@ -19,6 +19,10 @@ const obs = new PerformanceObserver((items) => { assert.strictEqual(typeof entry.streamAverageDuration, 'number'); assert.strictEqual(typeof entry.streamCount, 'number'); assert.strictEqual(typeof entry.framesReceived, 'number'); + assert.strictEqual(typeof entry.framesSent, 'number'); + assert.strictEqual(typeof entry.bytesWritten, 'number'); + assert.strictEqual(typeof entry.bytesRead, 'number'); + assert.strictEqual(typeof entry.maxConcurrentStreams, 'number'); switch (entry.type) { case 'server': assert.strictEqual(entry.streamCount, 1); @@ -34,12 +38,15 @@ const obs = new PerformanceObserver((items) => { break; case 'Http2Stream': assert.strictEqual(typeof entry.timeToFirstByte, 'number'); + assert.strictEqual(typeof entry.timeToFirstByteSent, 'number'); assert.strictEqual(typeof entry.timeToFirstHeader, 'number'); + assert.strictEqual(typeof entry.bytesWritten, 'number'); + assert.strictEqual(typeof entry.bytesRead, 'number'); break; default: assert.fail('invalid entry name'); } -}); +}, 4)); obs.observe({ entryTypes: ['http2'] }); const body =