ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
runtime.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define STATS_RUNTIME_TRANSPORT_BUFFER_SIZE 16
4#define STATS_RUNTIME_STATE_BUFFER_SIZE 64
5#define STATS_RUNTIME_OUTPUT_BUFFER_SIZE 8192
6#define STATS_RUNTIME_FIRST_USE_PATH_BUFFER_SIZE 4096
7#define STATS_RUNTIME_FIRST_USE_MESSAGE_BUFFER_SIZE 512
8
9/* Existing buffer budgets in bytes, including NUL for text storage. */
10#define STATS_PEER_CAPACITY 64
11
15#include <ascii-chat/otel.h>
17#include <libplatform/atomic.h>
18#include <ascii-chat/common.h>
21#include <libplatform/compiler.h>
22#include <libplatform/system.h>
23#include <libplatform/terminal.h>
24#include <libplatform/systemd.h>
25#include <libplatform/thread.h>
29#include <libplatform/filesystem.h>
35#include <stdarg.h>
36#include <stdio.h>
37#include <math.h>
38#include <ctype.h>
39#include <stdlib.h>
41#include <string.h>
42
54
55static stats_scope_t *g_scope;
56static stats_sampler_t *g_sampler;
57static mutex_t g_peers_mutex, g_provider_mutex;
58static stats_peer_t *g_peers[STATS_PEER_CAPACITY];
59static uint64_t g_peer_id, g_peer_count;
60static stats_provider_fn g_provider;
61static void *g_provider_data;
63static unsigned g_interval;
64static atomic_t g_stop, g_active, g_onboarding_wait;
65static platform_thread_t g_worker;
66static bool g_started, g_keyboard;
67static int g_fd;
68static atomic_t g_media_width, g_media_height, g_media_position, g_media_duration;
69void stats_runtime_media(unsigned width, unsigned height, double position, double duration) {
70 if (!g_scope)
71 return;
72 atomic_store_u64_impl(&g_media_width, width);
73 atomic_store_u64_impl(&g_media_height, height);
74 atomic_store_u64_impl(&g_media_position,
75 isfinite(position) && position >= 0 && position < 1e12 ? (uint64_t)(position * 1000) + 1 : 0);
76 atomic_store_u64_impl(&g_media_duration,
77 isfinite(duration) && duration > 0 && duration < 1e12 ? (uint64_t)(duration * 1000) + 1 : 0);
78}
79
81 return g_scope;
82}
84 return peer ? peer->scope : NULL;
85}
91
93 if (g_started && g_keyboard) {
94 bool active = !stats_runtime_active();
95 atomic_store_bool_impl(&g_active, active);
97 log_debug("Statistics TUI requested: %s", active ? "open" : "closed");
98 }
99}
100
101static void stats_view_vadd(stats_view_t *view, bool heading, const char *format, va_list args) {
102 if (view->count >= STATS_VIEW_LINES)
103 return;
104 vsnprintf(view->lines[view->count], sizeof(view->lines[0]), format, args);
105 // Provider labels must never inject terminal controls.
106 for (char *p = view->lines[view->count]; *p; ++p)
107 if ((unsigned char)*p < 32 || (unsigned char)*p > 126)
108 *p = '?';
109 view->headings[view->count++] = heading;
110}
111
112void stats_view_add(stats_view_t *view, const char *format, ...) {
113 va_list args;
114 va_start(args, format);
115 stats_view_vadd(view, false, format, args);
116 va_end(args);
117}
118
119void stats_view_add_header(stats_view_t *view, const char *format, ...) {
120 va_list args;
121 va_start(args, format);
122 stats_view_vadd(view, true, format, args);
123 va_end(args);
124}
125
127 if (!g_scope)
128 return;
129 mutex_lock(&g_provider_mutex);
130 g_provider = provider;
131 g_provider_data = data;
132 mutex_unlock(&g_provider_mutex);
133}
134
135void stats_runtime_connection_state(const char *state) {
136 if (!g_scope)
137 return;
138 mutex_lock(&g_provider_mutex);
139 snprintf(g_state, sizeof(g_state), "%s", state);
140 mutex_unlock(&g_provider_mutex);
141}
142
143static stats_capabilities_t network_capabilities(void) {
144 stats_capabilities_t caps = {0};
146 caps.counters[i] = true;
149 return caps;
150}
151
152stats_peer_t *stats_runtime_peer_open(const char *transport) {
153 if (!g_scope)
154 return NULL;
155 stats_peer_t *peer = SAFE_CALLOC(1, sizeof(*peer), stats_peer_t *);
156 if (!peer)
157 return NULL;
158 stats_capabilities_t caps = network_capabilities();
160 if (stats_scope_create(&caps, &peer->scope) != ASCIICHAT_OK ||
161 stats_sampler_create(&config, &peer->sampler) != ASCIICHAT_OK) {
163 SAFE_FREE(peer);
164 return NULL;
165 }
166 snprintf(peer->transport, sizeof(peer->transport), "%s", transport);
167 mutex_lock(&g_peers_mutex);
168 peer->id = ++g_peer_id;
169 ++g_peer_count;
170 stats_gauge_set(g_scope, STATS_GAUGE_CONNECTIONS_ACTIVE, g_peer_count);
171 for (unsigned i = 0; i < STATS_PEER_CAPACITY; ++i) {
172 if (!g_peers[i]) {
173 g_peers[i] = peer;
174 break;
175 }
176 }
177 mutex_unlock(&g_peers_mutex);
178 return peer;
179}
180
182 if (!peer)
183 return;
184 mutex_lock(&g_peers_mutex);
185 for (unsigned i = 0; i < STATS_PEER_CAPACITY; ++i)
186 if (g_peers[i] == peer)
187 g_peers[i] = NULL;
188 --g_peer_count;
189 stats_gauge_set(g_scope, STATS_GAUGE_CONNECTIONS_ACTIVE, g_peer_count);
190 mutex_unlock(&g_peers_mutex);
193 SAFE_FREE(peer);
194}
195
196void stats_runtime_packet(stats_peer_t *peer, unsigned type, size_t payload_bytes, bool sent, bool success) {
197 if (success) {
198 ascii_otel_metric(sent ? ASCII_OTEL_METRIC_NET_TX_SIZE : ASCII_OTEL_METRIC_NET_RX_SIZE, (double)payload_bytes);
199 }
200 if (sent && type == PACKET_TYPE_ERROR_MESSAGE)
202 stats_scope_t *scopes[] = {g_scope, peer ? peer->scope : NULL};
203 for (unsigned i = 0; i < 2; ++i) {
204 if (!success) {
206 continue;
207 }
210 sizeof(packet_header_t) + payload_bytes);
214 }
215}
216
217static void build_view(stats_view_t *view, stats_snapshot_t *snapshot, stats_rates_t *rates) {
218 snprintf(view->mode, sizeof(view->mode), "%s", g_mode);
219 view->sampled_ns = snapshot->sampled_ns;
220 if (!view->page) {
221 stats_view_add(view, "Uptime %.1fs", (double)(snapshot->sampled_ns - snapshot->started_ns) / 1e9);
222 stats_view_add_header(view, "%-28s %14s %14s",
223 strcmp(g_mode, "mcp-server") == 0 ? "MCP REQUESTS / RENDERS" : "PIPELINE / NETWORK", "TOTAL",
224 "PER SECOND");
225 for (int i = 0; i < STATS_COUNTER_COUNT; ++i) {
226 if (!snapshot->capabilities.counters[i])
227 continue;
228 const stats_descriptor_t *descriptor = stats_counter_descriptor(i);
229 if (rates->ready)
230 stats_view_add(view, "%-28s %14llu %14.1f", descriptor->name, (unsigned long long)snapshot->counters[i],
231 rates->counters_per_second[i]);
232 else
233 stats_view_add(view, "%-28s %14llu %14s", descriptor->name, (unsigned long long)snapshot->counters[i],
234 "warming up");
235 }
236 stats_view_add(view, "");
237 stats_view_add_header(view, "%-28s %14s %14s", "TIMINGS", "MEAN/LAST ms", "LIFETIME MAX");
238 for (int i = 0; i < STATS_DURATION_COUNT; ++i) {
239 if (!snapshot->capabilities.durations[i])
240 continue;
241 if (i == STATS_DURATION_ASCII_CONVERT && strcmp(g_mode, "client") == 0 && !snapshot->durations[i].observations)
242 stats_view_add(view, "%-28s %14s %14s", "ascii_convert", "n/a (server)", "n/a");
243 else if (i == STATS_DURATION_CONNECTION_SETUP && snapshot->durations[i].observations)
244 stats_view_add(view, "%-28s %14.3f %14.3f", "connection_setup (last)",
245 (double)snapshot->durations[i].last_ns / 1e6, (double)snapshot->durations[i].max_ns / 1e6);
246 else if (rates->duration_valid[i])
247 stats_view_add(view, "%-28s %14.3f %14.3f", stats_duration_descriptor(i)->name,
248 rates->duration_mean_ns[i] / 1e6, (double)snapshot->durations[i].max_ns / 1e6);
249 else if (snapshot->durations[i].observations)
250 stats_view_add(view, "%-28s %14s %14.3f", stats_duration_descriptor(i)->name, "idle",
251 (double)snapshot->durations[i].max_ns / 1e6);
252 else
253 stats_view_add(view, "%-28s %14s %14s", stats_duration_descriptor(i)->name, "not sampled", "not sampled");
254 }
255 stats_view_add(view, "");
256 for (int i = 0; i < STATS_GAUGE_COUNT; ++i)
257 if (snapshot->capabilities.gauges[i])
258 stats_view_add(view, "%s: %llu", stats_gauge_descriptor(i)->name, (unsigned long long)snapshot->gauges[i]);
259 uint64_t source_width = atomic_load_u64_impl(&g_media_width);
260 if (source_width) {
261 stats_view_add(view, "Source: %llux%llu Output: %ux%u Target: %d fps", (unsigned long long)source_width,
262 (unsigned long long)atomic_load_u64_impl(&g_media_height), GET_OPTION(width), GET_OPTION(height),
263 GET_OPTION(fps));
264 uint64_t position = atomic_load_u64_impl(&g_media_position), duration = atomic_load_u64_impl(&g_media_duration);
265 if (position && duration)
266 stats_view_add(view, "Media position: %.1fs / %.1fs", (double)(position - 1) / 1000,
267 (double)(duration - 1) / 1000);
268 else
269 stats_view_add(view, "Media position: unavailable (live source)");
270 }
271 if (strcmp(g_mode, "discovery-service") != 0 && strcmp(g_mode, "mcp-server") != 0)
272 stats_view_add(view, "Audio: %s Recording: %s", GET_OPTION(audio_enabled) ? "enabled" : "disabled",
273 GET_OPTION(render_file)[0] ? "enabled" : "disabled");
274 if (strcmp(g_mode, "mirror") != 0 && strcmp(g_mode, "mcp-server") != 0) {
275 stats_view_add(view, "Bytes: ACIP header + payload, excluding transport/encryption overhead.");
276 stats_view_add(view, "RTT includes uniquely matched probes only; unanswered probes are excluded.");
277 }
278 }
279 mutex_lock(&g_peers_mutex);
280 unsigned peers = 0;
281 if (view->page && strcmp(g_mode, "mcp-server") != 0)
282 stats_view_add_header(view, "%-5s %-12s %12s %12s %10s %10s", "ID", "TRANSPORT", "TX bytes/s", "RX bytes/s",
283 "TX fps", "RX fps");
284 for (unsigned i = 0; i < STATS_PEER_CAPACITY; ++i) {
285 stats_peer_t *peer = g_peers[i];
286 if (!peer)
287 continue;
288 peers++;
290 stats_rates_t pr;
291 stats_scope_snapshot(peer->scope, &ps);
292 stats_sampler_update(peer->sampler, &ps, &pr);
293 if (view->page)
295 view, "%-5llu %-12s %12.0f %12.0f %10.1f %10.1f", (unsigned long long)peer->id, peer->transport,
298 }
299 stats_gauge_set(g_scope, STATS_GAUGE_CONNECTIONS_ACTIVE, g_peer_count);
300 mutex_unlock(&g_peers_mutex);
301 if (view->page && strcmp(g_mode, "mcp-server") != 0)
302 stats_view_add(view, "%u transport handles shown (up to 64); process totals include all peers.", peers);
303 mutex_lock(&g_provider_mutex);
304 if (g_state[0])
305 stats_view_add(view, "State: %s", g_state);
306 if (g_provider)
307 g_provider(view, g_provider_data);
308 mutex_unlock(&g_provider_mutex);
309}
310
311static void sample_hidden_peers(void) {
312 mutex_lock(&g_peers_mutex);
313 for (unsigned i = 0; i < STATS_PEER_CAPACITY; ++i) {
314 stats_peer_t *peer = g_peers[i];
315 if (!peer)
316 continue;
317 stats_snapshot_t snapshot;
318 stats_rates_t rates;
319 stats_scope_snapshot(peer->scope, &snapshot);
320 stats_sampler_update(peer->sampler, &snapshot, &rates);
321 }
322 mutex_unlock(&g_peers_mutex);
323}
324
325static bool summary_append(char *output, size_t capacity, size_t *used, const char *format, ...) {
326 if (*used >= capacity)
327 return false;
328 va_list args;
329 va_start(args, format);
330 int written = vsnprintf(output + *used, capacity - *used, format, args);
331 va_end(args);
332 if (written < 0)
333 return false;
334 if ((size_t)written >= capacity - *used) {
335 *used = capacity - 1;
336 return false;
337 }
338 *used += (size_t)written;
339 return true;
340}
341
342static void print_summary(const stats_snapshot_t *snapshot, const stats_rates_t *rates) {
344 size_t used = 0;
345 // Reserve the final byte for the newline, even if a field is truncated.
346 size_t capacity = sizeof(output) - 1;
347 if (!summary_append(output, capacity, &used, "stats mode=%s uptime_s=%.3f", g_mode,
348 (double)(snapshot->sampled_ns - snapshot->started_ns) / 1e9))
349 goto done;
350 for (int i = 0; i < STATS_COUNTER_COUNT; ++i)
351 if (snapshot->capabilities.counters[i] &&
352 !summary_append(output, capacity, &used, " %s=%llu %s_per_s=%.3f", stats_counter_descriptor(i)->name,
353 (unsigned long long)snapshot->counters[i], stats_counter_descriptor(i)->name,
354 rates->counters_per_second[i]))
355 goto done;
356 for (int i = 0; i < STATS_GAUGE_COUNT; ++i)
357 if (snapshot->capabilities.gauges[i] &&
358 !summary_append(output, capacity, &used, " %s=%llu", stats_gauge_descriptor(i)->name,
359 (unsigned long long)snapshot->gauges[i]))
360 goto done;
361 for (int i = 0; i < STATS_DURATION_COUNT; ++i) {
362 if (!snapshot->capabilities.durations[i])
363 continue;
364 bool appended =
365 rates->duration_valid[i]
366 ? summary_append(output, capacity, &used, " %s_mean_ms=%.3f", stats_duration_descriptor(i)->name,
367 rates->duration_mean_ns[i] / 1e6)
368 : summary_append(output, capacity, &used, " %s_mean_ms=unavailable", stats_duration_descriptor(i)->name);
369 if (!appended)
370 goto done;
371 }
372done:
373 output[used++] = '\n';
374 ui_controller_write(STDOUT_FILENO, output, used);
375}
376
377static bool show_first_use_instructions(void) {
378 if (!g_keyboard || GET_OPTION(snapshot_mode) || GET_OPTION(render_file)[0] || !platform_isatty(STDERR_FILENO))
379 return false;
380 const char *title = NULL, *text = NULL;
381 if (!strcmp(g_mode, "mirror")) {
382 title = "Mirror mode";
383 text = "Preview your camera locally.\n\n?\tShow keyboard shortcuts";
384 } else if (!strcmp(g_mode, "client")) {
385 title = "Client mode";
386 text = "Connect to a server with ascii-chat client <address>.\n\n?\tShow keyboard shortcuts";
387 } else if (!strcmp(g_mode, "discovery")) {
388 title = "Discovery mode";
389 text = "Start without a name to create a session. Share its name to invite others. "
390 "Join with ascii-chat <session-name>.\n\n?\tShow keyboard shortcuts";
391 } else if (!strcmp(g_mode, "server")) {
392 title = "Server mode";
393 text = "Share this server's address and port with participants.\n\n?\tShow keyboard shortcuts";
394 } else if (!strcmp(g_mode, "discovery-service")) {
395 title = "Discovery service mode";
396 text = "Help participants find sessions. Configure clients to use this service's address and port."
397 "\n\n?\tShow keyboard shortcuts";
398 } else if (!strcmp(g_mode, "mcp-server")) {
399 title = "MCP server mode";
400 text = "Expose rendering tools to MCP clients. Configure an MCP client to connect."
401 "\n\n?\tShow keyboard shortcuts";
402 }
403 if (!text)
404 return false;
406 const char *disabled = platform_getenv("ASCII_CHAT_DISABLE_FIRST_USE");
407 if (disabled && !strcmp(disabled, "1")) {
409 return false;
410 }
411 const char *override = platform_getenv("ASCII_CHAT_FIRST_USE_DIR");
412 char *dir = override && *override ? platform_strdup(override) : platform_get_data_dir();
413 if (!dir) {
415 return false;
416 }
418 int length = snprintf(marker_path, sizeof(marker_path), "%s/first-use-%s", dir, g_mode);
420 snprintf(message, sizeof(message), "%s", text);
421 bool shown = length > 0 && (size_t)length < sizeof(marker_path) && !platform_is_regular_file(marker_path) &&
422 platform_mkdir_recursive(dir, 0700) == ASCIICHAT_OK &&
423 ui_notice_present_first_use(title, message, marker_path) == ASCIICHAT_OK;
424 SAFE_FREE(dir);
426 return shown;
427}
428
429static void *stats_worker(void *unused) {
430 (void)unused;
431 uint64_t next_sample = 0, next_print = time_get_ns() + (uint64_t)g_interval * NS_PER_SEC_INT;
432 unsigned page = 0;
433 size_t last_count = 0;
434 ui_viewport_t viewport = {0};
435 bool was_active = false;
436 uint64_t next_telemetry = 0;
437 while (!atomic_load_bool_impl(&g_stop) && !shutdown_is_requested()) {
438 platform_systemd_notify_watchdog();
440 bool active = stats_runtime_active();
441 // Own input only while our screen is visible, or while a server has no screen.
442 bool server_like = strcmp(g_mode, "server") == 0 || strcmp(g_mode, "discovery-service") == 0 ||
443 (strcmp(g_mode, "mcp-server") == 0 && !strcmp(GET_OPTION(mcp_transport), "http"));
444 bool first_use_input =
445 atomic_load_bool_impl(&g_onboarding_wait) &&
447 if (g_keyboard && (first_use_input || active || (server_like && !ui_controller_screen_live(UI_SCREEN_STATUS)))) {
448 keyboard_key_t key =
450 if (first_use_input && (key == 'q' || key == 3)) {
452 continue;
453 }
454 active = stats_runtime_active();
455 if (active && key == ']') {
457 active = false;
459 } else if (active && (key == '=' || key == KEY_ESCAPE)) {
460 atomic_store_bool_impl(&g_active, false);
461 active = false;
462 } else if (!active && key == '=') {
464 active = stats_runtime_active();
465 } else if (key == '\t' || key == KEY_LEFT || key == KEY_RIGHT) {
466 page = !page;
467 viewport = (ui_viewport_t){0};
468 } else {
469 terminal_size_t size = ui_controller_size();
470 ui_viewport_key(&viewport, last_count, size.rows > 6 ? (size_t)size.rows - 6 : 1, key);
471 }
472 }
473 if (active != was_active)
474 log_debug("Statistics TUI visibility: %d (selected=%d)", active, ui_controller_interactive_screen());
475 if (!active && was_active) {
477 if (ui_controller_state().screen < 0) {
478 const char restore[] = "\033[0m\033[2J\033[H\033[?25h";
479 ui_controller_write(g_fd, restore, sizeof(restore) - 1);
480 }
481 }
482 uint64_t now = time_get_ns();
483 if (now >= next_sample || active != was_active) {
484 stats_view_t view = {.page = page};
485 stats_snapshot_t snapshot;
486 stats_rates_t rates;
487 stats_scope_snapshot(g_scope, &snapshot);
488 stats_sampler_update(g_sampler, &snapshot, &rates);
489 bool print_due = g_interval && now >= next_print;
490 bool telemetry_due = ascii_otel_enabled() && now >= next_telemetry;
491 if (active || print_due || telemetry_due) {
492 build_view(&view, &snapshot, &rates);
493 // Providers refresh gauges while building the view.
494 stats_scope_snapshot(g_scope, &snapshot);
495 } else {
496 sample_hidden_peers();
497 }
498 if (telemetry_due)
499 next_telemetry = now + NS_PER_SEC_INT;
500 terminal_size_t size = ui_controller_size();
501 ui_viewport_update(&viewport, view.count, size.rows > 6 ? (size_t)size.rows - 6 : 1);
502 last_count = view.count;
503 view.selected = (unsigned)viewport.selected;
504 view.offset = (unsigned)viewport.first_visible;
505 if (active)
506 ui_controller_submit(UI_SCREEN_STATS, g_fd, tui_stats_minimum(&view), tui_stats_render, &view, sizeof(view));
507 if (print_due) {
508 print_summary(&snapshot, &rates);
509 next_print = now + (uint64_t)g_interval * NS_PER_SEC_INT;
510 }
511 next_sample = now + 250 * NS_PER_MS_INT;
512 }
513 was_active = active;
514 platform_sleep_ns(10 * NS_PER_MS_INT);
515 }
517 return NULL;
518}
519
520asciichat_error_t stats_runtime_start(const char *mode, unsigned interval_seconds) {
521 if (!mode)
522 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing statistics mode");
523 if (g_started)
524 return ASCIICHAT_OK;
525 bool mcp = strcmp(mode, "mcp-server") == 0;
526 stats_capabilities_t caps = network_capabilities();
527 bool mirror = strcmp(mode, "mirror") == 0, acds = strcmp(mode, "discovery-service") == 0;
528 if (mirror)
529 memset(&caps, 0, sizeof(caps));
530 if (!acds) {
531 for (int i = 0; i <= STATS_COUNTER_FRAMES_DROPPED; ++i)
532 caps.counters[i] = true;
535 GET_OPTION(audio_enabled);
536 for (int i = 0; i <= STATS_DURATION_TERMINAL_WRITE; ++i)
537 caps.durations[i] = true;
539 caps.durations[STATS_DURATION_AUDIO_MIX] = GET_OPTION(audio_enabled) && !mirror;
540 if (mirror) {
543 }
544 } else {
547 caps.counters[i] = true;
551 }
552 if (strcmp(mode, "discovery") == 0)
554 caps.gauges[STATS_GAUGE_VIDEO_QUEUE_DEPTH] = strcmp(mode, "server") == 0;
555 if (GET_OPTION(render_file)[0]) {
558 }
559 if (!mirror) {
561 caps.durations[STATS_DURATION_RTT] = !acds;
566 }
567 if (mcp) {
568 memset(&caps, 0, sizeof(caps));
570 caps.counters[i] = true;
572 caps.counters[i] = true;
577 }
578 asciichat_error_t result = stats_scope_create(&caps, &g_scope);
579 if (result != ASCIICHAT_OK)
580 return result;
583 result = stats_sampler_create(&config, &g_sampler);
584 if (result != ASCIICHAT_OK)
585 goto fail_scope;
586 if (mutex_init(&g_peers_mutex, "stats_peers") != 0) {
587 result = SET_ERRNO(ERROR_THREAD, "Cannot initialize stats peer mutex");
588 goto fail_sampler;
589 }
590 if (mutex_init(&g_provider_mutex, "stats_provider") != 0) {
591 result = SET_ERRNO(ERROR_THREAD, "Cannot initialize stats provider mutex");
592 goto fail_peers;
593 }
594 g_peer_count = g_peer_id = 0;
595 memset(g_peers, 0, sizeof(g_peers));
596 g_state[0] = 0;
597 stats_runtime_media(0, 0, -1, -1);
598 snprintf(g_mode, sizeof(g_mode), "%s", mode);
599 g_interval = interval_seconds;
600 g_fd = platform_isatty(STDOUT_FILENO) ? STDOUT_FILENO : STDERR_FILENO;
601 g_keyboard = (!mcp || !strcmp(GET_OPTION(mcp_transport), "http")) && platform_isatty(STDIN_FILENO) &&
602 platform_isatty(g_fd) && keyboard_init() == ASCIICHAT_OK;
603 atomic_store_bool_impl(&g_stop, false);
604 atomic_store_bool_impl(&g_active, false);
605 atomic_store_bool_impl(&g_onboarding_wait, false);
606 g_started = true;
607 bool server_like = strcmp(mode, "server") == 0 || strcmp(mode, "discovery-service") == 0 ||
608 (mcp && !strcmp(GET_OPTION(mcp_transport), "http"));
609 if (g_keyboard && server_like && ui_controller_start_terminal(g_fd) != ASCIICHAT_OK)
610 LOG_ERRNO_IF_SET("Cannot start terminal presentation");
611 if (platform_thread_create(&g_worker, "stats", stats_worker, NULL) == 0) {
612 atomic_store_bool_impl(&g_onboarding_wait, true);
613 bool shown = show_first_use_instructions();
616 if (state.screen == UI_SCREEN_NOTICE && state.covered)
617 break;
619 }
620 atomic_store_bool_impl(&g_onboarding_wait, false);
621 return ASCIICHAT_OK;
622 }
623 result = SET_ERRNO(ERROR_THREAD, "Cannot start stats worker");
624 g_started = false;
625 if (g_keyboard)
627 mutex_destroy(&g_provider_mutex);
628fail_peers:
629 mutex_destroy(&g_peers_mutex);
630fail_sampler:
631 stats_sampler_destroy(g_sampler);
632 g_sampler = NULL;
633fail_scope:
634 stats_scope_destroy(g_scope);
635 g_scope = NULL;
636 return result;
637}
638
640 if (!g_started)
641 return;
642 atomic_store_bool_impl(&g_stop, true);
643 thread_join(&g_worker, NULL);
645 if (g_keyboard)
647 g_provider = NULL;
648 g_provider_data = NULL;
649 stats_sampler_destroy(g_sampler);
650 stats_scope_destroy(g_scope);
651 g_sampler = NULL;
652 g_scope = NULL;
653 mutex_destroy(&g_provider_mutex);
654 mutex_destroy(&g_peers_mutex);
655 g_started = false;
656}
action_args_t args
Definition actions.c:78
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_scope_begin(void)
#define SET_ERRNO(code,...)
@ ASCIICHAT_ERRNO_DISMISSED
#define LOG_ERRNO_IF_SET(message)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
const stats_descriptor_t * stats_duration_descriptor(stats_duration_id_t id)
Definition descriptors.c:90
const stats_descriptor_t * stats_gauge_descriptor(stats_gauge_id_t id)
Definition descriptors.c:86
const stats_descriptor_t * stats_counter_descriptor(stats_counter_id_t id)
Definition descriptors.c:82
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
Buffered terminal frame rendering.
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_SEC_INT
Definition time.h:169
#define NS_PER_MS_INT
Definition time.h:168
@ PACKET_TYPE_IMAGE_FRAME
Complete RGB image with dimensions.
Definition packet.h:385
@ PACKET_TYPE_IMAGE_FRAME_H265
H.265-encoded frame from client (HEVC/H.265 codec).
Definition packet.h:387
@ PACKET_TYPE_ASCII_FRAME
Complete ASCII frame with all metadata.
Definition packet.h:383
@ PACKET_TYPE_IMAGE_FRAME_H264
Definition packet.h:389
@ PACKET_TYPE_ERROR_MESSAGE
Error packet with asciichat_error_t code and human-readable message.
Definition packet.h:374
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
asciichat_error_t keyboard_init(void)
Initialize keyboard input system.
const char * platform_getenv(const char *name)
Get an environment variable value.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
void keyboard_destroy(void)
Cleanup keyboard input system and restore terminal.
keyboard_key_t
Options for interactive line editing.
Definition keyboard.h:53
@ KEY_ESCAPE
Escape key (ESC).
Definition keyboard.h:55
@ KEY_LEFT
Left arrow key.
Definition keyboard.h:59
@ KEY_RIGHT
Right arrow key.
Definition keyboard.h:60
int keyboard_help_parent_screen(void)
void shutdown_request(void)
Definition common.c:82
bool shutdown_is_requested(void)
Check if shutdown has been requested.
Definition common.c:86
@ STATS_COUNTER_RATE_LIMIT_REJECTIONS
@ STATS_COUNTER_MIGRATION_FAILURES
@ STATS_COUNTER_MCP_HTTP_RESPONSES
@ STATS_GAUGE_PARTICIPANTS_ACTIVE
@ STATS_GAUGE_HTTP_CONNECTIONS_ACTIVE
@ STATS_GAUGE_HTTP_STREAMS_ACTIVE
@ STATS_GAUGE_AUDIO_BUFFERED_SAMPLES
@ STATS_DURATION_CONNECTION_SETUP
struct stats_sampler stats_sampler_t
struct stats_scope stats_scope_t
int keyboard_help_previous_interactive(void)
🆘 Interactive keyboard help overlay for session keyboard shortcuts
void stats_scope_destroy(stats_scope_t *scope)
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
void stats_sampler_destroy(stats_sampler_t *sampler)
asciichat_error_t stats_scope_snapshot(const stats_scope_t *scope, stats_snapshot_t *out)
stats sampler config keeps the values that move through the subsystem: window_ns, resolution_ns....
asciichat_error_t stats_scope_create(const stats_capabilities_t *capabilities, stats_scope_t **out_scope)
asciichat_error_t stats_sampler_update(stats_sampler_t *sampler, const stats_snapshot_t *snapshot, stats_rates_t *out_rates)
void stats_gauge_set(stats_scope_t *scope, stats_gauge_id_t id, uint64_t value)
asciichat_error_t stats_sampler_create(const stats_sampler_config_t *config, stats_sampler_t **out_sampler)
void tui_stats_render(terminal_size_t size, const void *data)
terminal_size_t tui_stats_minimum(const stats_view_t *view)
🔢 Mathematical Utility Functions
asciichat_error_t ui_notice_present_first_use(const char *title, const char *text, const char *marker_path)
Definition notice.c:416
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
bool ascii_otel_enabled(void)
Definition otel.c:158
void ascii_otel_stats_attach(stats_scope_t *scope)
Definition otel.c:218
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
stats_scope_t * stats_runtime_peer_scope(stats_peer_t *peer)
Definition runtime.c:83
#define STATS_RUNTIME_TRANSPORT_BUFFER_SIZE
Definition runtime.c:3
void stats_runtime_set_provider(stats_provider_fn provider, void *data)
Definition runtime.c:126
void stats_view_add_header(stats_view_t *view, const char *format,...)
Definition runtime.c:119
#define STATS_RUNTIME_STATE_BUFFER_SIZE
Definition runtime.c:4
void stats_runtime_toggle(void)
Definition runtime.c:92
void stats_runtime_connection_state(const char *state)
Definition runtime.c:135
void stats_runtime_peer_close(stats_peer_t *peer)
Definition runtime.c:181
void stats_runtime_media(unsigned width, unsigned height, double position, double duration)
Definition runtime.c:69
void stats_runtime_stop(void)
Definition runtime.c:639
bool stats_runtime_active(void)
Definition runtime.c:86
#define STATS_RUNTIME_OUTPUT_BUFFER_SIZE
Definition runtime.c:5
#define STATS_RUNTIME_FIRST_USE_MESSAGE_BUFFER_SIZE
Definition runtime.c:7
stats_peer_t * stats_runtime_peer_open(const char *transport)
Definition runtime.c:152
#define STATS_RUNTIME_FIRST_USE_PATH_BUFFER_SIZE
Definition runtime.c:6
void stats_view_add(stats_view_t *view, const char *format,...)
Definition runtime.c:112
void stats_runtime_packet(stats_peer_t *peer, unsigned type, size_t payload_bytes, bool sent, bool success)
Definition runtime.c:196
#define STATS_PEER_CAPACITY
Definition runtime.c:10
asciichat_error_t stats_runtime_start(const char *mode, unsigned interval_seconds)
Definition runtime.c:520
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
Interactive search filtering for terminal screens.
Shutdown check system for clean library/application separation.
#define STATS_VIEW_LINES
#define STATS_RUNTIME_MODE_BUFFER_SIZE
void(* stats_provider_fn)(stats_view_t *view, void *data)
struct stats_peer stats_peer_t
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
Fixed prefix shared by every ACIP packet on the wire.
Definition packet.h:628
stats descriptor is intentionally a small, concrete record. Fields such as name, unit,...
bool counters[STATS_COUNTER_COUNT]
bool durations[STATS_DURATION_COUNT]
When code needs to work with stats peer, it keeps scope, sampler, id here instead of scattering it ac...
Definition runtime.c:48
uint64_t id
Definition runtime.c:52
char transport[16]
Definition runtime.c:51
stats_sampler_t * sampler
Definition runtime.c:50
stats_scope_t * scope
Definition runtime.c:49
double duration_mean_ns[STATS_DURATION_COUNT]
bool duration_valid[STATS_DURATION_COUNT]
double counters_per_second[STATS_COUNTER_COUNT]
stats_capabilities_t capabilities
uint64_t gauges[STATS_GAUGE_COUNT]
uint64_t counters[STATS_COUNTER_COUNT]
stats_duration_snapshot_t durations[STATS_DURATION_COUNT]
uint64_t sampled_ns
char mode[32]
bool headings[128]
unsigned page
char lines[128][160]
unsigned selected
unsigned offset
unsigned count
For ui viewport, this is the live record that ties selected, first_visible, page_size together....
Definition viewport.h:10
size_t first_visible
Definition viewport.h:11
size_t selected
Definition viewport.h:11
Reusable "fixed header + scrolling logs" terminal screen abstraction.
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t ui_controller_start_terminal(int fd)
void ui_controller_remove(ui_screen_t screen)
void ui_controller_select_interactive(int screen)
asciichat_error_t ui_controller_submit(ui_screen_t screen, int fd, terminal_size_t minimum, ui_render_fn render, const void *snapshot, size_t bytes)
void ui_controller_toggle_logs(void)
int ui_controller_current_screen(void)
terminal_size_t ui_controller_size(void)
void ui_controller_shutdown(void)
int ui_controller_interactive_screen(void)
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
ui_presentation_state_t ui_controller_state(void)
bool ui_controller_screen_live(ui_screen_t screen)
@ UI_SCREEN_NOTICE
@ UI_SCREEN_STATS
@ UI_SCREEN_STATUS
@ UI_SCREEN_HELP
keyboard_key_t ui_input_read_key(ui_screen_t screen)
Definition tui/input.c:353
bool ui_viewport_key(ui_viewport_t *v, size_t count, size_t rows, keyboard_key_t key)
Definition viewport.c:38
ui_viewport_range_t ui_viewport_update(ui_viewport_t *v, size_t count, size_t rows)
Definition viewport.c:4