skip to content
==> src/programs.c <==
1/* ui.rs's "2 programs" section, ported. */
2#include "mesh/top_ui_programs.h"
3
4#include <math.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8
9typedef struct {
10    const char *name;
11    uint64_t *h;
12    size_t n, cap;
13} name_hashes;
14
15typedef struct {
16    name_hashes *v;
17    size_t n, cap;
18} hash_map;
19
20static void hashes_add(mt_frame *f, hash_map *m, const char *name, uint64_t h) {
21    name_hashes *e = NULL;
22    for (size_t i = 0; i < m->n && !e; i++)
23        if (strcmp(m->v[i].name, name) == 0) e = &m->v[i];
24    if (!e) {
25        if (m->n == m->cap) {
26            size_t cap = m->cap ? m->cap * 2 : 8;
27            m->v = tui_arena_grow(f->a, m->v, m->cap * sizeof *m->v, cap * sizeof *m->v);
28            m->cap = cap;
29        }
30        e = &m->v[m->n++];
31        memset(e, 0, sizeof *e);
32        e->name = name;
33    }
34    if (e->n == e->cap) {
35        size_t cap = e->cap ? e->cap * 2 : 4;
36        e->h = tui_arena_grow(f->a, e->h, e->cap * sizeof *e->h, cap * sizeof *e->h);
37        e->cap = cap;
38    }
39    e->h[e->n++] = h;
40}
41
42tui_table_cell mt_place_cell(mt_frame *f, const mt_section *p, uint32_t sched) {
43    mt_place pl;
44    if (!mt_place_of(p, &pl)) {
45        if (!(sched & MESH_PS_KNOWN)) return mt_cell_styled(f, "-", tui_fg(MT_FAINT));
46        const char *at;
47        if (sched & MESH_PS_ALL) {
48            at = "all";
49        } else {
50            char b[32];
51            size_t n = 0;
52            b[0] = 0;
53            for (uint32_t k = 0; k < 8; k++)
54                if (sched >> k & 1) n += (size_t)snprintf(b + n, sizeof b - n, "%s%u", n ? "," : "", k);
55            at = n ? tui_arena_strdup(f->a, b) : "none";
56        }
57        if (sched & MESH_PS_MIGRATING) {
58            uint32_t from = sched >> MESH_PS_FROM_SHIFT & 0xff;
59            return mt_cell_styled(f, MT_FMT(f, "%u>%s", from ? from - 1 : 0, at), tui_fg_bold(MT_WARN));
60        }
61        if (sched & MESH_PS_PINNED) return mt_cell_styled(f, MT_FMT(f, "pin %s", at), tui_fg_bold(MT_ACCENT));
62        return mt_cell_styled(f, at, tui_fg(MT_DIM));
63    }
64    switch (pl.kind) {
65    case MT_PLACE_LOCAL: return mt_cell_styled(f, "local", tui_fg(MT_DIM));
66    case MT_PLACE_PIN: return mt_cell_styled(f, MT_FMT(f, "pin %u", pl.pin), tui_fg_bold(MT_MACHINE[pl.pin % 8]));
67    default: return mt_cell_styled(f, "split", tui_fg_bold(MT_ACCENT));
68    }
69}
70
71static const char *quantile(mt_frame *f, const mt_rec *recs, size_t n, double q) {
72    double v;
73    return mt_quantile_us(recs, n, q, &v) ? mt_ff(f, v, 1) : "-";
74}
75
76static void draw_prog_table(mt_frame *f, tui_rect area, const mt_app *app, const mt_machine *m, bool focused, const hash_map *hashes,
77                            mt_instant now) {
78    /* The CPU executors: native worker processes and wasm lanes, their CPU and memory. */
79    const char *workers = "";
80    if (m->has_snap) {
81        const mt_snapshot *s = &m->snap;
82        uint32_t wk = mt_snapshot_host_u32(s, MESH_S_WORKERS), wl = mt_snapshot_host_u32(s, MESH_S_WASM_LANES);
83        if (wk + wl > 0)
84            workers = MT_FMT(f, "%s  workers %u, wasm lanes %u: %s%% CPU, %s", m->endpoint, wk, wl,
85                             mt_ff(f, 100.0f * mt_snapshot_host_f32(s, MESH_S_EXEC_CPU), 0), mt_fmt_bytes(f, mt_snapshot_host_u64(s, MESH_S_EXEC_RSS)));
86    }
87    tui_color color;
88    tui_line title = mt_machine_title(f, m, focused, *workers ? workers : m->endpoint, &color);
89    tui_block block = mt_panel(f, title, color);
90    tui_rect inner = tui_block_inner(&block, area);
91    mt_block(f, &block, area);
92    if (!m->has_snap) {
93        mt_no_data(f, inner, m);
94        return;
95    }
96    const mt_snapshot *s = &m->snap;
97    if (s->nsections == 0) {
98        mt_par_line(f, tui_line_span(f->a, mt_label("no programs loaded: press L")), inner);
99        return;
100    }
101    mt_rec *fresh;
102    size_t nfresh = mt_machine_fresh_trace(m, now, &fresh);
103    /* Narrow panes drop the last columns rather than squeezing the hash. */
104    size_t cols = inner.width >= 150 ? 18 : inner.width >= 120 ? 14 : 12;
105    static const char *const hnames[18] = {"slot", "name", "kind", "state", "place", "nodes", "tasks/s", "p50 µs", "p95 µs",
106                                           "regs/pid", "cpu%", "rss", "view B", "ptx", "warps", "rst/cr", "loads", "policy"};
107    tui_table_row header = tui_table_row_new();
108    for (size_t i = 0; i < cols; i++) tui_table_row_push(f->a, &header, mt_cell_styled(f, hnames[i], tui_fg(MT_DIM)));
109    mt_rows rows = {0};
110    mt_rec *recs = malloc((nfresh ? nfresh : 1) * sizeof *recs);
111    if (!recs) abort();
112    for (size_t pi = 0; pi < s->nsections; pi++) {
113        const mt_section *p = &s->sections[pi];
114        const char *name = mt_sec_name(f, p);
115        uint32_t slot = mt_section_slot(p);
116        size_t nr = 0;
117        for (size_t i = 0; i < nfresh; i++)
118            if (fresh[i].slot == slot) recs[nr++] = fresh[i];
119        uint64_t hash = mt_section_ptx_hash(p);
120        bool differs = false;
121        for (size_t i = 0; i < hashes->n; i++)
122            if (strcmp(hashes->v[i].name, name) == 0)
123                for (size_t k = 0; k < hashes->v[i].n; k++) differs |= hashes->v[i].h[k] != hash;
124        /* A GPU node's warps and, if any, its shared bytes; the CPU executors ignore the shape. */
125        uint32_t shared = mt_get_u32(mt_section_ext(p), MESH_PX_BYTES, MESH_PX_SHARED);
126        uint32_t warps = mt_section_pt(p, MESH_PT_WARPS);
127        const char *sms = shared == 0 ? MT_FMT(f, "%u", warps) : MT_FMT(f, "%u +%uK", warps, shared / 1024 + (shared % 1024 != 0));
128        uint32_t fault = mt_section_p(p, MESH_P_FAULT);
129        tui_table_cell state = fault != 0 ? mt_cell_styled(f, MT_FMT(f, "%s f%u", mt_section_state_name(p), fault & 0xff), tui_fg_bold(MT_BAD))
130                                          : mt_cell_styled(f, mt_section_state_name(p), mt_state_style(p));
131        bool native = mt_section_native(p);
132        tui_table_cell dash = mt_cell_styled(f, "-", tui_fg(MT_FAINT));
133        uint32_t exec = mt_get_u32(mt_section_ext(p), MESH_PX_BYTES, MESH_PX_EXEC);
134        tui_table_cell kind;
135        if (exec == MESH_BACKEND_NATIVE || exec == MESH_BACKEND_WASM) {
136            kind = mt_cell_styled(f, MESH_BACKENDS[exec], tui_fg(MT_ACCENT));
137        } else {
138            uint32_t bk = mesh_backend_kind(mt_snapshot_head(s, MESH_H_BACKEND));
139            kind = mt_cell_styled(f, bk < 4 ? MESH_BACKENDS[bk] : "?", tui_fg(MT_DIM));
140        }
141        /* CPU executors: CPU % of one core and resident bytes (pload PL_HOST). */
142        size_t pl = mesh_pload(slot);
143        bool host = (mt_snapshot_host_u32(s, pl + MESH_PL_FLAGS) & MESH_PLF_HOST) != 0;
144        uint32_t pid = mt_section_p(p, MESH_P_PID), cr = mt_section_p(p, MESH_P_CRASHES), spawns = mt_section_p(p, MESH_P_SPAWNS);
145        uint32_t pol = mt_section_pt(p, MESH_PT_POLICY);
146        tui_table_cell c[18] = {
147            mt_cell_str(f, MT_FMT(f, "%u", slot)),
148            mt_cell_span(f, mt_fgb(name, mt_prog_color(name, slot, 1.0f))),
149            kind,
150            state,
151            mt_place_cell(f, p, mt_snapshot_host_u32(s, mesh_placed(slot))),
152            mt_cell_str(f, MT_FMT(f, "%u/%u", mt_section_p(p, MESH_P_ONLINE), mt_section_pt(p, MESH_PT_NODES))),
153            mt_cell_styled(f, mt_fmt_rate(f, mt_snapshot_rate(s, slot)), tui_fg(MT_WARN)),
154            mt_cell_str(f, quantile(f, recs, nr, 0.5)),
155            mt_cell_str(f, quantile(f, recs, nr, 0.95)),
156            native ? (pid == 0 ? dash : mt_cell_str(f, MT_FMT(f, "%u", pid))) : mt_cell_str(f, MT_FMT(f, "%u", mt_section_pt(p, MESH_PT_REGS))),
157            host ? mt_cell_str(f, mt_ff(f, 100.0f * mt_snapshot_host_f32(s, pl + MESH_PL_CPU), 0)) : dash,
158            host ? mt_cell_str(f, mt_fmt_bytes(f, mt_snapshot_host_u64(s, pl + MESH_PL_RSS))) : dash,
159            mt_cell_str(f, MT_FMT(f, "%u", mt_section_pt(p, MESH_PT_VIEW_BYTES))),
160            mt_cell_styled(f, MT_FMT(f, "%08llx", (unsigned long long)(hash >> 32)), tui_fg(differs ? MT_WARN : MT_DIM)),
161            native || exec == MESH_BACKEND_WASM ? dash : mt_cell_str(f, sms),
162            native ? mt_cell_styled(f, MT_FMT(f, "%u/%u", spawns ? spawns - 1 : 0, cr), tui_fg(cr > 0 ? MT_BAD : MT_DIM)) : dash,
163            mt_cell_str(f, MT_FMT(f, "%u", mt_section_pt(p, MESH_PT_LOADS))),
164            mt_cell_str(f, pol < 4 ? MESH_POLICIES[pol] : "?"),
165        };
166        mt_rows_push(f, &rows, mt_row(f, c, cols));
167    }
168    free(recs);
169    free(fresh);
170    static const uint16_t wv[18] = {4, 15, 6, 9, 7, 5, 8, 7, 7, 8, 5, 7, 7, 8, 6, 6, 5, 10};
171    tui_constraint widths[18];
172    for (size_t i = 0; i < cols; i++) widths[i] = TUI_LENGTH(wv[i]);
173    tui_table t = tui_table_new(rows.rows, rows.n, widths, cols);
174    t.header = &header;
175    t.column_spacing = 1;
176    t.row_highlight_style = tui_style_add(tui_style_bg(tui_style_new(), TUI_RGB(40, 52, 80)), TUI_BOLD);
177    t.highlight_symbol = tui_text_from_str(f->a, "▶");
178    tui_table_state st = {0};
179    if (focused) {
180        st.has_selected = true;
181        st.selected = app->sel < s->nsections - 1 ? app->sel : s->nsections - 1;
182    }
183    tui_table_render(f->a, &t, inner, f->buf, &st);
184}
185
186/* The selected program's counters, manifest fields and a hexdump of its snap bytes. */
187static void draw_view_pane(mt_frame *f, tui_rect area, const mt_app *app) {
188    const mt_machine *m = mt_app_focused(app);
189    const mt_section *p = mt_app_selected(app);
190    if (!p) {
191        tui_block b = mt_panel(f, tui_line_span(f->a, mt_label(" view ")), MT_FAINT);
192        tui_rect inner = tui_block_inner(&b, area);
193        mt_block(f, &b, area);
194        mt_par_line(f, tui_line_span(f->a, mt_label("no program selected")), inner);
195        return;
196    }
197    const char *name = mt_sec_name(f, p);
198    uint32_t slot = mt_section_slot(p);
199    size_t vlen = mt_section_view_len(p);
200    tui_line title = MT_LINE(f, mt_label(" view "), mt_fgb(name, mt_prog_color(name, slot, 1.0f)),
201                             mt_label(MT_FMT(f, " %zu of %u B ", vlen, mt_section_pt(p, MESH_PT_VIEW_BYTES))));
202    tui_block b = mt_panel(f, title, MT_FAINT);
203    tui_rect inner = tui_block_inner(&b, area);
204    mt_block(f, &b, area);
205    mt_lines lines = {0};
206    uint32_t fault = mt_section_p(p, MESH_P_FAULT);
207    if (fault != 0)
208        mt_lines_push(f, &lines,
209                      tui_line_span(f->a, mt_styled(MT_FMT(f, " fault %u detail 0x%x ", fault & 0xff, fault >> 8), mt_fg_bg_bold(TUI_WHITE, MT_BAD))));
210    tui_line cs = tui_line_empty();
211    for (size_t i = 0; i < MESH_COUNTERS; i++) {
212        char cn[3 * MESH_NAME_BYTES + 1];
213        mt_section_counter_name(p, i, cn);
214        if (!*cn) continue;
215        tui_line_push(f->a, &cs, mt_label(MT_FMT(f, "%s ", cn)));
216        tui_line_push(f->a, &cs, mt_val(MT_FMT(f, "%u", mt_section_counter(p, i))));
217        tui_line_push(f->a, &cs, mt_label(MT_FMT(f, " (%s/s)  ", mt_fmt_rate(f, mt_machine_counter_rate(m, slot, i)))));
218    }
219    tui_line_push(f->a, &cs, mt_label("done "));
220    tui_line_push(f->a, &cs, mt_val(MT_FMT(f, "%u", mt_section_p(p, MESH_P_DONE))));
221    mt_lines_push(f, &lines, cs);
222    mt_field fds[MESH_MAX_FIELDS];
223    size_t nf = mt_section_fields(p, fds);
224    for (size_t i = 0; i < nf; i++) {
225        const char *ty = fds[i].ty < 5 ? MESH_FIELD_TYPES[fds[i].ty] : "?";
226        char v[128];
227        mt_section_field_value(p, &fds[i], v, sizeof v);
228        mt_lines_push(f, &lines,
229                      MT_LINE(f, mt_fgs(mt_pad(f, fds[i].name, 19, '<'), MT_ACCENT),
230                              mt_label(MT_FMT(f, " %s @0x%04zx ", mt_pad(f, ty, 3, '<'), fds[i].off)), mt_val(tui_arena_strdup(f->a, v))));
231    }
232    mt_lines_push(f, &lines, tui_line_span(f->a, mt_label("── snap bytes ──")));
233    size_t left = inner.height > lines.n ? inner.height - lines.n : 0;
234    /* 16 bytes a row when the pane is wide enough for hex + ascii, else 8. */
235    size_t per = inner.width >= 5 + 48 + 16 ? 16 : 8;
236    const uint8_t *v = mt_section_view(p);
237    for (size_t row = 0; row * per < vlen && row < left; row++) {
238        size_t n = vlen - row * per < per ? vlen - row * per : per;
239        char hex[16 * 3 + 1], ascii[17];
240        for (size_t i = 0; i < n; i++) {
241            uint8_t c = v[row * per + i];
242            snprintf(hex + 3 * i, 4, "%02x ", c);
243            ascii[i] = c > 0x20 && c < 0x7f ? (char)c : '.';
244        }
245        hex[3 * n] = 0;
246        ascii[n] = 0;
247        mt_lines_push(f, &lines,
248                      MT_LINE(f, mt_label(MT_FMT(f, "%04zx ", row * per)), mt_fgs(mt_pad(f, hex, (int)(3 * per), '<'), MT_FG),
249                              mt_fgs(tui_arena_strdup(f->a, ascii), MT_DIM)));
250    }
251    mt_par(f, mt_text(f, &lines), inner);
252}
253
254typedef struct {
255    uint32_t slot, node;
256    float *lane;
257} lane_t;
258
259static int lane_cmp(const void *a, const void *b) {
260    const lane_t *x = a, *y = b;
261    if (x->slot != y->slot) return x->slot < y->slot ? -1 : 1;
262    return x->node < y->node ? -1 : x->node > y->node;
263}
264
265/* Per (program, node) lanes over the last MT_FLAME_NS of trace: each cell's glyph height is the
266 * busy fraction of its time slice, its colour the program. */
267static void draw_flame(mt_frame *f, tui_rect area, const mt_machine *m, mt_instant now) {
268    mt_flame_rec *recs;
269    size_t nrecs = mt_machine_flame(m, now, MT_FLAME_NS, &recs);
270    tui_line title = MT_LINE(f, mt_label(" trace "), mt_label(MT_FMT(f, "last %u ms, %zu tasks ", MT_FLAME_NS / 1000000, nrecs)));
271    tui_block b = mt_panel(f, title, MT_FAINT);
272    tui_rect inner = tui_block_inner(&b, area);
273    mt_block(f, &b, area);
274    if (m->trace_err) {
275        tui_paragraph p = tui_paragraph_new(tui_text_line(f->a, tui_line_span(f->a, mt_fgs(MT_FMT(f, "trace: %s", m->trace_err), MT_BAD))));
276        p.wrap = TUI_WRAP_TRIM;
277        tui_paragraph_render(f->a, &p, inner, f->buf);
278        free(recs);
279        return;
280    }
281    enum { LABEL_W = 11 };
282    if (nrecs == 0 || inner.width <= LABEL_W + 2 || inner.height < 2) {
283        mt_par_line(f, tui_line_span(f->a, mt_label("no recent tasks")), inner);
284        free(recs);
285        return;
286    }
287    size_t w = (size_t)(inner.width - LABEL_W);
288    double cell_ns = (double)MT_FLAME_NS / (double)w;
289    lane_t *lanes = tui_arena_alloc(f->a, nrecs * sizeof *lanes);
290    size_t nl = 0;
291    for (size_t i = 0; i < nrecs; i++) {
292        const mt_rec *r = &recs[i].rec;
293        double end_ago = (double)recs[i].age;
294        double start_ago = end_ago + (double)mt_rec_ns(r);
295        if (start_ago > (double)MT_FLAME_NS) start_ago = (double)MT_FLAME_NS;
296        lane_t *lane = NULL;
297        for (size_t k = 0; k < nl && !lane; k++)
298            if (lanes[k].slot == r->slot && lanes[k].node == r->node) lane = &lanes[k];
299        if (!lane) {
300            lane = &lanes[nl++];
301            lane->slot = r->slot;
302            lane->node = r->node;
303            lane->lane = tui_arena_alloc(f->a, w * sizeof(float));
304        }
305        /* Time t ago is at x = (FLAME_NS - t) / cell_ns; each cell sums its busy fraction. */
306        double x0 = ((double)MT_FLAME_NS - start_ago) / cell_ns, x1 = ((double)MT_FLAME_NS - end_ago) / cell_ns;
307        size_t x = (size_t)mt_sat_u64(floor(x0));
308        while ((double)x < x1 && x < w) {
309            lane->lane[x] += (float)fmax(fmin(x1, (double)x + 1.0) - fmax(x0, (double)x), 0.0);
310            x++;
311        }
312    }
313    free(recs);
314    qsort(lanes, nl, sizeof *lanes, lane_cmp);
315    static const char *const LEVELS[8] = {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"};
316    size_t rows = (size_t)(inner.height - 1);
317    /* Lanes that do not fit leave their last row to a "+N lanes" note. */
318    size_t shown = nl > rows ? rows - 1 : rows;
319    for (size_t y = 0; y < nl && y < shown; y++) {
320        const mt_section *sec = m->has_snap ? mt_snapshot_by_slot(&m->snap, lanes[y].slot) : NULL;
321        const char *name = sec ? mt_sec_name(f, sec) : MT_FMT(f, "#%u", lanes[y].slot);
322        tui_color c = mt_prog_color(name, lanes[y].slot, 1.0f);
323        uint16_t yy = (uint16_t)(inner.y + y);
324        const char *lab = MT_FMT(f, "%s/%s", mt_pad(f, name, 8, '>'), mt_pad(f, MT_FMT(f, "%u", lanes[y].node), 2, '<'));
325        tui_buffer_set_stringn(f->buf, inner.x, yy, lab, strlen(lab), LABEL_W, tui_fg(c));
326        for (size_t x = 0; x < w; x++) {
327            tui_cell *cell = tui_buffer_cell(f->buf, (uint16_t)(inner.x + LABEL_W + x), yy);
328            if (!cell) continue;
329            float busy = lanes[y].lane[x];
330            if (busy > 0.0f) {
331                float bm = busy < 1.0f ? busy : 1.0f;
332                size_t lvl = (size_t)mt_sat_u64(ceilf(bm * 8.0f));
333                if (lvl < 1) lvl = 1;
334                if (lvl > 8) lvl = 8;
335                tui_cell_set_str(cell, LEVELS[lvl - 1]);
336                cell->fg = c;
337            } else {
338                tui_cell_set_str(cell, "·");
339                cell->fg = MT_FAINT;
340            }
341        }
342    }
343    if (shown < nl) {
344        const char *more = MT_FMT(f, "+%zu more lanes (taller pane shows them)", nl - shown);
345        tui_buffer_set_stringn(f->buf, inner.x, (uint16_t)(inner.y + shown), more, strlen(more), inner.width, tui_fg(MT_WARN));
346    }
347    const char *axis = mt_pad(f, MT_FMT(f, "-%ums", MT_FLAME_NS / 1000000), (int)w, '<');
348    uint16_t bottom = (uint16_t)(inner.y + inner.height - 1);
349    tui_buffer_set_stringn(f->buf, (uint16_t)(inner.x + LABEL_W), bottom, axis, strlen(axis), w, tui_fg(MT_DIM));
350    tui_buffer_set_string(f->buf, (uint16_t)(inner.x + inner.width - 3), bottom, "now", tui_fg(MT_DIM));
351}
352
353static void draw_log(mt_frame *f, tui_rect area, const mt_machine *m) {
354    tui_block b = mt_panel(f, tui_line_span(f->a, mt_label(MT_FMT(f, " log  %zu lines ", m->nlog))), MT_FAINT);
355    tui_rect inner = tui_block_inner(&b, area);
356    mt_block(f, &b, area);
357    if (m->log_err) {
358        mt_par_line(f, tui_line_span(f->a, mt_fgs(MT_FMT(f, "log: %s", m->log_err), MT_BAD)), inner);
359        return;
360    }
361    mt_lines lines = {0};
362    size_t from = m->nlog > inner.height ? m->nlog - inner.height : 0;
363    for (size_t i = from; i < m->nlog; i++) {
364        const char *l = m->log[i];
365        tui_color c = strstr(l, "ERROR") || strstr(l, " err") ? MT_BAD : strstr(l, "WARN") ? MT_WARN : MT_DIM;
366        mt_lines_push(f, &lines, tui_line_span(f->a, mt_fgs(l, c)));
367    }
368    mt_par(f, mt_text(f, &lines), inner);
369}
370
371void mt_draw_programs(mt_frame *f, tui_rect area, const mt_app *app, mt_instant now) {
372    /* Tables get what their rows need (a third of the height at most each); the selected
373     * program's view, the flame chart and the log share the rest. */
374    size_t n = app->nmachines;
375    uint16_t cap = tui_max16((uint16_t)(area.height / (n + 1)), 4);
376    tui_constraint *cons = tui_arena_alloc(f->a, (n + 1) * sizeof *cons);
377    for (size_t i = 0; i < n; i++) {
378        const mt_machine *m = &app->machines[i];
379        size_t secs = m->has_snap ? (m->snap.nsections ? m->snap.nsections : 1) : 1;
380        cons[i] = TUI_LENGTH(tui_min16((uint16_t)(secs + 3), cap));
381    }
382    cons[n] = TUI_FILL(1);
383    tui_rect *rows = tui_arena_alloc(f->a, (n + 1) * sizeof *rows);
384    tui_vsplit(area, n + 1, cons, rows);
385    /* Same name, different PTX on the two machines: shown, since peer = none programs may differ. */
386    hash_map hashes = {0};
387    for (size_t i = 0; i < n; i++) {
388        const mt_machine *m = &app->machines[i];
389        if (!m->has_snap) continue;
390        for (size_t k = 0; k < m->snap.nsections; k++)
391            hashes_add(f, &hashes, mt_sec_name(f, &m->snap.sections[k]), mt_section_ptx_hash(&m->snap.sections[k]));
392    }
393    for (size_t i = 0; i < n; i++) draw_prog_table(f, rows[i], app, &app->machines[i], i == app->focus, &hashes, now);
394    tui_rect lr[2], fr[2];
395    tui_hsplit(rows[n], 2, (tui_constraint[]){TUI_PERCENTAGE(45), TUI_FILL(1)}, lr);
396    tui_vsplit(lr[1], 2, (tui_constraint[]){TUI_PERCENTAGE(55), TUI_FILL(1)}, fr);
397    draw_view_pane(f, lr[0], app);
398    draw_flame(f, fr[0], mt_app_focused(app), now);
399    draw_log(f, fr[1], mt_app_focused(app));
400}