Add broker and web throughput diagnostics

This commit is contained in:
2026-09-08 22:40:58 +02:00
parent 36d41be422
commit 042499e4d6
20 changed files with 899 additions and 5 deletions
+4
View File
@@ -186,6 +186,10 @@ static const char *const s_completion_candidates[] = {
"web diagnostics disable",
"web diagnostics show",
"web diagnostics clear",
"web performance enable",
"web performance disable",
"web performance show",
"web performance clear",
"web certificate",
"web certificate info",
"web certificate rotate",
+7
View File
@@ -149,6 +149,10 @@ static void fan_out_rx_locked(const uint8_t *data, size_t size)
slot->counters.uart_rx_bytes += size;
size_t queued = xStreamBufferSend(slot->output, data, size, 0);
size_t pending = xStreamBufferBytesAvailable(slot->output);
if (pending > slot->counters.output_high_water_bytes) {
slot->counters.output_high_water_bytes = pending;
}
size_t dropped = size - queued;
slot->counters.output_queued_bytes += queued;
slot->counters.output_dropped_bytes += dropped;
@@ -740,6 +744,8 @@ esp_err_t session_broker_clear_counters(void)
memset(&s_counters, 0, sizeof(s_counters));
for (size_t i = 0; i < SESSION_BROKER_MAX_CLIENTS; ++i) {
memset(&s_slots[i].counters, 0, sizeof(s_slots[i].counters));
s_slots[i].counters.output_high_water_bytes =
xStreamBufferBytesAvailable(s_slots[i].output);
}
xSemaphoreGive(s_mutex);
return ESP_OK;
@@ -758,6 +764,7 @@ esp_err_t session_broker_clear_client_counters(session_broker_client_id_t client
return ESP_ERR_NOT_FOUND;
}
memset(&slot->counters, 0, sizeof(slot->counters));
slot->counters.output_high_water_bytes = xStreamBufferBytesAvailable(slot->output);
xSemaphoreGive(s_mutex);
return ESP_OK;
}
+14 -2
View File
@@ -49,6 +49,11 @@ typedef struct {
session_broker_client_id_t writer_id;
} session_broker_event_t;
/*
* Active-client counters cover this connection (or the last counter clear).
* They become unavailable on disconnect and reset on slot/generation reuse;
* global totals retain disconnected clients' traffic until explicitly cleared.
*/
typedef struct {
/* UART bytes considered for delivery while this client was connected. */
uint64_t uart_rx_bytes;
@@ -67,13 +72,18 @@ typedef struct {
uint64_t events_queued;
uint64_t events_popped;
uint64_t event_drops;
/* Peak output occupancy, <= SESSION_BROKER_OUTPUT_SIZE; clear seeds pending. */
size_t output_high_water_bytes;
} session_broker_client_counters_t;
typedef struct {
uint64_t uart_rx_bytes;
/* UART RX drained when there were no connected observers. */
uint64_t unobserved_rx_bytes;
/* Queue/read/drop totals count one copy per client observer. */
/* Queue/read/drop totals count one copy per client observer.
* Drops include full-buffer losses and unread output discarded on disconnect.
* Read means handed to a transport, not confirmed delivery to its peer.
*/
uint64_t output_queued_bytes;
uint64_t output_read_bytes;
uint64_t output_dropped_bytes;
@@ -158,7 +168,9 @@ esp_err_t session_broker_get_global_snapshot(session_broker_global_snapshot_t *s
size_t session_broker_list_clients(session_broker_client_snapshot_t *clients,
size_t capacity);
/* Counter clearing does not reset client IDs, queued data, or event sequence. */
/* Counter clearing does not reset client IDs, queued data, or event sequence.
* Client output high-water marks restart at current queued occupancy, not zero.
*/
esp_err_t session_broker_clear_counters(void);
esp_err_t session_broker_clear_client_counters(session_broker_client_id_t client_id);
+20
View File
@@ -182,6 +182,26 @@ static int show_counters(void)
counter->events_queued,
counter->events_popped,
counter->event_drops);
session_broker_client_snapshot_t clients[SESSION_BROKER_MAX_CLIENTS];
size_t count = session_broker_list_clients(clients, SESSION_BROKER_MAX_CLIENTS);
printf("Active clients (since connect/clear; lost on disconnect; global totals retained):\n");
printf("Output bytes: HWM <= %u; clear seeds pending; read = handed to transport.\n",
(unsigned int)SESSION_BROKER_OUTPUT_SIZE);
printf("Global dropped also includes unread output discarded on disconnect.\n");
printf("ID type pending HWM UART queued read dropped\n");
for (size_t index = 0; index < count; ++index) {
const session_broker_client_snapshot_t *client = &clients[index];
printf("%-10lu %-9s %-7u %-7u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 "\n",
(unsigned long)client->id,
client_type_name(client->type),
(unsigned int)client->output_bytes_pending,
(unsigned int)client->counters.output_high_water_bytes,
client->counters.uart_rx_bytes,
client->counters.output_queued_bytes,
client->counters.output_read_bytes,
client->counters.output_dropped_bytes);
}
return 0;
}
+49
View File
@@ -24,6 +24,7 @@ static void print_usage(void)
printf(" web status|start|stop\n");
printf(" web counters|clear-counters\n");
printf(" web diagnostics enable|disable|show|clear\n");
printf(" web performance enable|disable|show|clear\n");
printf(" web certificate info\n");
printf(" web certificate rotate --force\n");
printf(" web reset --force (TLS certificate and private key only)\n");
@@ -295,12 +296,60 @@ static int reset_material(void)
return 0;
}
static void print_performance_time(const char *name, const web_serial_performance_timing_t *t)
{
printf(" %s: count=%" PRIu64 " sum_us=%" PRIu64 " avg_us_est=%" PRIu64 " max_us=%" PRIu64 "\n",
name, t->count, t->sum_us, t->count ? t->sum_us / t->count : 0, t->max_us);
}
static int performance_command(const char *action)
{
esp_err_t result = ESP_OK;
if (!strcmp(action, "enable")) result = web_serial_performance_enable(true);
else if (!strcmp(action, "disable")) result = web_serial_performance_enable(false);
else if (!strcmp(action, "clear")) result = web_serial_performance_clear();
else if (strcmp(action, "show")) return 1;
if (result != ESP_OK) {
printf("Web performance: %s\n", esp_err_to_name(result));
return 1;
}
web_serial_performance_snapshot_t s;
web_serial_performance_snapshot(&s);
printf("Web performance: enabled=%u epoch=%" PRIu32 " epoch_exhausted=%u; binary TX only\n",
s.enabled, s.epoch, s.epoch_exhausted);
printf("Send-call return is synchronous HTTPD-owner bytes send, not peer receipt. Timings are instrumented estimates; saturated=1 invalidates averages/count totals.\n");
printf("Completion->nonempty includes idle gaps; first-nonempty excludes observed empty attempts, not proof of backlog.\n");
for (unsigned i = 0; i < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++i) {
const web_serial_performance_session_t *r = &s.sessions[i];
if (!r->active) continue;
printf("slot=%u fd=%d generation=%" PRIu32 " broker=%" PRIu32
" pending=%u measured_pending=%u executing=%u pending_age_us=%" PRIu64 " saturated=%u\n",
i, r->socket_fd, r->generation, (uint32_t)r->broker_client_id,
r->pending, r->measured_pending, r->executing, r->pending_age_us, r->saturated);
printf(" queued_frames=%" PRIu64 " queued_bytes=%" PRIu64 " queue_errors=%" PRIu64
" sent_frames=%" PRIu64 " sent_bytes=%" PRIu64 " send_errors=%" PRIu64 " retired=%" PRIu64 "\n",
r->queued_frames, r->queued_bytes, r->queue_errors, r->sent_frames,
r->sent_bytes, r->send_errors, r->retired);
print_performance_time("queue->callback-entry", &r->queue_wait);
print_performance_time("send-call", &r->send_call);
print_performance_time("completion->first-drain-attempt-return", &r->completion_attempt);
print_performance_time("completion->next-nonempty-drain-return (includes idle)", &r->completion_nonempty);
print_performance_time("completion->first-attempt-nonempty-return", &r->completion_first_nonempty);
}
return 0;
}
static int command_web(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "help") == 0)) {
print_usage();
return 0;
}
if (argc == 3 && strcmp(argv[1], "performance") == 0) {
if (performance_command(argv[2]) == 0) return 0;
print_usage();
return 1;
}
if (argc == 3 && strcmp(argv[1], "diagnostics") == 0) {
if (web_diagnostics_command(argv[2]) == 0) return 0;
print_usage();
+148
View File
@@ -86,6 +86,10 @@ typedef struct web_serial_slot {
uint8_t rx_data[WEB_SERIAL_TRANSPORT_MAX_RX_PAYLOAD];
uint8_t tx_data[WEB_SERIAL_TRANSPORT_TX_PAYLOAD_SIZE];
web_serial_work_t work;
web_serial_performance_session_t performance;
uint32_t performance_epoch, completion_epoch;
int64_t queued_us, completed_us;
bool completion_waiting, completion_attempted;
} web_serial_slot_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
@@ -103,6 +107,88 @@ static uint32_t s_inflight_handlers;
/* Cancels ticket publication across revocation and server detach/re-attach.
* Never wraps: exhaustion disables minting for the remainder of the boot. */
static uint64_t s_ticket_epoch;
/* Atomic gate permits a true pre-lock callback-entry timestamp. Never reused. */
static uint32_t s_performance_gate, s_performance_epoch;
static uint32_t performance_gate(void)
{
return __atomic_load_n(&s_performance_gate, __ATOMIC_RELAXED);
}
static void performance_add(web_serial_slot_t *slot, uint64_t *value, uint64_t n)
{
if (UINT64_MAX - *value < n) {
*value = UINT64_MAX;
slot->performance.saturated = true;
} else *value += n;
}
static void performance_time(web_serial_slot_t *slot,
web_serial_performance_timing_t *value,
int64_t start, int64_t end)
{
uint64_t elapsed = end >= start ? (uint64_t)(end - start) : 0;
performance_add(slot, &value->count, 1);
performance_add(slot, &value->sum_us, elapsed);
if (elapsed > value->max_us) value->max_us = elapsed;
}
static esp_err_t performance_control(bool enabled, bool clear)
{
taskENTER_CRITICAL(&s_lock);
if (clear) enabled = performance_gate() != 0;
if (s_performance_epoch == UINT32_MAX) {
__atomic_store_n(&s_performance_gate, 0, __ATOMIC_RELAXED);
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
++s_performance_epoch;
__atomic_store_n(&s_performance_gate, enabled ? s_performance_epoch : 0,
__ATOMIC_RELAXED);
for (unsigned i = 0; i < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++i) {
s_slots[i].completion_waiting = false;
if (clear) memset(&s_slots[i].performance, 0, sizeof(s_slots[i].performance));
}
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
esp_err_t web_serial_performance_enable(bool enabled)
{
return performance_control(enabled, false);
}
esp_err_t web_serial_performance_clear(void)
{
return performance_control(false, true);
}
esp_err_t web_serial_performance_snapshot(web_serial_performance_snapshot_t *out)
{
if (!out) return ESP_ERR_INVALID_ARG;
taskENTER_CRITICAL(&s_lock);
uint32_t gate = performance_gate();
int64_t now = gate ? esp_timer_get_time() : 0;
*out = (web_serial_performance_snapshot_t){
.enabled = gate != 0, .epoch = s_performance_epoch,
.epoch_exhausted = s_performance_epoch == UINT32_MAX};
for (unsigned i = 0; i < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++i) {
web_serial_slot_t *slot = &s_slots[i];
web_serial_performance_session_t *row = &out->sessions[i];
*row = slot->performance;
row->active = slot->state == WEB_SERIAL_SLOT_ACTIVE;
row->socket_fd = slot->socket_fd;
row->generation = slot->generation;
row->broker_client_id = slot->broker_client_id;
row->pending = slot->work_pending;
row->measured_pending = row->pending && gate && slot->performance_epoch == gate;
row->executing = row->measured_pending && row->executing;
row->pending_age_us = row->measured_pending && now >= slot->queued_us ?
(uint64_t)(now - slot->queued_us) : 0;
}
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
static esp_err_t identity_is_current(const user_principal_t *principal,
web_session_id_t id, bool *current)
@@ -377,6 +463,9 @@ static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
if (slot->state != WEB_SERIAL_SLOT_FREE) {
continue;
}
memset(&slot->performance, 0, sizeof(slot->performance));
slot->performance_epoch = 0;
slot->completion_waiting = false;
slot->generation = next_generation(slot->generation);
slot->state = WEB_SERIAL_SLOT_RESERVED;
slot->server = server;
@@ -902,6 +991,8 @@ static void finish_closing_slot_locked(web_serial_slot_t *slot)
static void web_serial_send_work(void *argument)
{
uint32_t entry_epoch = performance_gate();
int64_t entry_us = entry_epoch ? esp_timer_get_time() : 0;
web_serial_work_t *work = argument;
web_serial_slot_t *slot = work != NULL ? work->slot : NULL;
if (!slot_pointer_valid(slot)) {
@@ -928,6 +1019,14 @@ static void web_serial_send_work(void *argument)
generation = work->generation;
type = slot->tx_type;
length = slot->tx_length;
uint32_t sample_epoch = owned_work && entry_epoch &&
entry_epoch == performance_gate() && slot->performance_epoch == entry_epoch &&
type == HTTPD_WS_TYPE_BINARY ? entry_epoch : 0;
if (sample_epoch) {
performance_time(slot, &slot->performance.queue_wait, slot->queued_us, entry_us);
slot->performance.executing = valid;
if (!valid) performance_add(slot, &slot->performance.retired, 1);
}
if (owned_work && !valid) {
slot->work_pending = false;
slot->tx_length = 0U;
@@ -943,6 +1042,8 @@ static void web_serial_send_work(void *argument)
}
esp_err_t result = ESP_FAIL;
int64_t send_start = 0, send_end = 0;
bool send_called = false;
void *current_context = httpd_sess_get_ctx(server, socket_fd);
if (current_context == slot &&
httpd_ws_get_fd_info(server, socket_fd) ==
@@ -954,7 +1055,10 @@ static void web_serial_send_work(void *argument)
.payload = slot->tx_data,
.len = length,
};
send_called = true;
if (sample_epoch && performance_gate() == sample_epoch) send_start = esp_timer_get_time();
result = httpd_ws_send_frame_async(server, socket_fd, &frame);
if (sample_epoch && performance_gate() == sample_epoch) send_end = esp_timer_get_time();
if (result == ESP_OK) {
(void)httpd_sess_update_lru_counter(server, socket_fd);
}
@@ -963,6 +1067,19 @@ static void web_serial_send_work(void *argument)
taskENTER_CRITICAL(&s_lock);
if (slot->work_pending && slot->generation == generation &&
work == &slot->work) {
if (sample_epoch && performance_gate() == sample_epoch &&
slot->performance_epoch == sample_epoch) {
slot->performance.executing = false;
if (send_called) performance_time(slot, &slot->performance.send_call, send_start, send_end);
if (result == ESP_OK) {
performance_add(slot, &slot->performance.sent_frames, 1);
performance_add(slot, &slot->performance.sent_bytes, length);
slot->completion_epoch = sample_epoch;
slot->completed_us = send_end;
slot->completion_waiting = true;
slot->completion_attempted = false;
} else performance_add(slot, &slot->performance.send_errors, 1);
}
slot->work_pending = false;
slot->tx_length = 0U;
if (result == ESP_OK) {
@@ -986,6 +1103,8 @@ static void web_serial_send_work(void *argument)
static esp_err_t queue_slot_frame(web_serial_slot_t *slot, uint32_t generation,
httpd_ws_type_t type, size_t length)
{
uint32_t epoch = performance_gate();
int64_t queued_us = epoch ? esp_timer_get_time() : 0;
httpd_handle_t server;
bool prepared = false;
@@ -994,6 +1113,13 @@ static esp_err_t queue_slot_frame(web_serial_slot_t *slot, uint32_t generation,
slot->generation == generation && !slot->work_pending &&
!slot->close_requested && slot->server == s_server &&
length <= sizeof(slot->tx_data)) {
slot->performance_epoch = type == HTTPD_WS_TYPE_BINARY && epoch == performance_gate() ? epoch : 0;
if (slot->performance_epoch) {
slot->queued_us = queued_us;
slot->performance.executing = false;
performance_add(slot, &slot->performance.queued_frames, 1);
performance_add(slot, &slot->performance.queued_bytes, length);
}
slot->tx_type = type;
slot->tx_length = length;
slot->work.slot = slot;
@@ -1025,6 +1151,8 @@ static esp_err_t queue_slot_frame(web_serial_slot_t *slot, uint32_t generation,
slot->tx_length = 0U;
slot->close_requested = true;
++s_counters.queue_failures;
if (epoch && performance_gate() == epoch && slot->performance_epoch == epoch)
performance_add(slot, &slot->performance.queue_errors, 1);
}
taskEXIT_CRITICAL(&s_lock);
notify_transport_task();
@@ -1168,9 +1296,29 @@ static bool prepare_writer_sync(web_serial_slot_t *slot, uint32_t generation,
static void drain_binary_output(web_serial_slot_t *slot, uint32_t generation,
session_broker_client_id_t client_id)
{
uint32_t epoch = performance_gate();
size_t received = 0U;
esp_err_t result = session_broker_read(client_id, slot->tx_data,
sizeof(slot->tx_data), &received);
int64_t drained_us = epoch && performance_gate() == epoch ? esp_timer_get_time() : 0;
if (epoch) {
taskENTER_CRITICAL(&s_lock);
if (performance_gate() == epoch && slot->completion_epoch == epoch &&
slot->generation == generation && slot->broker_client_id == client_id &&
slot->state == WEB_SERIAL_SLOT_ACTIVE && slot->completion_waiting) {
bool nonempty = result == ESP_OK && received > 0;
if (!slot->completion_attempted) {
performance_time(slot, &slot->performance.completion_attempt, slot->completed_us, drained_us);
if (nonempty) performance_time(slot, &slot->performance.completion_first_nonempty, slot->completed_us, drained_us);
}
slot->completion_attempted = true;
if (nonempty) {
performance_time(slot, &slot->performance.completion_nonempty, slot->completed_us, drained_us);
slot->completion_waiting = false;
}
}
taskEXIT_CRITICAL(&s_lock);
}
if (result == ESP_OK && received > 0U) {
(void)queue_slot_frame(slot, generation, HTTPD_WS_TYPE_BINARY, received);
} else if (result == ESP_ERR_NOT_FOUND) {
+38
View File
@@ -135,6 +135,44 @@ esp_err_t web_serial_transport_revoke_web_session(web_session_id_t id);
esp_err_t web_serial_transport_get_snapshot(
web_serial_transport_snapshot_t *snapshot);
typedef struct {
uint64_t count, sum_us, max_us;
} web_serial_performance_timing_t;
typedef struct {
bool active, pending, measured_pending, executing, saturated;
int socket_fd;
uint32_t generation;
session_broker_client_id_t broker_client_id;
uint64_t pending_age_us;
uint64_t queued_frames, queued_bytes, queue_errors;
uint64_t sent_frames, sent_bytes, send_errors, retired;
web_serial_performance_timing_t queue_wait, send_call;
web_serial_performance_timing_t completion_attempt, completion_nonempty;
web_serial_performance_timing_t completion_first_nonempty;
} web_serial_performance_session_t;
typedef struct {
bool enabled, epoch_exhausted;
uint32_t epoch;
web_serial_performance_session_t sessions[WEB_SERIAL_TRANSPORT_MAX_SESSIONS];
} web_serial_performance_snapshot_t;
/* Binary TX only; no broker/HTTPD queries, heap scans, or sensitive identities.
* queued_* counts transport reservations, including reported queue failures;
* sent_* counts successful send-call returns. send_errors includes owner-context
* rejection; send_call timings count only actual API calls. queue_wait includes
* owned callbacks retired without sending. Pending age is since reservation-path
* entry, including an executing send; unavailable epochs are explicitly marked.
* All timing endpoints are local monotonic estimates, never peer acknowledgments.
* Disable freezes aggregates; enable resumes them. Every toggle/clear fences
* in-flight samples. Clear preserves enabled state. Epoch exhaustion fails closed.
* Nonempty completion intervals include idle; first_nonempty requires the first
* subsequent read to return data (not proof of backlog at send completion). */
esp_err_t web_serial_performance_enable(bool enabled);
esp_err_t web_serial_performance_clear(void);
esp_err_t web_serial_performance_snapshot(web_serial_performance_snapshot_t *out);
/* Clearing counters does not alter tickets, sessions, ownership, or queued data. */
esp_err_t web_serial_transport_clear_counters(void);