ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
sync.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define TUI_SYNC_NAME_BUFFER_SIZE 256
4#define TUI_SYNC_TYPE_BUFFER_SIZE 24
5#define TUI_SYNC_FILE_BUFFER_SIZE 160
6#define TUI_SYNC_OUTPUT_BUFFER_SIZE 2048
7#define TUI_SYNC_LINE_BUFFER_SIZE 2048
8#define TUI_SYNC_LABEL_BUFFER_SIZE 288
9#define TUI_SYNC_DEADLOCK_BUFFER_SIZE 128
10#define TUI_SYNC_LINE_SHORT_BUFFER_SIZE 1024
11#define TUI_SYNC_NAME_EXTENDED_BUFFER_SIZE 288
12#define TUI_SYNC_STATE_BUFFER_SIZE 128
13#define TUI_SYNC_PADDED_STATE_BUFFER_SIZE 256
14#define TUI_SYNC_PLAIN_ACTIONS_BUFFER_SIZE 32
15#define TUI_SYNC_ACTIONS_BUFFER_SIZE 256
16#define TUI_SYNC_PADDED_ACTIONS_BUFFER_SIZE 32
17#define TUI_SYNC_CHANGES_BUFFER_SIZE 64
18#define TUI_SYNC_PADDED_WAIT_BUFFER_SIZE 48
19#define TUI_SYNC_COLORED_WAIT_BUFFER_SIZE 512
20#define TUI_SYNC_LOCKS_BUFFER_SIZE 96
21#define TUI_SYNC_UNLOCKS_BUFFER_SIZE 96
22#define TUI_SYNC_NUMBERS_BUFFER_SIZE 512
23#define TUI_SYNC_FULL_NAME_BUFFER_SIZE 288
24#define TUI_SYNC_COLORED_NAME_BUFFER_SIZE 640
25#define TUI_SYNC_OUTPUT_SHORT_BUFFER_SIZE 512
26
27/* Existing buffer budgets in bytes, including NUL for text storage. */
28#define SYNC_WAIT_SNAPSHOT_CAPACITY 256
29#define SYNC_ROW_CAPACITY 128
30#define SYNC_OUTPUT_BUFFER_SIZE (64 * 1024)
31
35#include <ascii-chat/ui/table.h>
36#include <ascii-chat/ui/text.h>
37#include <ascii-chat/tui/sync.h>
44#include <libplatform/compiler.h>
45#include <libplatform/system.h>
46#include <libplatform/terminal.h>
47#include <libplatform/thread.h>
48#include <libplatform/cond.h>
49#include <libplatform/rwlock.h>
50#include <ascii-chat/common.h>
55#include <stdatomic.h>
56#include <inttypes.h>
57#include <string.h>
58#include <stdio.h>
59#include <stdlib.h>
60#include <limits.h>
61
62#ifndef NDEBUG
63
75
93
94static sync_sample_t g_sync_samples[3] = {
95 {.busy = ATOMIC_FLAG_INIT}, {.busy = ATOMIC_FLAG_INIT}, {.busy = ATOMIC_FLAG_INIT}};
96static _Atomic int g_sync_published = -1;
97static _Atomic int g_sync_selected;
98static _Atomic int g_sync_page_size = 1;
99static _Atomic int g_sync_total;
100static _Atomic bool g_sync_stop;
101static atomic_flag g_sync_starting = ATOMIC_FLAG_INIT;
102static bool g_sync_started;
103static platform_thread_t g_sync_thread;
104static int g_sync_fd = -1;
105static _Thread_local bool g_sync_use_color, g_sync_use_utf8;
106
107static void sync_copy_row(uintptr_t key, uint64_t generation, const char *name, const char *type, const char *file,
108 int line, void *data) {
109 sync_sample_t *sample = data;
110 if (!type || (strcmp(type, "mutex") && strcmp(type, "rwlock") && strcmp(type, "cond") && strcmp(type, "atomic") &&
111 strcmp(type, "atomic_ptr")))
112 return;
113 // Initializers and their owning resources can both register the same
114 // primitive. Show its latest name once, rather than counting aliases.
115 sync_row_t *r = NULL;
116 for (size_t i = 0; i < sample->count; ++i) {
117 if (sample->rows[i].key != key)
118 continue;
119 if (sample->rows[i].generation >= generation)
120 return;
121 r = &sample->rows[i];
122 break;
123 }
124 if (!r)
125 r = &sample->rows[sample->count++];
126 memset(r, 0, sizeof(*r));
127 r->key = key;
128 r->generation = generation;
129 r->line = line;
130 snprintf(r->name, sizeof(r->name), "%s", name ? name : "?");
131 snprintf(r->type, sizeof(r->type), "%s", type);
132 const char *source = file ? file : "?";
133 for (const char *p = source; *p; ++p) {
134 if (p != source && p[-1] != '/' && p[-1] != '\\')
135 continue;
136 if (((!strncmp(p, "lib", 3) || !strncmp(p, "src", 3)) && (p[3] == '/' || p[3] == '\\')) ||
137 (!strncmp(p, "include", 7) && (p[7] == '/' || p[7] == '\\'))) {
138 source = p;
139 break;
140 }
141 }
142 snprintf(r->file, sizeof(r->file), "%s", source);
143 for (char *p = r->file; *p; ++p)
144 if (*p == '\\')
145 *p = '/';
146 if (!strcmp(type, "mutex")) {
147 const mutex_t *m = (const mutex_t *)key;
148 r->owner = m->currently_held_by_key;
149 r->locks = m->lock_count + m->trylock_success_count;
150 r->unlocks = m->unlock_count;
151 r->last_ns = m->last_lock_time_ns;
152 } else if (!strcmp(type, "rwlock")) {
153 const rwlock_t *m = (const rwlock_t *)key;
154 r->owner = m->write_held_by_key;
155 r->value = atomic_load(&m->read_lock_count);
156 r->locks = m->rdlock_count + m->wrlock_count;
157 r->unlocks = m->unlock_count;
158 r->last_ns = m->last_wrlock_time_ns;
159 } else if (!strcmp(type, "cond")) {
160 const cond_t *m = (const cond_t *)key;
161 r->value = atomic_load(&m->waiting_count);
162 r->locks = m->wait_count;
163 r->unlocks = m->signal_count + m->broadcast_count;
164 r->last_ns = m->last_wait_time_ns;
165 r->owner = m->last_waiting_key;
166 r->wait_known = cond_total_wait_ns(m, time_get_ns(), &r->wait_ns);
167 } else if (!strcmp(type, "atomic")) {
168 const atomic_t *m = (const atomic_t *)key;
169 r->value = atomic_load(&m->impl);
170 r->changes = m->change_count;
171 r->actions = atomic_load(&m->load_count) + atomic_load(&m->store_count) + atomic_load(&m->cas_count) +
172 atomic_load(&m->fetch_count);
173 r->last_ns = m->last_store_time_ns;
174 } else {
175 const atomic_ptr_t *m = (const atomic_ptr_t *)key;
176 r->value = (uintptr_t)atomic_load(&m->impl);
177 r->changes = m->change_count;
178 r->actions = atomic_load(&m->load_count) + atomic_load(&m->store_count) + atomic_load(&m->cas_count) +
179 atomic_load(&m->exchange_count);
180 r->last_ns = m->last_store_time_ns;
181 }
182 if (strncmp(type, "atomic", 6))
183 r->actions = r->locks + r->unlocks;
184}
185
186static const char *sync_short_name(const sync_row_t *row) {
187 const char *name = strrchr(row->name, '#');
188 return name ? name + 1 : row->name;
189}
190
191static void sync_row_label(const sync_row_t *row, char *label, size_t capacity) {
192 const char *name = sync_short_name(row);
193 if (name != row->name)
194 snprintf(label, capacity, "%s.%" PRIu64, name, row->generation);
195 else
196 snprintf(label, capacity, "%s", name);
197}
198
199static int sync_compare_rows(const void *a, const void *b) {
200 const sync_row_t *x = a, *y = b;
201 if (x->cycle != y->cycle)
202 return x->cycle ? -1 : 1;
203 double x_activity = x->lock_rate + x->unlock_rate + x->change_rate;
204 double y_activity = y->lock_rate + y->unlock_rate + y->change_rate;
205 if (x_activity != y_activity)
206 return x_activity > y_activity ? -1 : 1;
207 if (x->total_score != y->total_score)
208 return x->total_score > y->total_score ? -1 : 1;
209 int result = strcmp(x->type, y->type);
210 if (!result)
211 result = strcmp(sync_short_name(x), sync_short_name(y));
212 return result ? result : x->generation < y->generation ? -1 : x->generation > y->generation;
213}
214
215static double sync_rate(uint64_t current, uint64_t previous, double seconds) {
216 return current >= previous && seconds > 0 ? (double)(current - previous) / seconds : 0;
217}
218
219static void *sync_collect_main(void *unused) {
220 (void)unused;
221 uint64_t next_sample = 0;
222 while (!atomic_load(&g_sync_stop)) {
224 // Poll the debug shortcut while hidden, but collect registry and waiter
225 // snapshots only for the visible overlay. The pairwise rate comparisons
226 // otherwise compete with live capture and input throughout the session.
227 if (!ui_sync_is_visible() &&
229 next_sample = 0;
230 platform_sleep_ns(10 * NS_PER_MS_INT);
231 continue;
232 }
233 uint64_t now = time_get_ns();
234 if (now >= next_sample) {
235 next_sample = now + 250 * NS_PER_MS_INT;
236 int previous = atomic_load(&g_sync_published);
237 for (int slot = 0; slot < 3; ++slot) {
238 sync_sample_t *sample = &g_sync_samples[slot];
239 if (slot == previous || atomic_flag_test_and_set(&sample->busy))
240 continue;
241 size_t required = 0;
242 bool complete = false;
243 sample->count = 0;
244 asciichat_error_t result =
245 named_registry_try_snapshot(sample->capacity, &required, &complete, sync_copy_row, sample);
246 if (result == ASCIICHAT_OK && !complete && required > sample->capacity &&
247 required <= SIZE_MAX / sizeof(sync_row_t)) {
248 // Diagnostic storage must not enter memory tracking or the registry.
249 sync_row_t *rows = UNTRACKED_MALLOC(required * sizeof(*rows), sync_row_t *);
250 if (rows) {
251 UNTRACKED_FREE(sample->rows);
252 sample->rows = rows;
253 sample->capacity = required;
254 }
255 }
256 if (complete) {
257 sample->sampled_ns = now;
259 size_t wait_count = 0;
260 sample->limited = false;
261 sample->waits_complete = mutex_stack_try_snapshot(waits, 256, &wait_count, &sample->limited);
262 for (size_t i = 0; i < sample->count; ++i) {
263 sync_row_t *r = &sample->rows[i];
264 for (size_t j = 0; j < wait_count; ++j) {
265 if (waits[j].mutex_key == r->key) {
266 ++r->waiters;
267 r->waiter = waits[j].thread_key;
268 r->cycle |= waits[j].cycle;
269 }
270 }
271 if (previous >= 0) {
272 const sync_sample_t *old = &g_sync_samples[previous];
273 double seconds = (double)(now - old->sampled_ns) / NS_PER_SEC_INT;
274 for (size_t j = 0; j < old->count; ++j) {
275 const sync_row_t *p = &old->rows[j];
276 if (p->generation == r->generation) {
277 r->lock_rate = sync_rate(r->locks, p->locks, seconds);
278 r->unlock_rate = sync_rate(r->unlocks, p->unlocks, seconds);
279 r->change_rate = sync_rate(r->changes, p->changes, seconds);
280 break;
281 }
282 }
283 }
284 }
285 uint64_t max_actions = 0, max_wait = 0;
286 for (size_t i = 0; i < sample->count; ++i) {
287 const sync_row_t *r = &sample->rows[i];
288 if (r->actions > max_actions)
289 max_actions = r->actions;
290 if (r->wait_known && r->wait_ns > max_wait)
291 max_wait = r->wait_ns;
292 }
293 // Normalize counts and durations before combining them with equal weight.
294 for (size_t i = 0; i < sample->count; ++i) {
295 sync_row_t *r = &sample->rows[i];
296 r->total_score = (max_actions ? (double)r->actions / (double)max_actions : 0) +
297 (max_wait && r->wait_known ? (double)r->wait_ns / (double)max_wait : 0);
298 }
299 qsort(sample->rows, sample->count, sizeof(*sample->rows), sync_compare_rows);
300 atomic_store(&g_sync_published, slot);
301 }
302 atomic_flag_clear(&sample->busy);
303 break;
304 }
305 }
306 platform_sleep_ns(10 * NS_PER_MS_INT);
307 }
308 return NULL;
309}
310
312 while (atomic_flag_test_and_set(&g_sync_starting))
315 if (!g_sync_started && platform_isatty(fd) && platform_isatty(STDIN_FILENO)) {
316 g_sync_fd = platform_dup(fd);
317 if (g_sync_fd < 0) {
318 result = SET_ERRNO_SYS(ERROR_FILE_OPERATION, "Cannot retain sync display terminal");
319 } else if ((result = keyboard_init()) != ASCIICHAT_OK) {
320 platform_close(g_sync_fd);
321 g_sync_fd = -1;
322 } else {
323 atomic_store(&g_sync_stop, false);
324 result = platform_thread_create(&g_sync_thread, "sync_collector", sync_collect_main, NULL);
325 if (result == ASCIICHAT_OK) {
326 g_sync_started = true;
327 } else {
329 platform_close(g_sync_fd);
330 g_sync_fd = -1;
331 result = SET_ERRNO(ERROR_THREAD, "Cannot start sync collector");
332 }
333 }
334 }
335 atomic_flag_clear(&g_sync_starting);
336 return result;
337}
338
339void ui_sync_stop(void) {
340 if (!g_sync_started)
341 return;
342 atomic_store(&g_sync_stop, true);
343 thread_join(&g_sync_thread, NULL);
345 platform_close(g_sync_fd);
346 g_sync_fd = -1;
347 for (int i = 0; i < 3; ++i) {
348 UNTRACKED_FREE(g_sync_samples[i].rows);
349 g_sync_samples[i].capacity = g_sync_samples[i].count = 0;
350 }
351 atomic_store(&g_sync_published, -1);
352 if (ui_sync_is_visible())
354 g_sync_started = false;
355}
356
361 return g_sync_fd;
362}
363
365 if (key == KEY_0) {
367 return KEY_NONE;
368 }
369 if (!ui_sync_is_visible())
370 return key;
371 // Leave sync selected until the replacement has a complete snapshot.
372 if (key == '=' || key == KEY_BACKTICK || key == KEY_QUESTION || key == ']' || key == 'o' || key == 'O' || key == 15)
373 return key;
374 if (key == KEY_ESCAPE) {
376 return KEY_NONE;
377 }
378 ui_viewport_t viewport = {.selected = (size_t)atomic_load(&g_sync_selected),
379 .page_size = (size_t)atomic_load(&g_sync_page_size)};
380 ui_viewport_key(&viewport, (size_t)atomic_load(&g_sync_total), viewport.page_size, key);
381 atomic_store(&g_sync_selected, (int)viewport.selected);
382 return key == 'q' || key == 3 ? key : KEY_NONE;
383}
384
385static const char *sync_color(log_color_t color) {
386 return g_sync_use_color ? ui_text_color_prefix(color) : "";
387}
388
389static const char *sync_rate_color(double rate) {
390 return sync_color(rate >= 60 ? LOG_COLOR_ERROR
391 : rate >= 10 ? LOG_COLOR_WARN
392 : rate > 0 ? LOG_COLOR_INFO
394}
395
396// Only the presentation worker writes this storage. Diagnostic rendering must
397// remain usable even when the allocator or another subsystem mutex is stuck.
398static char g_sync_output[SYNC_OUTPUT_BUFFER_SIZE];
399static size_t g_sync_output_used;
400
401static void sync_flush(void) {
402 if (g_sync_output_used) {
403 ui_controller_write(g_sync_fd, g_sync_output, g_sync_output_used);
404 g_sync_output_used = 0;
405 }
406}
407
408static void sync_write(const char *text, size_t length) {
409 if (length > sizeof(g_sync_output) - g_sync_output_used)
410 sync_flush();
411 if (length > sizeof(g_sync_output)) {
412 ui_controller_write(g_sync_fd, text, length);
413 return;
414 }
415 memcpy(g_sync_output + g_sync_output_used, text, length);
416 g_sync_output_used += length;
417}
418
419static void sync_write_line(const char *text, int columns, bool newline, int *measured_width) {
420 char output[TUI_SYNC_OUTPUT_BUFFER_SIZE];
421 size_t used = 0;
422 int visible = 0;
423 const char *prefix = "\033[0m";
424 memcpy(output, prefix, strlen(prefix));
425 used = strlen(prefix);
426 for (size_t i = 0; text[i] && used + 16 < sizeof(output);) {
427 if (text[i] == '\033' && text[i + 1] == '[') {
428 output[used++] = text[i++];
429 output[used++] = text[i++];
430 while (text[i] && !(text[i] >= '@' && text[i] <= '~'))
431 output[used++] = text[i++];
432 if (text[i])
433 output[used++] = text[i++];
434 } else {
435 if (!measured_width && visible >= columns - 1)
436 break;
437 int bytes = utf8_next_char_bytes(text + i, strlen(text + i));
438 if (bytes <= 0)
439 break;
440 int width = utf8_display_width_n(text + i, bytes);
441 if (!measured_width && visible + width > columns - 1)
442 break;
443 memcpy(output + used, text + i, bytes);
444 used += bytes;
445 i += bytes;
446 visible += width;
447 }
448 }
449 if (measured_width) {
450 if (visible > *measured_width)
451 *measured_width = visible;
452 return;
453 }
454 const char *suffix = newline ? "\033[0m\033[K\r\n" : "\033[0m\033[K";
455 memcpy(output + used, suffix, strlen(suffix));
456 used += strlen(suffix);
457 sync_write(output, used);
458}
459
460// Header/detail numbers share the log palette, including hexadecimal addresses.
461static void sync_color_numbers(const char *text, char *output, size_t capacity) {
462 ui_text_values(output, capacity, text);
463}
464
465static void sync_write_numbers(const char *text, int columns, int *measure) {
466 char output[TUI_SYNC_OUTPUT_BUFFER_SIZE];
467 sync_color_numbers(text, output, sizeof(output));
468 sync_write_line(output, columns, true, measure);
469}
470
471static void sync_separator(int columns, int *measure) {
472 if (measure)
473 return;
474 char line[TUI_SYNC_LINE_BUFFER_SIZE];
475 const char *rule = g_sync_use_utf8 ? "━" : "-";
476 size_t width = strlen(rule), used = 0;
477 used += snprintf(line, sizeof(line), "%s", sync_color(LOG_COLOR_DEBUG));
478 for (int col = 0; col < columns - 1 && used + width + 1 < sizeof(line); ++col) {
479 memcpy(line + used, rule, width);
480 used += width;
481 }
482 line[used] = '\0';
483 sync_write_line(line, columns, true, NULL);
484}
485
486void ui_sync_render(terminal_size_t size) {
488 terminal_capabilities_t caps = detect_terminal_capabilities(options_terminal_settings());
489 terminal_color_mode_t mode = GET_OPTION(color_mode);
490 g_sync_use_color = mode != TERM_COLOR_NONE && GET_OPTION(color) != COLOR_SETTING_FALSE && !GET_OPTION(strip_ansi) &&
491 (mode != TERM_COLOR_AUTO || caps.color_level != TERM_COLOR_NONE);
492 g_sync_use_utf8 = terminal_should_use_utf8(GET_OPTION(force_utf8));
493 // Copy only the visible page before terminal writes; the collector never
494 // waits for output, and the renderer never waits for collection.
496 size_t count = 0, total = 0, cycles = 0;
497 size_t mutexes = 0, conditions = 0, rwlocks = 0, atomics = 0;
498 uint64_t sampled = 0;
499 bool waits_complete = false, limited = false;
500 enum { header_rows = 4, detail_rows = 3, footer_rows = 1 };
501 const int fixed_rows = header_rows + detail_rows + footer_rows;
502 int name_width = (int)strlen("Name");
503 ui_layout_t layout;
504 if (ui_layout_measure(size, header_rows, detail_rows, footer_rows, &layout) != ASCIICHAT_OK)
505 return;
506 int per_page = layout.body_rows > 0 ? layout.body_rows : 1;
507 if (per_page > 128)
508 per_page = 128;
509 atomic_store(&g_sync_page_size, per_page);
510 int selected = atomic_load(&g_sync_selected);
511 int page = 0, pages = 1;
512 int slot = atomic_load(&g_sync_published);
513 if (slot >= 0) {
514 // Keep the last complete screen if collection is reusing this mailbox.
515 if (atomic_flag_test_and_set(&g_sync_samples[slot].busy)) {
516 return;
517 }
518 sync_sample_t *s = &g_sync_samples[slot];
519 total = s->count;
520 atomic_store(&g_sync_total, total > INT_MAX ? INT_MAX : (int)total);
521 if ((size_t)selected >= total) {
522 int previous = selected;
523 selected = total ? (int)total - 1 : 0;
524 // Do not overwrite a key received while the renderer copies a sample.
525 atomic_compare_exchange_strong(&g_sync_selected, &previous, selected);
526 }
527 pages = total ? (int)((total + per_page - 1) / per_page) : 1;
528 page = selected / per_page;
529 size_t first = (size_t)page * per_page;
530 count = total > first ? total - first : 0;
531 if (count > (size_t)per_page)
532 count = per_page;
533 if (count)
534 memcpy(rows, s->rows + first, count * sizeof(*rows));
535 for (size_t i = 0; i < total; ++i) {
536 cycles += s->rows[i].cycle;
537 const char *type = s->rows[i].type;
538 mutexes += !strcmp(type, "mutex");
539 conditions += !strcmp(type, "cond");
540 rwlocks += !strcmp(type, "rwlock");
541 atomics += !strncmp(type, "atomic", 6);
542 char label[TUI_SYNC_LABEL_BUFFER_SIZE];
543 sync_row_label(&s->rows[i], label, sizeof(label));
544 int width = (int)strlen(label);
545 if (width > name_width)
546 name_width = width;
547 }
548 sampled = s->sampled_ns;
549 waits_complete = s->waits_complete;
550 limited = s->limited;
551 atomic_flag_clear(&s->busy);
552 }
553 if (name_width > 30)
554 name_width = 30;
555 uint64_t now = time_get_ns();
556 selected %= per_page;
557 if ((size_t)selected >= count)
558 selected = count ? (int)count - 1 : 0;
559 char deadlock[TUI_SYNC_DEADLOCK_BUFFER_SIZE];
560 snprintf(deadlock, sizeof(deadlock), "%s%s%s", sync_color(LOG_COLOR_ERROR),
561 g_sync_use_utf8 ? "DEADLOCK ❌" : "DEADLOCK!", sync_color(LOG_COLOR_RESET));
562 int measured_width = 0;
563 for (int pass = 0; pass < 2; ++pass) {
564 int *measure = pass == 0 ? &measured_width : NULL;
566 if (!measure)
567 sync_write("\033[?2026h\033[H", 11);
568 snprintf(line, sizeof(line), "Sync primitives | %zu objects | page %d/%d | item %zu/%zu | age %.1fs", total,
569 page + 1, pages, count ? (size_t)page * per_page + selected + 1 : 0, total,
570 sampled ? (double)(now - sampled) / NS_PER_SEC_INT : 0.0);
571 sync_write_numbers(line, size.cols, measure);
572 snprintf(line, sizeof(line), "%s%s | M=%zu RW=%zu C=%zu A=%zu | %zu mutexes in wait cycles%s",
573 cycles ? sync_color(LOG_COLOR_ERROR) : "",
574 !sampled ? "Waiting for registry"
575 : now - sampled > NS_PER_SEC_INT ? "STALE: collection unavailable"
576 : "Live registered",
577 mutexes, rwlocks, conditions, atomics, cycles,
578 !waits_complete ? " | wait tracker busy"
579 : limited ? " | wait tracker capacity reached"
580 : "");
581 sync_write_numbers(line, size.cols, measure);
582 snprintf(line, sizeof(line), "%s %-8s %-*s %-12s %13s %6s %6s %7s %12s%s", sync_color(LOG_COLOR_GREY), "Type",
583 name_width, "Name", "State/value", "Total Actions", "L/s", "U/s", "Change/s", "Total Wait",
584 sync_color(LOG_COLOR_RESET));
585 sync_write_line(line, size.cols, true, measure);
586 sync_separator(size.cols, measure);
587 for (size_t i = 0; i < count; ++i) {
588 sync_row_t *r = &rows[i];
589 // Parent names may be media paths; keep the primitive visible in the table.
590 // The detail row identifies the source location and address.
592 sync_row_label(r, name, sizeof(name));
593 if (strlen(name) > (size_t)name_width) {
594 const char *suffix = strrchr(name, '.');
595 size_t suffix_bytes = suffix && strspn(suffix + 1, "0123456789") == strlen(suffix + 1) ? strlen(suffix) : 0;
596 if (suffix_bytes >= (size_t)name_width - 1)
597 suffix_bytes = 0;
598 size_t used = 0;
599 int columns = 0;
600 while (name[used]) {
601 int bytes = utf8_next_char_bytes(name + used, strlen(name + used));
602 if (bytes <= 0)
603 break;
604 int width = utf8_display_width_n(name + used, bytes);
605 if (columns + width > name_width - 1 - (int)suffix_bytes)
606 break;
607 used += bytes;
608 columns += width;
609 }
610 name[used++] = '~';
611 if (suffix_bytes) {
612 memmove(name + used, suffix, suffix_bytes);
613 used += suffix_bytes;
614 }
615 name[used] = '\0';
616 }
617 char state[TUI_SYNC_STATE_BUFFER_SIZE];
618 if (r->cycle)
619 snprintf(state, sizeof(state), "DEADLOCK");
620 else if (!strcmp(r->type, "mutex"))
621 snprintf(state, sizeof(state), "%s t=%" PRIxPTR " w=%u",
622 r->cycle ? "CYCLE"
623 : r->owner ? "HELD"
624 : "FREE",
625 r->owner, r->waiters);
626 else if (!strcmp(r->type, "rwlock"))
627 snprintf(state, sizeof(state), "W=%" PRIxPTR " R=%" PRIu64, r->owner, r->value);
628 else if (!strcmp(r->type, "cond"))
629 snprintf(state, sizeof(state), "waiting=%" PRIu64, r->value);
630 else
631 snprintf(state, sizeof(state), "0x%" PRIx64 " (%" PRIu64 ")", r->value, r->value);
632 // Prefer the complete hex value over a clipped redundant decimal copy.
633 if (!strncmp(r->type, "atomic", 6) && strlen(state) > 12)
634 snprintf(state, sizeof(state), "0x%" PRIx64, r->value);
635 char padded_state[TUI_SYNC_PADDED_STATE_BUFFER_SIZE];
636 if (r->cycle) {
637 int width = utf8_display_width(g_sync_use_utf8 ? "DEADLOCK ❌" : "DEADLOCK!");
638 snprintf(padded_state, sizeof(padded_state), "%s%*s", deadlock, 12 - width, "");
639 } else {
640 snprintf(padded_state, sizeof(padded_state), "%-12.12s", state);
641 }
643 format_number_compact(r->actions, plain_actions, sizeof(plain_actions));
644 char padded_actions[TUI_SYNC_PADDED_ACTIONS_BUFFER_SIZE];
645 snprintf(padded_actions, sizeof(padded_actions), "%13s", plain_actions);
646 sync_color_numbers(padded_actions, actions, sizeof(actions));
647 char changes[TUI_SYNC_CHANGES_BUFFER_SIZE];
648 if (!strncmp(r->type, "atomic", 6))
649 snprintf(changes, sizeof(changes), "%s%7.1f", sync_rate_color(r->change_rate), r->change_rate);
650 else
651 snprintf(changes, sizeof(changes), "%s%7s", sync_color(LOG_COLOR_RESET), "-");
652 char wait[TIME_DURATION_BUFFER_SIZE] = "-";
653 if (!strcmp(r->type, "cond")) {
654 if (r->wait_known)
655 time_pretty(r->wait_ns, -1, wait, sizeof(wait));
656 else
657 snprintf(wait, sizeof(wait), "busy");
658 }
660 snprintf(padded_wait, sizeof(padded_wait), "%12s", wait);
661 sync_color_numbers(padded_wait, colored_wait, sizeof(colored_wait));
663 snprintf(locks, sizeof(locks), "%s%6.1f%s", sync_rate_color(r->lock_rate), r->lock_rate,
664 sync_color(LOG_COLOR_RESET));
665 snprintf(unlocks, sizeof(unlocks), "%s%6.1f%s", sync_rate_color(r->unlock_rate), r->unlock_rate,
666 sync_color(LOG_COLOR_RESET));
667 const ui_table_column_t columns[] = {
668 {"Type", 8, UI_ALIGN_LEFT, false}, {"Name", name_width, UI_ALIGN_LEFT, false},
669 {"State/value", 13, UI_ALIGN_LEFT, false}, {"Total Actions", 13, UI_ALIGN_RIGHT, false},
670 {"L/s", 6, UI_ALIGN_RIGHT, false}, {"U/s", 6, UI_ALIGN_RIGHT, false},
671 {"Change/s", 7, UI_ALIGN_RIGHT, false}, {"Total Wait", 12, UI_ALIGN_RIGHT, false}};
672 const char *cells[] = {r->type, name, padded_state, actions, locks, unlocks, changes, colored_wait};
673 ui_table_row(line, sizeof(line), columns, cells, 8, name_width + 83, true, (int)i == selected);
674 sync_write_line(line, size.cols, true, measure);
675 }
676 if (!measure) {
677 // Clear only unused table rows, leaving the live footer in place.
678 for (int row = header_rows + (int)count + 1; row < size.rows - 3; ++row) {
679 snprintf(line, sizeof(line), "\033[%d;1H\033[K", row);
680 sync_write(line, strlen(line));
681 }
682 snprintf(line, sizeof(line), "\033[%d;1H", size.rows > 3 ? size.rows - 3 : 1);
683 sync_write(line, strlen(line));
684 }
685 sync_separator(size.cols, measure);
686 if (count) {
687 sync_row_t *r = &rows[selected];
688 snprintf(line, sizeof(line), "%d @0x%" PRIxPTR, r->line, r->key);
689 char numbers[TUI_SYNC_NUMBERS_BUFFER_SIZE];
690 sync_color_numbers(line, numbers, sizeof(numbers));
692 sync_row_label(r, full_name, sizeof(full_name));
693 sync_color_numbers(full_name, colored_name, sizeof(colored_name));
694 snprintf(line, sizeof(line), "%s%s%s:%s | %s", sync_color(LOG_COLOR_DEV), r->file, sync_color(LOG_COLOR_RESET),
695 numbers, colored_name);
696 sync_write_line(line, size.cols, true, measure);
697 snprintf(line, sizeof(line),
698 "owner=0x%" PRIxPTR " waiter=0x%" PRIxPTR " | value=%" PRIu64 " | last operation %.2fs ago%s%s",
699 r->owner, r->waiter, r->value, r->last_ns ? (double)(now - r->last_ns) / NS_PER_SEC_INT : 0.0,
700 r->cycle ? " " : "", r->cycle ? deadlock : "");
701 sync_write_numbers(line, size.cols, measure);
702 } else if (!measure) {
703 snprintf(line, sizeof(line), "\033[%d;1H\033[K\033[%d;1H\033[K", size.rows - 2, size.rows - 1);
704 sync_write(line, strlen(line));
705 }
706 snprintf(line, sizeof(line), "\033[%d;1H", size.rows);
707 if (!measure)
708 sync_write(line, strlen(line));
709 snprintf(line, sizeof(line),
710 "%s0%s/%sEsc%s close | %s?%s help | %sLeft%s/%sRight%s page | %sUp%s/%sDown%s select | "
711 "%sHome%s/%sEnd%s first/last",
712 sync_color(LOG_COLOR_FATAL), sync_color(LOG_COLOR_RESET), sync_color(LOG_COLOR_WARN),
713 sync_color(LOG_COLOR_RESET), sync_color(LOG_COLOR_WARN), sync_color(LOG_COLOR_RESET),
714 sync_color(LOG_COLOR_WARN), sync_color(LOG_COLOR_RESET), sync_color(LOG_COLOR_WARN),
715 sync_color(LOG_COLOR_RESET), sync_color(LOG_COLOR_WARN), sync_color(LOG_COLOR_RESET),
716 sync_color(LOG_COLOR_WARN), sync_color(LOG_COLOR_RESET), sync_color(LOG_COLOR_WARN),
717 sync_color(LOG_COLOR_RESET), sync_color(LOG_COLOR_WARN), sync_color(LOG_COLOR_RESET));
718 sync_write_line(line, size.cols, false, measure);
719 if (measure) {
720 terminal_size_t minimum = {.cols = measured_width + 1, .rows = fixed_rows + 1};
721 if (ui_too_small(size, minimum)) {
723 size_t length = ui_too_small_format(output, sizeof(output), size, minimum);
724 ui_controller_write(g_sync_fd, output, length);
725 return;
726 }
727 }
728 }
729 sync_write("\033[?2026l", 8);
730 sync_flush();
731}
732
733#else
735 (void)fd;
736 return ASCIICHAT_OK;
737}
738void ui_sync_stop(void) {}
739bool ui_sync_is_visible(void) {
740 return false;
741}
743 return key;
744}
745void ui_sync_render(terminal_size_t size) {
746 (void)size;
747}
748int ui_sync_output_fd(void) {
749 return -1;
750}
751#endif
#define SET_ERRNO(code,...)
#define SET_ERRNO_SYS(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
int color_mode
Definition fft.c:25
char cells[(512 *256 *2)]
Definition fft.c:21
audio_visualization_source_t source
Definition fft.c:24
unsigned int width
Definition fft.c:23
Buffered terminal frame rendering.
#define UNTRACKED_FREE(ptr)
Definition common.h:414
unsigned long long uint64_t
Definition common.h:63
#define UNTRACKED_MALLOC(size, cast)
Definition common.h:405
asciichat_error_t named_registry_try_snapshot(size_t capacity, size_t *required, bool *completed, named_snapshot_fn copy, void *data)
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_FILE_OPERATION
@ ERROR_THREAD
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:147
@ LOG_COLOR_RESET
Definition log/log.h:155
@ LOG_COLOR_GREY
Definition log/log.h:154
@ LOG_COLOR_FATAL
Definition log/log.h:153
@ LOG_COLOR_ERROR
Definition log/log.h:152
@ LOG_COLOR_DEV
Definition log/log.h:148
@ LOG_COLOR_DEBUG
Definition log/log.h:149
@ LOG_COLOR_INFO
Definition log/log.h:150
@ LOG_COLOR_WARN
Definition log/log.h:151
#define NS_PER_SEC_INT
Definition time.h:169
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
#define NS_PER_MS_INT
Definition time.h:168
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
#define options_terminal_settings()
Definition options.h:1296
@ COLOR_SETTING_FALSE
Force colors OFF: disable all colors and logging colors.
Definition options.h:249
asciichat_error_t keyboard_init(void)
Initialize keyboard input system.
int platform_dup(int fd)
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
atomic_t read_lock_count
Number of threads holding read locks (thread-safe atomic).
Definition rwlock.h:71
@ KEY_BACKTICK
'`' key - print lock state (debug builds)
Definition keyboard.h:84
@ KEY_ESCAPE
Escape key (ESC).
Definition keyboard.h:55
@ KEY_0
'0' key - toggle sync debug screen
Definition keyboard.h:75
@ KEY_NONE
No key pressed or no input available.
Definition keyboard.h:54
@ KEY_QUESTION
'?' key - show help screen
Definition keyboard.h:83
int utf8_display_width_n(const char *str, size_t max_bytes)
Calculate terminal display width of a UTF-8 string up to byte limit.
Definition utf8.c:102
int colored_string_set_output_fd(int fd)
Definition string.c:274
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:98
int utf8_next_char_bytes(const char *str, size_t max_bytes)
Get byte length of next UTF-8 character.
Definition utf8.c:177
int keyboard_help_previous_interactive(void)
🆘 Interactive keyboard help overlay for session keyboard shortcuts
bool mutex_stack_try_snapshot(mutex_wait_snapshot_t *waits, size_t capacity, size_t *count, bool *limited)
Definition mutex.c:507
Per-thread mutex lock stack for deadlock detection.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🔤 String Manipulation and Shell Escaping Utilities
For mutex wait snapshot, this is the live record that ties mutex_key, thread_key, since_ns,...
Definition mutex.h:111
uintptr_t thread_key
Definition mutex.h:113
Read-write lock type (POSIX: pthread_rwlock_t with debug tracking).
Definition rwlock.h:63
char type[24]
Definition sync.c:68
uint64_t actions
Definition sync.c:66
double unlock_rate
Definition sync.c:67
unsigned waiters
Definition sync.c:70
char name[256]
Definition sync.c:68
double total_score
Definition sync.c:67
bool cycle
Definition sync.c:71
uint64_t changes
Definition sync.c:66
uintptr_t key
Definition sync.c:65
int line
Definition sync.c:69
char file[160]
Definition sync.c:68
uint64_t generation
Definition sync.c:66
uint64_t wait_ns
Definition sync.c:66
uint64_t value
Definition sync.c:66
uintptr_t owner
Definition sync.c:65
uint64_t locks
Definition sync.c:66
double change_rate
Definition sync.c:67
uint64_t unlocks
Definition sync.c:66
uint64_t last_ns
Definition sync.c:66
uintptr_t waiter
Definition sync.c:65
double lock_rate
Definition sync.c:67
bool wait_known
Definition sync.c:71
sync row keeps the values that move through the subsystem: key, owner, waiter, generation....
Definition sync.c:81
size_t capacity
Definition sync.c:84
bool waits_complete
Definition sync.c:86
size_t count
Definition sync.c:84
sync_row_t * rows
Definition sync.c:83
bool limited
Definition sync.c:86
atomic_flag busy
Definition sync.c:82
uint64_t sampled_ns
Definition sync.c:85
int body_rows
Definition ui/layout.h:5
For ui viewport, this is the live record that ties selected, first_visible, page_size together....
Definition viewport.h:10
size_t selected
Definition viewport.h:11
size_t page_size
Definition viewport.h:11
#define TUI_SYNC_STATE_BUFFER_SIZE
Definition sync.c:12
#define TUI_SYNC_OUTPUT_BUFFER_SIZE
Definition sync.c:6
#define TUI_SYNC_FULL_NAME_BUFFER_SIZE
Definition sync.c:23
keyboard_key_t ui_sync_handle_key(keyboard_key_t key)
Definition sync.c:364
#define TUI_SYNC_NAME_BUFFER_SIZE
Definition sync.c:3
void ui_sync_stop(void)
Definition sync.c:339
#define SYNC_OUTPUT_BUFFER_SIZE
Definition sync.c:30
#define TUI_SYNC_DEADLOCK_BUFFER_SIZE
Definition sync.c:9
#define TUI_SYNC_FILE_BUFFER_SIZE
Definition sync.c:5
#define TUI_SYNC_ACTIONS_BUFFER_SIZE
Definition sync.c:15
#define TUI_SYNC_CHANGES_BUFFER_SIZE
Definition sync.c:17
asciichat_error_t ui_sync_start(int fd)
Definition sync.c:311
#define TUI_SYNC_PLAIN_ACTIONS_BUFFER_SIZE
Definition sync.c:14
#define TUI_SYNC_PADDED_ACTIONS_BUFFER_SIZE
Definition sync.c:16
#define SYNC_WAIT_SNAPSHOT_CAPACITY
Definition sync.c:28
#define TUI_SYNC_NAME_EXTENDED_BUFFER_SIZE
Definition sync.c:11
void ui_sync_render(terminal_size_t size)
Definition sync.c:486
#define SYNC_ROW_CAPACITY
Definition sync.c:29
#define TUI_SYNC_LOCKS_BUFFER_SIZE
Definition sync.c:20
bool ui_sync_is_visible(void)
Definition sync.c:357
#define TUI_SYNC_LABEL_BUFFER_SIZE
Definition sync.c:8
#define TUI_SYNC_COLORED_WAIT_BUFFER_SIZE
Definition sync.c:19
#define TUI_SYNC_COLORED_NAME_BUFFER_SIZE
Definition sync.c:24
#define TUI_SYNC_TYPE_BUFFER_SIZE
Definition sync.c:4
#define TUI_SYNC_PADDED_WAIT_BUFFER_SIZE
Definition sync.c:18
#define TUI_SYNC_LINE_BUFFER_SIZE
Definition sync.c:7
#define TUI_SYNC_NUMBERS_BUFFER_SIZE
Definition sync.c:22
#define TUI_SYNC_UNLOCKS_BUFFER_SIZE
Definition sync.c:21
#define TUI_SYNC_OUTPUT_SHORT_BUFFER_SIZE
Definition sync.c:25
int ui_sync_output_fd(void)
Definition sync.c:360
#define TUI_SYNC_PADDED_STATE_BUFFER_SIZE
Definition sync.c:13
#define TUI_SYNC_LINE_SHORT_BUFFER_SIZE
Definition sync.c:10
size_t ui_table_row(char *out, size_t cap, const ui_table_column_t *cols, const char *const *cells, size_t count, int width, bool selectable, bool selected)
When code needs to work with ui table, it keeps columns, column_count, row_count, viewport here inste...
Definition table.c:18
const char * ui_text_color_prefix(log_color_t color)
Definition text.c:18
void ui_text_values(char *out, size_t cap, const char *text)
Definition text.c:142
@ UI_ALIGN_RIGHT
Definition text.h:9
@ UI_ALIGN_LEFT
Definition text.h:9
⏱️ High-precision timing utilities using sokol_time.h and uthash
size_t ui_too_small_format(char *output, size_t capacity, terminal_size_t actual, terminal_size_t minimum)
Definition too_small.c:14
bool ui_too_small(terminal_size_t actual, terminal_size_t minimum)
Definition too_small.c:10
void ui_controller_select_interactive(int screen)
int ui_controller_current_screen(void)
int ui_controller_interactive_screen(void)
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
@ UI_SCREEN_SYNC
@ UI_SCREEN_HELP
void ui_input_poll_sync(void)
Definition tui/input.c:282
asciichat_error_t ui_layout_measure(terminal_size_t size, int header, int detail, int footer, ui_layout_t *out)
Definition ui/layout.c:3
🔤 UTF-8 Encoding and Decoding Utilities
void format_number_compact(uint64_t value, char *out, size_t out_capacity)
Definition util/format.c:46
📊 String Formatting Utilities
bool ui_viewport_key(ui_viewport_t *v, size_t count, size_t rows, keyboard_key_t key)
Definition viewport.c:38