Skip to content

Commit aadc57a

Browse files
authored
H2 monitor (#377)
Add support of connection monitor for HTTP/2
1 parent 1a3614f commit aadc57a

File tree

9 files changed

+268
-36
lines changed

9 files changed

+268
-36
lines changed

include/aws/http/private/h2_connection.h

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <aws/http/private/connection_impl.h>
1515
#include <aws/http/private/h2_frames.h>
16+
#include <aws/http/statistics.h>
1617

1718
struct aws_h2_decoder;
1819
struct aws_h2_stream;
@@ -116,6 +117,14 @@ struct aws_h2_connection {
116117
int channel_shutdown_error_code;
117118
bool channel_shutdown_immediately;
118119
bool channel_shutdown_waiting_for_goaway_to_be_written;
120+
121+
/* TODO: Consider adding stream monitor */
122+
struct aws_crt_statistics_http2_channel stats;
123+
124+
/* Timestamp when connection has data to send, which is when there is an active stream with body to send */
125+
uint64_t outgoing_timestamp_ns;
126+
/* Timestamp when connection has data to receive, which is when there is an active stream */
127+
uint64_t incoming_timestamp_ns;
119128
} thread_data;
120129

121130
/* Any thread may touch this data, but the lock must be held (unless it's an atomic) */

include/aws/http/statistics.h

+22
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
enum aws_crt_http_statistics_category {
1414
AWSCRT_STAT_CAT_HTTP1_CHANNEL = AWS_CRT_STATISTICS_CATEGORY_BEGIN_RANGE(AWS_C_HTTP_PACKAGE_ID),
15+
AWSCRT_STAT_CAT_HTTP2_CHANNEL,
1516
};
1617

1718
/**
@@ -28,6 +29,16 @@ struct aws_crt_statistics_http1_channel {
2829
uint32_t current_incoming_stream_id;
2930
};
3031

32+
struct aws_crt_statistics_http2_channel {
33+
aws_crt_statistics_category_t category;
34+
35+
uint64_t pending_outgoing_stream_ms;
36+
uint64_t pending_incoming_stream_ms;
37+
38+
/* True if during the time of report, there has ever been no active streams on the connection */
39+
bool was_inactive;
40+
};
41+
3142
AWS_EXTERN_C_BEGIN
3243

3344
/**
@@ -48,6 +59,17 @@ void aws_crt_statistics_http1_channel_cleanup(struct aws_crt_statistics_http1_ch
4859
AWS_HTTP_API
4960
void aws_crt_statistics_http1_channel_reset(struct aws_crt_statistics_http1_channel *stats);
5061

62+
/**
63+
* Initializes a HTTP/2 channel handler statistics struct
64+
*/
65+
AWS_HTTP_API
66+
void aws_crt_statistics_http2_channel_init(struct aws_crt_statistics_http2_channel *stats);
67+
/**
68+
* Resets a HTTP/2 channel handler statistics struct's statistics
69+
*/
70+
AWS_HTTP_API
71+
void aws_crt_statistics_http2_channel_reset(struct aws_crt_statistics_http2_channel *stats);
72+
5173
AWS_EXTERN_C_END
5274

5375
#endif /* AWS_HTTP_STATISTICS_H */

source/connection_monitor.c

+32-14
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@ static void s_process_statistics(
3232
uint64_t pending_write_interval_ms = 0;
3333
uint64_t bytes_read = 0;
3434
uint64_t bytes_written = 0;
35-
uint32_t current_outgoing_stream_id = 0;
36-
uint32_t current_incoming_stream_id = 0;
35+
uint32_t h1_current_outgoing_stream_id = 0;
36+
uint32_t h1_current_incoming_stream_id = 0;
3737

3838
/*
3939
* Pull out the data needed to perform the throughput calculation
4040
*/
4141
size_t stats_count = aws_array_list_length(stats_list);
42+
bool h2 = false;
43+
bool h2_was_inactive = false;
44+
4245
for (size_t i = 0; i < stats_count; ++i) {
4346
struct aws_crt_statistics_base *stats_base = NULL;
4447
if (aws_array_list_get_at(stats_list, &stats_base, i)) {
@@ -54,13 +57,24 @@ static void s_process_statistics(
5457
}
5558

5659
case AWSCRT_STAT_CAT_HTTP1_CHANNEL: {
60+
AWS_ASSERT(!h2);
5761
struct aws_crt_statistics_http1_channel *http1_stats =
5862
(struct aws_crt_statistics_http1_channel *)stats_base;
5963
pending_read_interval_ms = http1_stats->pending_incoming_stream_ms;
6064
pending_write_interval_ms = http1_stats->pending_outgoing_stream_ms;
61-
current_outgoing_stream_id = http1_stats->current_outgoing_stream_id;
62-
current_incoming_stream_id = http1_stats->current_incoming_stream_id;
65+
h1_current_outgoing_stream_id = http1_stats->current_outgoing_stream_id;
66+
h1_current_incoming_stream_id = http1_stats->current_incoming_stream_id;
67+
68+
break;
69+
}
6370

71+
case AWSCRT_STAT_CAT_HTTP2_CHANNEL: {
72+
struct aws_crt_statistics_http2_channel *h2_stats =
73+
(struct aws_crt_statistics_http2_channel *)stats_base;
74+
pending_read_interval_ms = h2_stats->pending_incoming_stream_ms;
75+
pending_write_interval_ms = h2_stats->pending_outgoing_stream_ms;
76+
h2_was_inactive |= h2_stats->was_inactive;
77+
h2 = true;
6478
break;
6579
}
6680

@@ -110,17 +124,21 @@ static void s_process_statistics(
110124
bytes_per_second);
111125

112126
/*
113-
* Check throughput only if at least one stream exists and was observed in that role previously
114-
*
115-
* ToDo: This logic only makes sense from an h1 perspective. A similar requirement could be placed on
116-
* h2 stats by analyzing/tracking the min and max stream ids (per odd/even) at process timepoints.
127+
* Check throughput only if the connection has active stream and no gap between.
117128
*/
118-
bool check_throughput =
119-
(current_incoming_stream_id != 0 && current_incoming_stream_id == impl->last_incoming_stream_id) ||
120-
(current_outgoing_stream_id != 0 && current_outgoing_stream_id == impl->last_outgoing_stream_id);
121-
122-
impl->last_outgoing_stream_id = current_outgoing_stream_id;
123-
impl->last_incoming_stream_id = current_incoming_stream_id;
129+
bool check_throughput = false;
130+
if (h2) {
131+
/* For HTTP/2, check throughput only if there always has any active stream on the connection */
132+
check_throughput = !h2_was_inactive;
133+
} else {
134+
/* For HTTP/1, check throughput only if at least one stream exists and was observed in that role previously */
135+
check_throughput =
136+
(h1_current_incoming_stream_id != 0 && h1_current_incoming_stream_id == impl->last_incoming_stream_id) ||
137+
(h1_current_outgoing_stream_id != 0 && h1_current_outgoing_stream_id == impl->last_outgoing_stream_id);
138+
139+
impl->last_outgoing_stream_id = h1_current_outgoing_stream_id;
140+
impl->last_incoming_stream_id = h1_current_incoming_stream_id;
141+
}
124142
impl->last_measured_throughput = bytes_per_second;
125143

126144
if (!check_throughput) {

source/h2_connection.c

+96-1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ struct aws_h2err s_decoder_on_goaway(
146146
uint32_t error_code,
147147
struct aws_byte_cursor debug_data,
148148
void *userdata);
149+
static void s_reset_statistics(struct aws_channel_handler *handler);
150+
static void s_gather_statistics(struct aws_channel_handler *handler, struct aws_array_list *stats);
149151

150152
static struct aws_http_connection_vtable s_h2_connection_vtable = {
151153
.channel_handler_vtable =
@@ -157,6 +159,8 @@ static struct aws_http_connection_vtable s_h2_connection_vtable = {
157159
.initial_window_size = s_handler_initial_window_size,
158160
.message_overhead = s_handler_message_overhead,
159161
.destroy = s_handler_destroy,
162+
.reset_statistics = s_reset_statistics,
163+
.gather_statistics = s_gather_statistics,
160164
},
161165

162166
.on_channel_handler_installed = s_handler_installed,
@@ -219,6 +223,14 @@ static void s_release_stream_and_connection_lock(struct aws_h2_stream *stream, s
219223
(void)err;
220224
}
221225

226+
static void s_add_time_measurement_to_stats(uint64_t start_ns, uint64_t end_ns, uint64_t *output_ms) {
227+
if (end_ns > start_ns) {
228+
*output_ms += aws_timestamp_convert(end_ns - start_ns, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_MILLIS, NULL);
229+
} else {
230+
*output_ms = 0;
231+
}
232+
}
233+
222234
/**
223235
* Internal function for bringing connection to a stop.
224236
* Invoked multiple times, including when:
@@ -373,6 +385,9 @@ static struct aws_h2_connection *s_connection_new(
373385
connection->thread_data.goaway_received_last_stream_id = AWS_H2_STREAM_ID_MAX;
374386
connection->thread_data.goaway_sent_last_stream_id = AWS_H2_STREAM_ID_MAX;
375387

388+
aws_crt_statistics_http2_channel_init(&connection->thread_data.stats);
389+
connection->thread_data.stats.was_inactive = true; /* Start with non active streams */
390+
376391
connection->synced_data.is_open = true;
377392
connection->synced_data.new_stream_error_code = AWS_ERROR_SUCCESS;
378393

@@ -795,6 +810,9 @@ static int s_encode_data_from_outgoing_streams(struct aws_h2_connection *connect
795810

796811
AWS_PRECONDITION(aws_channel_thread_is_callers_thread(connection->base.channel_slot->channel));
797812
struct aws_linked_list *outgoing_streams_list = &connection->thread_data.outgoing_streams_list;
813+
if (aws_linked_list_empty(outgoing_streams_list)) {
814+
return AWS_OP_SUCCESS;
815+
}
798816
struct aws_linked_list *stalled_window_streams_list = &connection->thread_data.stalled_window_streams_list;
799817
struct aws_linked_list *waiting_streams_list = &connection->thread_data.waiting_streams_list;
800818

@@ -816,7 +834,7 @@ static int s_encode_data_from_outgoing_streams(struct aws_h2_connection *connect
816834
"Peer connection's flow-control window is too small now %zu. Connection will stop sending DATA until "
817835
"WINDOW_UPDATE is received.",
818836
connection->thread_data.window_size_peer);
819-
break;
837+
goto done;
820838
}
821839

822840
/* Stop looping if message is so full it's not worth the bother */
@@ -885,6 +903,16 @@ static int s_encode_data_from_outgoing_streams(struct aws_h2_connection *connect
885903
return aws_raise_error(aws_error_code);
886904
}
887905

906+
if (aws_linked_list_empty(outgoing_streams_list)) {
907+
/* transition from something to write -> nothing to write */
908+
uint64_t now_ns = 0;
909+
aws_channel_current_clock_time(connection->base.channel_slot->channel, &now_ns);
910+
s_add_time_measurement_to_stats(
911+
connection->thread_data.outgoing_timestamp_ns,
912+
now_ns,
913+
&connection->thread_data.stats.pending_outgoing_stream_ms);
914+
}
915+
888916
return AWS_OP_SUCCESS;
889917
}
890918

@@ -1768,6 +1796,19 @@ static void s_stream_complete(struct aws_h2_connection *connection, struct aws_h
17681796
aws_linked_list_remove(&stream->node);
17691797
}
17701798

1799+
if (aws_hash_table_get_entry_count(&connection->thread_data.active_streams_map) == 0 &&
1800+
connection->thread_data.incoming_timestamp_ns != 0) {
1801+
uint64_t now_ns = 0;
1802+
aws_channel_current_clock_time(connection->base.channel_slot->channel, &now_ns);
1803+
/* transition from something to read -> nothing to read and nothing to write */
1804+
s_add_time_measurement_to_stats(
1805+
connection->thread_data.incoming_timestamp_ns,
1806+
now_ns,
1807+
&connection->thread_data.stats.pending_incoming_stream_ms);
1808+
connection->thread_data.stats.was_inactive = true;
1809+
connection->thread_data.incoming_timestamp_ns = 0;
1810+
}
1811+
17711812
aws_h2_stream_complete(stream, error_code);
17721813

17731814
/* release connection's hold on stream */
@@ -1867,6 +1908,14 @@ static void s_move_stream_to_thread(
18671908
if (aws_h2_stream_on_activated(stream, &body_state)) {
18681909
goto error;
18691910
}
1911+
1912+
if (aws_hash_table_get_entry_count(&connection->thread_data.active_streams_map) == 1) {
1913+
/* transition from nothing to read -> something to read */
1914+
uint64_t now_ns = 0;
1915+
aws_channel_current_clock_time(connection->base.channel_slot->channel, &now_ns);
1916+
connection->thread_data.incoming_timestamp_ns = now_ns;
1917+
}
1918+
18701919
switch (body_state) {
18711920
case AWS_H2_STREAM_BODY_STATE_WAITING_WRITES:
18721921
aws_linked_list_push_back(&connection->thread_data.waiting_streams_list, &stream->node);
@@ -2753,3 +2802,49 @@ static size_t s_handler_message_overhead(struct aws_channel_handler *handler) {
27532802
/* "All frames begin with a fixed 9-octet header followed by a variable-length payload" (RFC-7540 4.1) */
27542803
return 9;
27552804
}
2805+
2806+
static void s_reset_statistics(struct aws_channel_handler *handler) {
2807+
struct aws_h2_connection *connection = handler->impl;
2808+
aws_crt_statistics_http2_channel_reset(&connection->thread_data.stats);
2809+
if (aws_hash_table_get_entry_count(&connection->thread_data.active_streams_map) == 0) {
2810+
/* Check the current state */
2811+
connection->thread_data.stats.was_inactive = true;
2812+
}
2813+
return;
2814+
}
2815+
2816+
static void s_gather_statistics(struct aws_channel_handler *handler, struct aws_array_list *stats) {
2817+
2818+
struct aws_h2_connection *connection = handler->impl;
2819+
AWS_PRECONDITION(aws_channel_thread_is_callers_thread(connection->base.channel_slot->channel));
2820+
2821+
/* TODO: Need update the way we calculate statistics, to account for user-controlled pauses.
2822+
* If user is adding chunks 1 by 1, there can naturally be a gap in the upload.
2823+
* If the user lets the stream-window go to zero, there can naturally be a gap in the download. */
2824+
uint64_t now_ns = 0;
2825+
if (aws_channel_current_clock_time(connection->base.channel_slot->channel, &now_ns)) {
2826+
return;
2827+
}
2828+
2829+
if (!aws_linked_list_empty(&connection->thread_data.outgoing_streams_list)) {
2830+
s_add_time_measurement_to_stats(
2831+
connection->thread_data.outgoing_timestamp_ns,
2832+
now_ns,
2833+
&connection->thread_data.stats.pending_outgoing_stream_ms);
2834+
2835+
connection->thread_data.outgoing_timestamp_ns = now_ns;
2836+
}
2837+
if (aws_hash_table_get_entry_count(&connection->thread_data.active_streams_map) != 0) {
2838+
s_add_time_measurement_to_stats(
2839+
connection->thread_data.incoming_timestamp_ns,
2840+
now_ns,
2841+
&connection->thread_data.stats.pending_incoming_stream_ms);
2842+
2843+
connection->thread_data.incoming_timestamp_ns = now_ns;
2844+
} else {
2845+
connection->thread_data.stats.was_inactive = true;
2846+
}
2847+
2848+
void *stats_base = &connection->thread_data.stats;
2849+
aws_array_list_push_back(stats, &stats_base);
2850+
}

source/statistics.c

+11
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,14 @@ void aws_crt_statistics_http1_channel_reset(struct aws_crt_statistics_http1_chan
2222
stats->current_outgoing_stream_id = 0;
2323
stats->current_incoming_stream_id = 0;
2424
}
25+
26+
void aws_crt_statistics_http2_channel_init(struct aws_crt_statistics_http2_channel *stats) {
27+
AWS_ZERO_STRUCT(*stats);
28+
stats->category = AWSCRT_STAT_CAT_HTTP2_CHANNEL;
29+
}
30+
31+
void aws_crt_statistics_http2_channel_reset(struct aws_crt_statistics_http2_channel *stats) {
32+
stats->pending_outgoing_stream_ms = 0;
33+
stats->pending_incoming_stream_ms = 0;
34+
stats->was_inactive = false;
35+
}

tests/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -616,14 +616,14 @@ add_net_test_case(h2_sm_mock_goaway)
616616
# Tests against real world server
617617
add_net_test_case(h2_sm_acquire_stream)
618618
add_net_test_case(h2_sm_acquire_stream_multiple_connections)
619-
add_net_test_case(h2_sm_acquire_stream_stress)
620619
add_net_test_case(h2_sm_closing_before_connection_acquired)
621620
# Tests against local server
622621
if (ENABLE_LOCALHOST_INTEGRATION_TESTS)
623622
# Tests should be named with localhost_integ_*
624623
add_net_test_case(localhost_integ_h2_sm_prior_knowledge)
625624
add_net_test_case(localhost_integ_h2_sm_acquire_stream_stress)
626625
add_net_test_case(localhost_integ_h2_sm_acquire_stream_stress_with_body)
626+
add_net_test_case(localhost_integ_h2_sm_connection_monitor_kill_slow_connection)
627627
endif()
628628

629629
add_test_case(random_access_set_sanitize_test)

0 commit comments

Comments
 (0)