Add broker and web throughput diagnostics
This commit is contained in:
@@ -44,6 +44,7 @@ static void vTaskDelay(unsigned delay) { assert(delay==100); ++waits; }
|
||||
#define pdMS_TO_TICKS(ms) (ms)
|
||||
static void print_usage(void) { ++usages; }
|
||||
static int web_diagnostics_command(const char *action) { assert(!strcmp(action, "show")); return 0; }
|
||||
static int performance_command(const char *action) { return strcmp(action, "show") ? 1 : 0; }
|
||||
static int show_status(void) { assert(false); return 1; }
|
||||
static int show_counters(void) { assert(false); return 1; }
|
||||
static int show_certificate(void) { assert(false); return 1; }
|
||||
@@ -69,6 +70,11 @@ int main(void) {
|
||||
remote=web=false;
|
||||
char *diagnostics[]={"web", "diagnostics", "show"};
|
||||
assert(command_web(3, diagnostics)==0 && !stops && !scheduled);
|
||||
char *performance[]={"web", "performance", "show", "extra"};
|
||||
assert(command_web(3, performance)==0 && !stops && !scheduled);
|
||||
assert(command_web(4, performance)==1 && !stops && !scheduled);
|
||||
performance[2]="invalid";
|
||||
assert(command_web(3, performance)==1 && !stops && !scheduled);
|
||||
char *stop[]={"web", "stop"};
|
||||
remote=web=true;
|
||||
assert(command_web(2,stop)==0 && scheduled==1 && !stops && last_action==ADMIN_CONSOLE_DEFER_WEB_STOP);
|
||||
|
||||
@@ -47,6 +47,8 @@ int main(void) {
|
||||
{"", true}, {" ", true}, {" ", true},
|
||||
{"memory", true}, {"user", true}, {"user list", true},
|
||||
{"user show bootstrap", true}, {"exit", true},
|
||||
{"web performance enable", true}, {"web performance disable", true},
|
||||
{"web performance show", true}, {"web performance clear", true},
|
||||
/* Removed verbs reach the canonical handler, not a bootstrap policy. */
|
||||
{"user bootstrap", true}, {"user bootstrap extra", true},
|
||||
{"user recover", false}, {"user recover --force", false},
|
||||
@@ -75,6 +77,7 @@ int main(void) {
|
||||
const char *web_denied[] = {
|
||||
"web", "web help", "web start", "web stop extra", "web counters", "web clear-counters",
|
||||
"web diagnostics enable", "web diagnostics disable", "web diagnostics show", "web diagnostics clear",
|
||||
"web performance enable", "web performance disable", "web performance show", "web performance clear",
|
||||
"web credentials show", "web credentials rotate --force", "web certificate info",
|
||||
"web certificate rotate", "web certificate rotate --force extra",
|
||||
"web certificate rotate --force --force", "web certificate rotate --Force",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
|
||||
typedef int esp_err_t;
|
||||
#define ESP_OK 0
|
||||
#define ESP_ERR_INVALID_STATE 1
|
||||
#define ESP_ERR_INVALID_ARG 2
|
||||
#define ESP_ERR_NO_MEM 3
|
||||
#define ESP_ERR_NOT_FOUND 4
|
||||
#define ESP_ERR_TIMEOUT 5
|
||||
static const char *esp_err_to_name(esp_err_t e) { (void)e; return "fake-error"; }
|
||||
#define MALLOC_CAP_SPIRAM 1
|
||||
#define MALLOC_CAP_8BIT 2
|
||||
#define MALLOC_CAP_INTERNAL 4
|
||||
static size_t allocations;
|
||||
static void *heap_caps_calloc_prefer(size_t n, size_t s, int choices, ...) {
|
||||
(void)choices; ++allocations; return calloc(n, s);
|
||||
}
|
||||
#define heap_caps_free free
|
||||
|
||||
typedef unsigned TickType_t;
|
||||
typedef unsigned UBaseType_t;
|
||||
#define pdTRUE 1
|
||||
#define pdPASS 1
|
||||
#define portMAX_DELAY UINT32_MAX
|
||||
#define pdMS_TO_TICKS(ms) (ms)
|
||||
static int mutex;
|
||||
typedef int *SemaphoreHandle_t;
|
||||
static SemaphoreHandle_t xSemaphoreCreateMutex(void) { return &mutex; }
|
||||
static int xSemaphoreTake(SemaphoreHandle_t m, TickType_t ticks) {
|
||||
if (*m) { assert(ticks == 0); return 0; } *m = 1; return pdTRUE;
|
||||
}
|
||||
static void xSemaphoreGive(SemaphoreHandle_t m) { assert(*m); *m = 0; }
|
||||
static void vSemaphoreDelete(SemaphoreHandle_t m) { assert(!*m); }
|
||||
|
||||
typedef struct { uint8_t *data; size_t capacity, used; } StaticStreamBuffer_t;
|
||||
typedef StaticStreamBuffer_t *StreamBufferHandle_t;
|
||||
static StreamBufferHandle_t xStreamBufferCreateStatic(size_t size, size_t trigger,
|
||||
uint8_t *data, StaticStreamBuffer_t *s) {
|
||||
assert(trigger == 1); *s = (StaticStreamBuffer_t){data, size - 1, 0}; return s;
|
||||
}
|
||||
static size_t xStreamBufferBytesAvailable(StreamBufferHandle_t s) {
|
||||
assert(mutex); return s->used;
|
||||
}
|
||||
static size_t xStreamBufferSend(StreamBufferHandle_t s, const void *data, size_t size, TickType_t ticks) {
|
||||
assert(mutex && ticks == 0);
|
||||
if (size > s->capacity - s->used) size = s->capacity - s->used;
|
||||
memcpy(s->data + s->used, data, size); s->used += size; return size;
|
||||
}
|
||||
static size_t xStreamBufferReceive(StreamBufferHandle_t s, void *data, size_t size, TickType_t ticks) {
|
||||
assert(mutex && ticks == 0);
|
||||
if (size > s->used) size = s->used;
|
||||
memcpy(data, s->data, size); s->used -= size;
|
||||
memmove(s->data, s->data + size, s->used); return size;
|
||||
}
|
||||
static void xStreamBufferReset(StreamBufferHandle_t s) { assert(mutex); s->used = 0; }
|
||||
static void vStreamBufferDelete(StreamBufferHandle_t s) { (void)s; }
|
||||
|
||||
typedef struct { uint8_t *data; size_t capacity, item_size, used; } StaticQueue_t;
|
||||
typedef StaticQueue_t *QueueHandle_t;
|
||||
static QueueHandle_t xQueueCreateStatic(size_t capacity, size_t size, uint8_t *data, StaticQueue_t *q) {
|
||||
*q = (StaticQueue_t){data, capacity, size, 0}; return q;
|
||||
}
|
||||
static int xQueueSend(QueueHandle_t q, const void *item, TickType_t ticks) {
|
||||
assert(mutex && ticks == 0); if (q->used == q->capacity) return 0;
|
||||
memcpy(q->data + q->used++ * q->item_size, item, q->item_size); return pdTRUE;
|
||||
}
|
||||
static int xQueueReceive(QueueHandle_t q, void *item, TickType_t ticks) {
|
||||
assert(mutex && ticks == 0); if (!q->used) return 0;
|
||||
memcpy(item, q->data, q->item_size); --q->used;
|
||||
memmove(q->data, q->data + q->item_size, q->used * q->item_size); return pdTRUE;
|
||||
}
|
||||
static UBaseType_t uxQueueMessagesWaiting(QueueHandle_t q) { assert(mutex); return q->used; }
|
||||
static void xQueueReset(QueueHandle_t q) { assert(mutex); q->used = 0; }
|
||||
static void vQueueDelete(QueueHandle_t q) { (void)q; }
|
||||
|
||||
typedef void *TaskHandle_t;
|
||||
static void (*task_entry)(void *);
|
||||
static jmp_buf task_exit;
|
||||
static const uint8_t *serial_input;
|
||||
static size_t serial_remaining;
|
||||
static int xTaskCreate(void (*entry)(void *), const char *name, unsigned stack,
|
||||
void *context, unsigned priority, TaskHandle_t *handle) {
|
||||
(void)name; (void)stack; (void)context; (void)priority;
|
||||
task_entry = entry; *handle = &mutex; return pdPASS;
|
||||
}
|
||||
static void vTaskDelay(TickType_t ticks) {
|
||||
assert(!mutex && ticks > 0); if (!serial_remaining) longjmp(task_exit, 1);
|
||||
}
|
||||
static size_t serial_service_read(uint8_t *data, size_t size) {
|
||||
assert(mutex); if (size > serial_remaining) size = serial_remaining;
|
||||
memcpy(data, serial_input, size); serial_input += size; serial_remaining -= size; return size;
|
||||
}
|
||||
static size_t serial_service_write(const uint8_t *data, size_t size) { (void)data; assert(mutex); return size; }
|
||||
static esp_err_t serial_service_set_session_active(bool active) { (void)active; assert(mutex); return ESP_OK; }
|
||||
|
||||
typedef struct {
|
||||
const char *command, *help, *hint;
|
||||
int (*func)(int, char **);
|
||||
void *argtable;
|
||||
} esp_console_cmd_t;
|
||||
static int (*registered_command)(int, char **);
|
||||
static esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd) {
|
||||
registered_command = cmd->func; return ESP_OK;
|
||||
}
|
||||
static char console_output[16384];
|
||||
static size_t console_used;
|
||||
static int capture_printf(const char *format, ...) {
|
||||
/* Console formatting must happen after releasing the broker mutex. */
|
||||
assert(!mutex);
|
||||
va_list args; va_start(args, format);
|
||||
int n = vsnprintf(console_output + console_used, sizeof(console_output) - console_used, format, args);
|
||||
va_end(args); assert(n >= 0 && (size_t)n < sizeof(console_output) - console_used);
|
||||
console_used += (size_t)n; return n;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile unchanged production broker/console with isolated deterministic doubles.
|
||||
No SDK, device, network, or persistent generated files required.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
with tempfile.TemporaryDirectory(prefix="broker-diagnostics-") as directory:
|
||||
build = Path(directory)
|
||||
(build / "freertos").mkdir()
|
||||
for name in ("session_broker.c", "session_broker.h", "session_console.c", "session_console.h"):
|
||||
shutil.copyfile(ROOT / "src" / name, build / name)
|
||||
shutil.copyfile(HERE / "fake.h", build / "fake.h")
|
||||
shutil.copyfile(HERE / "test.c", build / "test.c")
|
||||
for name in ("esp_err.h", "esp_heap_caps.h", "esp_console.h", "serial_service.h",
|
||||
"freertos/FreeRTOS.h", "freertos/queue.h", "freertos/semphr.h",
|
||||
"freertos/stream_buffer.h", "freertos/task.h"):
|
||||
(build / name).write_text('#include "fake.h"\n')
|
||||
subprocess.run(["cc", "-std=c11", "-D_POSIX_C_SOURCE=200809L", "-Wall", "-Wextra",
|
||||
"-Werror", "-I", str(build), str(build / "test.c"),
|
||||
"-o", str(build / "test")], check=True)
|
||||
subprocess.run([str(build / "test")], check=True)
|
||||
@@ -0,0 +1,155 @@
|
||||
#include "fake.h"
|
||||
#include "session_broker.c"
|
||||
#define printf capture_printf
|
||||
#include "session_console.c"
|
||||
#undef printf
|
||||
|
||||
static uint8_t payload[SESSION_BROKER_OUTPUT_SIZE + 256];
|
||||
static void feed(size_t size) {
|
||||
assert(size <= sizeof(payload));
|
||||
serial_input = payload; serial_remaining = size;
|
||||
if (setjmp(task_exit) == 0) task_entry(NULL);
|
||||
assert(!mutex && serial_remaining == 0);
|
||||
}
|
||||
static session_broker_client_snapshot_t snapshot(session_broker_client_id_t id) {
|
||||
session_broker_client_snapshot_t s;
|
||||
assert(session_broker_get_client_snapshot(id, &s) == ESP_OK); return s;
|
||||
}
|
||||
static session_broker_global_snapshot_t global(void) {
|
||||
session_broker_global_snapshot_t s;
|
||||
assert(session_broker_get_global_snapshot(&s) == ESP_OK); return s;
|
||||
}
|
||||
static size_t drain(session_broker_client_id_t id, size_t size) {
|
||||
uint8_t data[sizeof(payload)]; size_t received;
|
||||
assert(size <= sizeof(data));
|
||||
assert(session_broker_read(id, data, size, &received) == ESP_OK); return received;
|
||||
}
|
||||
static session_broker_client_id_t connect_type(session_broker_client_type_t type) {
|
||||
session_broker_client_id_t id;
|
||||
assert(session_broker_connect(type, "SECRET-NAME-\033[2J", &id) == ESP_OK); return id;
|
||||
}
|
||||
static int command(const char *operation, session_broker_client_id_t id, const char *size) {
|
||||
char id_text[16]; snprintf(id_text, sizeof(id_text), "%u", id);
|
||||
char *argv[] = {"broker", (char *)operation, id_text, (char *)size};
|
||||
console_used = 0; console_output[0] = 0;
|
||||
return registered_command(id ? (size ? 4 : 3) : 2, argv);
|
||||
}
|
||||
static void disconnect_all(void) {
|
||||
session_broker_client_snapshot_t clients[SESSION_BROKER_MAX_CLIENTS];
|
||||
size_t n = session_broker_list_clients(clients, SESSION_BROKER_MAX_CLIENTS);
|
||||
for (size_t i = 0; i < n; ++i) assert(session_broker_disconnect(clients[i].id) == ESP_OK);
|
||||
}
|
||||
static void comparison(unsigned browsers) {
|
||||
session_broker_client_id_t ids[4];
|
||||
ids[0] = connect_type(SESSION_BROKER_CLIENT_USB);
|
||||
ids[1] = connect_type(SESSION_BROKER_CLIENT_SSH);
|
||||
for (unsigned i = 0; i < browsers; ++i) ids[2+i] = connect_type(SESSION_BROKER_CLIENT_WEB);
|
||||
assert(session_broker_clear_counters() == ESP_OK);
|
||||
size_t total = 0;
|
||||
while (total < 71292) {
|
||||
size_t n = 256;
|
||||
if (n > 71292 - total) n = 71292 - total;
|
||||
/* Deterministic slow-browser window, not a claim about real scheduling. */
|
||||
if (browsers == 2 && total < 23412 && n > 23412 - total) n = 23412 - total;
|
||||
feed(n); total += n;
|
||||
for (unsigned i = 0; i < 2+browsers; ++i) {
|
||||
if (browsers == 2 && i == 3 && total < 23412) continue;
|
||||
drain(ids[i], sizeof(payload));
|
||||
}
|
||||
}
|
||||
session_broker_global_snapshot_t g = global();
|
||||
uint64_t expected = 71292U * (2+browsers);
|
||||
uint64_t drops = browsers == 2 ? 19316 : 0;
|
||||
assert(g.counters.uart_rx_bytes == 71292 && g.counters.disconnections == 0);
|
||||
assert(g.counters.output_queued_bytes == expected-drops);
|
||||
assert(g.counters.output_read_bytes == expected-drops && g.counters.output_dropped_bytes == drops);
|
||||
for (unsigned i = 0; i < 2+browsers; ++i) {
|
||||
session_broker_client_snapshot_t s = snapshot(ids[i]);
|
||||
assert(s.counters.output_dropped_bytes == (i == 3 ? drops : 0));
|
||||
assert(s.counters.output_high_water_bytes == (i == 3 ? 4096 : 256));
|
||||
}
|
||||
printf("PASS synthetic %u-browser comparison: UART=71292 copies=%" PRIu64 " queued=%" PRIu64 " read=%" PRIu64 " dropped=%" PRIu64 " disconnect=0\n",
|
||||
browsers, expected, g.counters.output_queued_bytes, g.counters.output_read_bytes, drops);
|
||||
disconnect_all();
|
||||
}
|
||||
int main(void) {
|
||||
for (size_t i = 0; i < sizeof(payload); ++i) payload[i] = (uint8_t)i;
|
||||
assert(session_broker_get_global_snapshot(&(session_broker_global_snapshot_t){0}) == ESP_ERR_INVALID_STATE);
|
||||
assert(session_broker_init() == ESP_OK);
|
||||
assert(session_console_register_commands() == ESP_OK);
|
||||
size_t initial_allocations = allocations;
|
||||
session_broker_client_id_t fast = connect_type(SESSION_BROKER_CLIENT_USB);
|
||||
session_broker_client_id_t slow = connect_type(SESSION_BROKER_CLIENT_WEB);
|
||||
assert(session_broker_request_writer(fast) == ESP_OK);
|
||||
for (unsigned i = 0; i < 17; ++i) { feed(256); assert(drain(fast, 256) == 256); }
|
||||
session_broker_client_snapshot_t s = snapshot(slow);
|
||||
assert(s.output_bytes_pending == 4096 && s.counters.output_high_water_bytes == 4096);
|
||||
assert(s.counters.output_queued_bytes == 4096 && s.counters.output_dropped_bytes == 256);
|
||||
s = snapshot(fast);
|
||||
assert(s.counters.output_read_bytes == 4352 && s.counters.output_high_water_bytes == 256);
|
||||
assert(s.counters.output_dropped_bytes == 0 && s.is_writer);
|
||||
puts("PASS isolated fanout overflow and exact 4096-byte high-water");
|
||||
|
||||
assert(drain(slow, 4000) == 4000);
|
||||
assert(snapshot(slow).counters.output_high_water_bytes == 4096);
|
||||
session_broker_global_snapshot_t before = global();
|
||||
assert(session_broker_clear_client_counters(slow) == ESP_OK);
|
||||
s = snapshot(slow);
|
||||
assert(s.output_bytes_pending == 96 && s.counters.output_high_water_bytes == 96);
|
||||
assert(s.counters.output_queued_bytes == 0 && s.counters.output_read_bytes == 0 && s.counters.output_dropped_bytes == 0);
|
||||
assert(global().counters.output_queued_bytes == before.counters.output_queued_bytes);
|
||||
feed(256); assert(snapshot(slow).counters.output_high_water_bytes == 352);
|
||||
assert(session_broker_clear_counters() == ESP_OK);
|
||||
s = snapshot(slow);
|
||||
assert(s.output_bytes_pending == 352 && s.counters.output_high_water_bytes == 352);
|
||||
assert(global().writer_id == fast && global().latest_event_sequence == before.latest_event_sequence);
|
||||
assert(global().counters.uart_rx_bytes == 0 && s.counters.uart_rx_bytes == 0);
|
||||
assert(drain(slow, 352) == 352);
|
||||
assert(snapshot(slow).counters.output_read_bytes == 352);
|
||||
feed(sizeof(payload));
|
||||
assert(snapshot(slow).counters.output_dropped_bytes == 256);
|
||||
puts("PASS per-client/global clears seed pending; subsequent read/drop/HWM accounting");
|
||||
|
||||
before = global();
|
||||
assert(session_broker_disconnect(slow) == ESP_OK);
|
||||
assert(global().counters.output_dropped_bytes == before.counters.output_dropped_bytes + 4096);
|
||||
assert(session_broker_get_client_snapshot(slow, &s) == ESP_ERR_NOT_FOUND);
|
||||
session_broker_client_id_t replacement = connect_type(SESSION_BROKER_CLIENT_WEB);
|
||||
assert(replacement != slow && (replacement & 7) == (slow & 7));
|
||||
s = snapshot(replacement);
|
||||
assert(s.output_bytes_pending == 0 && s.counters.output_high_water_bytes == 0);
|
||||
assert(s.counters.output_queued_bytes == 0 && s.counters.output_read_bytes == 0 && s.counters.output_dropped_bytes == 0);
|
||||
assert(session_broker_clear_client_counters(slow) == ESP_ERR_NOT_FOUND);
|
||||
puts("PASS disconnect discard retained globally and generation-safe reuse resets diagnostics");
|
||||
|
||||
while (session_broker_list_clients(NULL, 0) < SESSION_BROKER_MAX_CLIENTS)
|
||||
connect_type(SESSION_BROKER_CLIENT_INTERNAL);
|
||||
struct { session_broker_client_snapshot_t row; uint64_t guard; } bounded = {.guard = UINT64_MAX};
|
||||
assert(session_broker_list_clients(&bounded.row, 1) == 1 && bounded.guard == UINT64_MAX);
|
||||
before = global();
|
||||
assert(command("counters", 0, NULL) == 0);
|
||||
assert(strstr(console_output, "pending HWM") && !strstr(console_output, "SECRET-NAME") && !strchr(console_output, '\033'));
|
||||
unsigned lines = 0;
|
||||
for (const char *p = strstr(console_output, "ID type"); *p; ++p) lines += *p == '\n';
|
||||
assert(lines == 1 + SESSION_BROKER_MAX_CLIENTS && console_used < 4096);
|
||||
assert(global().counters.output_read_bytes == before.counters.output_read_bytes);
|
||||
puts("PASS bounded eight-row metadata-only console counters; snapshots do not consume output");
|
||||
|
||||
feed(1024);
|
||||
assert(command("read", replacement, "513") == 1);
|
||||
assert(snapshot(replacement).output_bytes_pending == 1024);
|
||||
assert(command("read", replacement, "0") == 1);
|
||||
assert(command("read", replacement, NULL) == 0);
|
||||
assert(strstr(console_output, "read 512 bytes: 00010203") && !strchr(console_output, '\033'));
|
||||
assert(snapshot(replacement).output_bytes_pending == 512);
|
||||
assert(command("read", replacement, "1") == 0);
|
||||
assert(snapshot(replacement).output_bytes_pending == 511);
|
||||
puts("PASS console read bounds and binary-safe hexadecimal rendering");
|
||||
disconnect_all();
|
||||
assert(command("counters", 0, NULL) == 0);
|
||||
comparison(1); comparison(2);
|
||||
assert(allocations == initial_allocations);
|
||||
puts("PASS no post-init broker allocations; 7 diagnostic groups passed");
|
||||
cleanup_allocations();
|
||||
return 0;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user