==> 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}