Skip to content
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

http2: use aliased buffer for perf stats, add stats #18020

Closed
wants to merge 2 commits into from
Closed
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
20 changes: 16 additions & 4 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
68 changes: 68 additions & 0 deletions lib/perf_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down
120 changes: 67 additions & 53 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
}

Expand All @@ -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.
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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));
Expand Down
Loading