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
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Compile actual transport callbacks using the existing serial/store doubles."""
import os
import pathlib
import subprocess
import sys
import tempfile
sys.dont_write_bytecode = True
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parents[1]
BASE = ROOT / 'tests/web_session_store'
sys.path.insert(0, str(BASE))
from run import HEADERS
from serial_headers import SERIAL_HEADERS
os.environ['CCACHE_DISABLE'] = '1'
with tempfile.TemporaryDirectory(prefix='web-performance-') as directory:
tmp = pathlib.Path(directory)
headers = HEADERS | SERIAL_HEADERS
headers['freertos/FreeRTOS.h'] = headers['freertos/FreeRTOS.h'].replace(
'extern int host_lock_depth;', 'extern int host_lock_depth; void host_before_lock(void);').replace(
'(void)(m); assert(host_lock_depth++', '(void)(m); host_before_lock(); assert(host_lock_depth++')
for name, text in headers.items():
path = tmp / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
fixture = (BASE / 'serial_test.c').read_text().replace('int main(void)', 'int serial_tests(void)')
fixture = fixture.replace('#include "test.c"', '#include "' + str(BASE / 'test.c') + '"')
fixture = fixture.replace('../../src/web_serial_transport.c', str(ROOT / 'src/web_serial_transport.c'))
(tmp / 'fixture.c').write_text(fixture)
console = (ROOT / 'src/web_console.c').read_text()
start = console.index('static void print_performance_time(')
end = console.index('static int command_web(', start)
(tmp / 'console.inc').write_text(
'static const char *esp_err_to_name(esp_err_t e) { (void)e; return "error"; }\n'
+ console[start:end])
flags = ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] if '--sanitize' in sys.argv else []
subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', '-g', *flags,
'-I'+str(tmp), '-I'+str(ROOT / 'src'), '-ffunction-sections', '-fdata-sections',
'-Wl,--gc-sections', str(HERE / 'test.c'), str(ROOT / 'src/web_session_store.c'),
str(ROOT / 'src/web_auth_parse.c'), '-o', str(tmp / 'test')], check=True, timeout=30)
subprocess.run([str(tmp / 'test')], check=True, timeout=15)
+172
View File
@@ -0,0 +1,172 @@
/* Production queue/callback/drain paths; no payload is printed. */
#include "fixture.c"
#include "console.inc"
static void (*queued)(void *), (*send_hook)(void), (*queue_hook)(void), (*read_hook)(void);
static void *queued_arg;
static bool early;
static esp_err_t queue_result, send_result;
static size_t read_bytes;
static unsigned sends;
static unsigned lock_delay;
static bool disconnect_stalled;
void host_before_lock(void) { now += lock_delay; lock_delay = 0; }
void vTaskDelay(TickType_t ticks)
{
assert(!host_lock_depth); now += ticks * 1000;
if (!disconnect_stalled) process_broker_disconnect(&s_slots[0]);
}
session_broker_client_id_t session_broker_get_writer_id(void) { return SESSION_BROKER_NO_CLIENT; }
esp_err_t session_broker_force_writer(session_broker_client_id_t id) { (void)id; return ESP_OK; }
static void detach(void) { assert(web_serial_transport_detach_server(request.handle) == ESP_OK); }
static void reuse_during_read(void) { ++s_slots[0].generation; }
esp_err_t httpd_queue_work(httpd_handle_t h, void (*fn)(void *), void *arg)
{
(void)h; assert(!host_lock_depth);
queued = fn; queued_arg = arg;
if (queue_hook) queue_hook();
if (early && queue_result == ESP_OK) { now += 7; fn(arg); }
return queue_result;
}
esp_err_t httpd_ws_send_frame_async(httpd_handle_t h, int fd, httpd_ws_frame_t *f)
{
(void)h; (void)fd; assert(!host_lock_depth); assert(f->payload == s_slots[0].tx_data);
++sends; now += 11;
if (send_hook) send_hook();
return send_result;
}
esp_err_t httpd_sess_update_lru_counter(httpd_handle_t h, int fd)
{ (void)h; (void)fd; assert(!host_lock_depth); now += 3; return ESP_OK; }
esp_err_t session_broker_read(session_broker_client_id_t id, uint8_t *data, size_t n, size_t *out)
{
(void)id; (void)data; assert(!host_lock_depth && read_bytes <= n);
if (read_hook) read_hook();
*out = read_bytes; return ESP_OK;
}
static web_serial_slot_t *setup(void)
{
serial_reset(); issued_t a = mint(&alice); web_serial_slot_t *s = connect_session(&a);
early = false; disconnect_stalled = false; lock_delay = 0; queue_result = send_result = ESP_OK;
send_hook = queue_hook = read_hook = NULL; read_bytes = 0; sends = 0;
assert(web_serial_performance_enable(false) == ESP_OK);
assert(web_serial_performance_clear() == ESP_OK);
return s;
}
static void enable(void) { assert(web_serial_performance_enable(true) == ESP_OK); }
static void clear(void) { assert(web_serial_performance_clear() == ESP_OK); }
static void disable(void) { assert(web_serial_performance_enable(false) == ESP_OK); }
static void cycle(void) { disable(); enable(); }
static void run_work(void) { assert(queued); queued(queued_arg); }
static void queue_binary(web_serial_slot_t *s)
{ assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_BINARY, 512) == ESP_OK); }
static web_serial_performance_session_t row(void)
{
web_serial_performance_snapshot_t s;
assert(web_serial_performance_snapshot(&s) == ESP_OK);
return s.sessions[0];
}
int main(void)
{
assert(serial_tests() == 0);
web_serial_slot_t *s = setup();
queue_binary(s); run_work(); assert(row().sent_frames == 0); /* default off */
enable(); early = true; queue_binary(s);
assert(row().queue_wait.sum_us == 7 && row().send_call.sum_us == 11);
assert(row().sent_frames == 1 && row().sent_bytes == 512 && !row().pending);
now += 20; read_bytes = 512; early = false;
drain_binary_output(s, s->generation, s->broker_client_id);
assert(row().completion_first_nonempty.count == 1 && row().completion_attempt.sum_us == 23);
now += 100; assert(row().pending_age_us == 100 && row().measured_pending);
run_work(); now += 10; read_bytes = 0;
drain_binary_output(s, s->generation, s->broker_client_id);
uint64_t first = row().completion_attempt.sum_us;
now += 1000000; read_bytes = 512;
drain_binary_output(s, s->generation, s->broker_client_id);
assert(row().completion_attempt.sum_us == first);
assert(row().completion_first_nonempty.count == 1 && row().completion_nonempty.max_us > 1000000);
run_work();
/* Control sends must not contribute or erase the binary completion chain. */
assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_TEXT, 4) == ESP_OK); run_work();
drain_binary_output(s, s->generation, s->broker_client_id);
assert(row().completion_nonempty.count == 3);
s = setup(); enable(); queue_binary(s); now += 5; lock_delay = 100;
run_work(); assert(row().queue_wait.sum_us == 5); /* before validation lock */
s = setup(); enable(); queue_result = ESP_FAIL;
assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_BINARY, 8) == ESP_FAIL);
assert(row().queue_errors == 1 && !row().pending && s->close_requested);
s = setup(); enable(); send_result = ESP_FAIL; queue_binary(s); run_work();
assert(row().send_errors == 1 && row().send_call.count == 1 && row().sent_frames == 0);
s = setup(); enable(); queue_binary(s); s->close_requested = true; run_work();
assert(!sends && row().retired == 1 && !s->work_pending);
void (*hooks[])(void) = {clear, disable, cycle};
for (unsigned i = 0; i < 3; ++i) {
s = setup(); enable(); queue_binary(s); hooks[i](); run_work();
assert(row().sent_frames == 0 && row().queue_wait.count == 0);
s = setup(); enable(); send_hook = hooks[i]; queue_binary(s); run_work();
assert(row().sent_frames == 0 && row().send_call.count == 0);
s = setup(); enable(); queue_hook = hooks[i]; early = true; queue_binary(s);
assert(row().sent_frames == 0 && row().queue_wait.count == 0);
s = setup(); enable(); queue_hook = hooks[i]; queue_result = ESP_FAIL;
assert(queue_slot_frame(s, s->generation, HTTPD_WS_TYPE_BINARY, 8) == ESP_FAIL);
assert(row().queue_errors == 0 && s->close_requested);
}
s = setup(); enable(); queue_binary(s); run_work(); disable();
uint64_t frozen = row().sent_frames; queue_binary(s); run_work();
assert(row().active && row().sent_frames == frozen && !row().measured_pending);
enable(); queue_binary(s); run_work(); assert(row().sent_frames == frozen + 1);
read_hook = clear; read_bytes = 512;
drain_binary_output(s, s->generation, s->broker_client_id);
assert(row().completion_nonempty.count == 0);
s = setup(); enable(); queue_binary(s); run_work();
read_hook = reuse_during_read; read_bytes = 512;
drain_binary_output(s, s->generation, s->broker_client_id);
assert(row().completion_nonempty.count == 0 && !s->work_pending);
s = setup(); enable(); queue_binary(s); detach(); run_work();
assert(!sends && row().sent_frames == 0);
assert(web_serial_transport_attach_server(request.handle) == ESP_OK);
s = setup(); enable(); send_hook = detach; queue_binary(s); run_work();
assert(sends == 1 && row().sent_frames == 0 && !s->work_pending);
s = setup(); enable(); queue_binary(s); disconnect_stalled = true;
assert(web_serial_transport_detach_server(request.handle) == ESP_ERR_TIMEOUT);
run_work(); assert(!sends && row().sent_frames == 0);
s = setup(); enable(); queue_binary(s); request.sess_ctx = &s_slots[1];
run_work(); assert(!sends && row().send_errors == 1 && !row().send_call.count);
/* A copied/stale callback argument cannot own a reused slot. */
s = setup(); enable(); queue_binary(s); web_serial_work_t stale = s->work;
web_serial_send_work(&stale); assert(!sends && s->work_pending);
++s->generation; run_work(); assert(!sends && row().sent_frames == 0);
s = setup(); enable(); queue_binary(s); web_serial_session_free(s);
run_work(); assert(!sends && !s->work_pending); process_broker_disconnect(s);
assert(s->state == WEB_SERIAL_SLOT_FREE);
uint32_t generation; s = reserve_slot(request.handle, 10, &generation);
assert(s && !s->performance.sent_frames && !s->performance_epoch);
s = setup(); enable(); s->performance.sent_bytes = UINT64_MAX - 1;
queue_binary(s); run_work(); assert(row().sent_bytes == UINT64_MAX && row().saturated);
clear(); assert(!row().saturated);
s->performance.queue_wait.count = UINT64_MAX;
s->performance.queue_wait.sum_us = UINT64_MAX - 1;
queue_binary(s); now += 9; run_work();
assert(row().queue_wait.count == UINT64_MAX && row().queue_wait.sum_us == UINT64_MAX);
assert(row().queue_wait.max_us == 9 && row().saturated);
clear();
assert(performance_command("show") == 0);
assert(performance_command("disable") == 0 && !performance_gate());
assert(performance_command("enable") == 0 && performance_gate());
assert(performance_command("clear") == 0);
assert(performance_command("bogus") == 1);
s_performance_epoch = UINT32_MAX - 1; enable();
assert(web_serial_performance_clear() == ESP_ERR_INVALID_STATE && !performance_gate());
assert(!host_lock_depth);
printf("PASS: performance production queue/send/drain, epochs, idle, retirement, saturation\n");
printf("host slot diagnostics storage=%zu bytes (two slots); target ABI may differ\n",
2 * (sizeof(s->performance) + sizeof(s->performance_epoch) + sizeof(s->completion_epoch)
+ sizeof(s->queued_us) + sizeof(s->completed_us) + sizeof(s->completion_waiting)
+ sizeof(s->completion_attempted)));
return 0;
}