http2: use aliased buffer for perf stats, add stats

Add an aliased buffer for session and stream statistics,
add a few more metrics

PR-URL: https://github.com/nodejs/node/pull/18020
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
This commit is contained in:
James M Snell 2018-01-06 11:50:58 -08:00
parent ececdd3167
commit a02fcd2716
6 changed files with 240 additions and 66 deletions

View File

@ -3003,23 +3003,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`.

View File

@ -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);

View File

@ -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(
@ -559,28 +561,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> context = env->context();
Http2StreamPerformanceEntry* entry =
static_cast<Http2StreamPerformanceEntry*>(data);
if (HasHttp2Observer(env)) {
Local<Object> obj = entry->ToObject();
v8::PropertyAttribute attr =
static_cast<v8::PropertyAttribute>(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<double, v8::Float64Array>& 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<void*>(entry));
@ -590,45 +599,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> context = env->context();
Http2SessionPerformanceEntry* entry =
static_cast<Http2SessionPerformanceEntry*>(data);
if (HasHttp2Observer(env)) {
Local<Object> obj = entry->ToObject();
v8::PropertyAttribute attr =
static_cast<v8::PropertyAttribute>(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<double, v8::Float64Array>& 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<void*>(entry));
@ -694,6 +683,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());
}
@ -962,6 +954,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<Http2Session*>(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,
@ -1039,6 +1039,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;
@ -1493,6 +1494,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<char*>(outgoing_storage_.data() + offset),
@ -1642,6 +1644,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
@ -2141,6 +2144,8 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
void* user_data) {
Http2Session* session = static_cast<Http2Session*>(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());
@ -2191,6 +2196,7 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
stream->statistics_.sent_bytes += numchars;
return numchars;
}
@ -2216,6 +2222,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
Http2Session* session = static_cast<Http2Session*>(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.
@ -2249,6 +2257,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;
}
@ -2862,6 +2872,10 @@ void Initialize(Local<Object> 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));

View File

@ -716,7 +716,10 @@ class Http2Stream : public AsyncWrap,
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_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<Value> 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<Value> 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 {

View File

@ -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<uint8_t, v8::Uint8Array> root_buffer;
AliasedBuffer<double, v8::Float64Array> session_state_buffer;
AliasedBuffer<double, v8::Float64Array> stream_state_buffer;
AliasedBuffer<double, v8::Float64Array> stream_stats_buffer;
AliasedBuffer<double, v8::Float64Array> session_stats_buffer;
AliasedBuffer<uint32_t, v8::Uint32Array> padding_buffer;
AliasedBuffer<uint32_t, v8::Uint32Array> options_buffer;
AliasedBuffer<uint32_t, v8::Uint32Array> 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];

View File

@ -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 =