Skip to content

Commit

Permalink
http2: use aliased buffer for perf stats, add stats
Browse files Browse the repository at this point in the history
Add an aliased buffer for session and stream statistics,
add a few more metrics

PR-URL: nodejs#18020
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
  • Loading branch information
jasnell authored and kjin committed Apr 30, 2018
1 parent 4eb9cca commit 39a0d35
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 66 deletions.
20 changes: 16 additions & 4 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

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

Expand Down Expand Up @@ -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<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 @@ -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;
Expand Down Expand Up @@ -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<char*>(outgoing_storage_.data() + offset),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2142,6 +2145,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 @@ -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;
}

Expand All @@ -2217,6 +2223,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 @@ -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;
}

Expand Down Expand Up @@ -2863,6 +2873,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

0 comments on commit 39a0d35

Please sign in to comment.