Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
supervisor_window.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel.
3 Copyright (C) 2026 Aurélien Pierre.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
20#include "caches/image_cache.h"
21#include "caches/mipmap_cache.h"
22#include "common/opencl.h"
24#include "develop/supervisor.h"
25#include "gui/application.h"
26
27#include <gtk/gtk.h>
28#include <json-glib/json-glib.h>
29
30// Cap on how many timeline rows we keep live (older ones are trimmed). The full
31// log lives in the supervisor; this only bounds GTK widget pressure.
32#define TIMELINE_MAX_ROWS 4000
33#define POLL_INTERVAL_MS 300
34#define MEMORY_MAX_ROWS 500 // cap items shown per cache in the memory view
35#define MEMORY_REFRESH_TICKS 4 // refresh the memory view every Nth poll tick when visible
36
37static struct
38{
44 GtkWidget *mem_box; // vbox holding the memory view content
45 GtkWidget *search_entry; // global search entry (in the toolbar)
46 GtkWidget *search_list; // search results page
48 GtkWidget *groupby; // GtkComboBoxText: domain / thread / op
49 GHashTable *decl_map; // hex string -> GtkListBoxRow* (the create/declaration row)
50 GHashTable *group_map; // group key -> _group_t* (incremental grouped buckets)
51 uint64_t last_seq; // highest captured seq already displayed in the timeline
52 uint64_t grouped_last_seq; // highest seq already folded into the grouped view
53 gchar *scroll_target; // hash of a timeline row to scroll into view (deferred)
54 guint timer_id;
55 guint tick; // poll tick counter (for throttling the memory view)
56 int page_timeline, page_grouped, page_memory, page_search; // notebook page indices
57} _g = { 0 };
58
59// One grouped bucket: its detail body (where event rows are appended) and the
60// header label (whose count we keep in sync).
68
69static void _group_free(gpointer p)
70{
71 _group_t *g = (_group_t *)p;
72 if(!g) return;
73 g_free(g->key);
74 g_free(g); // widgets are owned by the list box
75}
76
77static gchar *_hashhex(const uint64_t h)
78{
79 return g_strdup_printf("0x%016" G_GINT64_MODIFIER "x", h);
80}
81
82static const char *_domain_color(const char *d)
83{
84 if(!g_strcmp0(d, "history")) return "#7fbfff";
85 if(!g_strcmp0(d, "node")) return "#9fe0a0";
86 if(!g_strcmp0(d, "cacheline")) return "#ffd080";
87 if(!g_strcmp0(d, "backbuf")) return "#ff9a9a";
88 if(!g_strcmp0(d, "widget")) return "#d0a8ff";
89 if(!g_strcmp0(d, "thumbnail")) return "#bcbcbc";
90 if(!g_strcmp0(d, "mipmap")) return "#b0c4de";
91 if(!g_strcmp0(d, "image")) return "#e8c8a0";
92 if(!g_strcmp0(d, "form")) return "#f0a0c0";
93 return "#dddddd";
94}
95
96static gchar *_pretty_json(const char *compact)
97{
98 JsonParser *p = json_parser_new();
99 gchar *out = NULL;
100 if(json_parser_load_from_data(p, compact, -1, NULL))
101 {
102 JsonGenerator *g = json_generator_new();
103 json_generator_set_root(g, json_parser_get_root(p));
104 json_generator_set_pretty(g, TRUE);
105 out = json_generator_to_data(g, NULL);
106 g_object_unref(g);
107 }
108 g_object_unref(p);
109 return out ? out : g_strdup(compact);
110}
111
112static void _run_search(const char *query);
113
114// Scroll a timeline row (centred) into the visible area of its scrolled window.
116{
117 if(!row || !_g.timeline_scroll) return;
118 GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
119 if(!vadj) return;
120 gint y = 0;
121 if(!gtk_widget_translate_coordinates(row, _g.timeline_list, 0, 0, NULL, &y)) return;
122 GtkAllocation alloc;
123 gtk_widget_get_allocation(row, &alloc);
124 const double page = gtk_adjustment_get_page_size(vadj);
125 const double upper = gtk_adjustment_get_upper(vadj);
126 double target = (double)y + alloc.height / 2.0 - page / 2.0; // centre the row
127 target = CLAMP(target, 0.0, MAX(0.0, upper - page));
128 gtk_adjustment_set_value(vadj, target);
129}
130
131// Deferred so the scroll runs after the page switch / row expansion are laid out.
132static gboolean _scroll_target_idle(gpointer u)
133{
134 if(_g.window && _g.scroll_target)
135 {
136 GtkWidget *row = (GtkWidget *)g_hash_table_lookup(_g.decl_map, _g.scroll_target);
138 }
139 g_clear_pointer(&_g.scroll_target, g_free);
140 return G_SOURCE_REMOVE;
141}
142
143// Clicking a hash jumps to the declaration (create event) of that object.
144static gboolean _on_link(GtkLabel *label, gchar *uri, gpointer user_data)
145{
146 if(!_g.decl_map) return TRUE;
147 GtkWidget *row = (GtkWidget *)g_hash_table_lookup(_g.decl_map, uri);
148 if(row)
149 {
150 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_timeline);
151 GtkWidget *toggle = (GtkWidget *)g_object_get_data(G_OBJECT(row), "toggle");
152 if(toggle) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), TRUE); // expand
153 gtk_list_box_select_row(GTK_LIST_BOX(_g.timeline_list), GTK_LIST_BOX_ROW(row));
154 gtk_widget_grab_focus(row);
155 // Defer the explicit scroll: the page just switched and the row may not be
156 // laid out yet, so translate_coordinates would be stale right now.
157 g_free(_g.scroll_target);
158 _g.scroll_target = g_strdup(uri);
159 g_idle_add(_scroll_target_idle, NULL);
160 }
161 return TRUE; // handled: do not try to open as an URL
162}
163
164static void _on_search_this_hash(GtkMenuItem *item, gpointer u)
165{
166 const char *uri = (const char *)g_object_get_data(G_OBJECT(item), "uri");
167 if(uri && _g.search_entry)
168 {
169 gtk_entry_set_text(GTK_ENTRY(_g.search_entry), uri); // triggers search-changed → _run_search
170 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_search);
171 }
172}
173
174// Right-click over a hash link offers to search for that hash.
175static gboolean _on_label_button(GtkWidget *label, GdkEventButton *e, gpointer u)
176{
177 if(e->type != GDK_BUTTON_PRESS || e->button != GDK_BUTTON_SECONDARY) return FALSE;
178 const char *uri = gtk_label_get_current_uri(GTK_LABEL(label)); // link under the pointer
179 // Inside a list box (timeline/grouped/search) the pointer-tracking that sets
180 // the "current link" is unreliable, so fall back to the label's single own hash.
181 if(!uri || !*uri) uri = (const char *)g_object_get_data(G_OBJECT(label), "own-hash");
182 if(!uri || !*uri) return FALSE;
183
184 GtkWidget *menu = gtk_menu_new();
185 GtkWidget *mi = gtk_menu_item_new_with_label(_("Search for this hash"));
186 g_object_set_data_full(G_OBJECT(mi), "uri", g_strdup(uri), g_free);
187 g_signal_connect(mi, "activate", G_CALLBACK(_on_search_this_hash), NULL);
188 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
189 gtk_widget_show_all(menu);
190 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
191 return TRUE;
192}
193
194// Wire a hash-bearing label: left-click navigates, right-click offers search.
195static void _wire_hash_label(GtkWidget *label)
196{
197 gtk_label_set_track_visited_links(GTK_LABEL(label), FALSE);
198 g_signal_connect(label, "activate-link", G_CALLBACK(_on_link), NULL);
199 gtk_widget_add_events(label, GDK_BUTTON_PRESS_MASK);
200 g_signal_connect(label, "button-press-event", G_CALLBACK(_on_label_button), NULL);
201}
202
203static GtkWidget *_event_body_from(const char *json, GArray *links);
204
205static void _on_toggle(GtkToggleButton *t, gpointer u)
206{
207 const gboolean a = gtk_toggle_button_get_active(t);
208 GtkWidget *rev = (GtkWidget *)g_object_get_data(G_OBJECT(t), "revealer");
209 GtkWidget *arrow = (GtkWidget *)g_object_get_data(G_OBJECT(t), "arrow");
210
211 // Lazily build an event's detail body the first time it is expanded, so the
212 // JSON is not parsed for rows the user never opens (important under streaming).
213 if(a && rev && !gtk_bin_get_child(GTK_BIN(rev)))
214 {
215 const char *json = (const char *)g_object_get_data(G_OBJECT(t), "lazy_json");
216 if(json)
217 {
218 GtkWidget *body = _event_body_from(json, (GArray *)g_object_get_data(G_OBJECT(t), "lazy_links"));
219 gtk_container_add(GTK_CONTAINER(rev), body);
220 gtk_widget_show_all(body);
221 }
222 }
223 if(rev) gtk_revealer_set_reveal_child(GTK_REVEALER(rev), a);
224 if(arrow)
225 gtk_image_set_from_icon_name(GTK_IMAGE(arrow), a ? "pan-down-symbolic" : "pan-end-symbolic",
226 GTK_ICON_SIZE_BUTTON);
227}
228
229// A collapsible block: [arrow toggle][header label] + a revealer holding `body`.
230// The header is a separate label (NOT the toggle), so its hash links stay
231// clickable. Returns a vbox; the toggle button is stored as data "toggle".
232static GtkWidget *_collapsible(const char *header_markup, GtkWidget *body, const gboolean expanded)
233{
234 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
235 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
236
237 GtkWidget *toggle = gtk_toggle_button_new();
238 gtk_button_set_relief(GTK_BUTTON(toggle), GTK_RELIEF_NONE);
239 gtk_widget_set_focus_on_click(toggle, FALSE);
240 GtkWidget *arrow = gtk_image_new_from_icon_name(expanded ? "pan-down-symbolic" : "pan-end-symbolic",
241 GTK_ICON_SIZE_BUTTON);
242 gtk_container_add(GTK_CONTAINER(toggle), arrow);
243
244 GtkWidget *header = gtk_label_new(NULL);
245 gtk_label_set_markup(GTK_LABEL(header), header_markup);
246 gtk_label_set_xalign(GTK_LABEL(header), 0.0);
247 _wire_hash_label(header);
248
249 GtkWidget *revealer = gtk_revealer_new();
250 gtk_revealer_set_reveal_child(GTK_REVEALER(revealer), expanded);
251 if(body) gtk_container_add(GTK_CONTAINER(revealer), body);
252
253 g_object_set_data(G_OBJECT(toggle), "revealer", revealer);
254 g_object_set_data(G_OBJECT(toggle), "arrow", arrow);
255 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), expanded);
256 g_signal_connect(toggle, "toggled", G_CALLBACK(_on_toggle), NULL);
257
258 gtk_box_pack_start(GTK_BOX(hbox), toggle, FALSE, FALSE, 0);
259 gtk_box_pack_start(GTK_BOX(hbox), header, TRUE, TRUE, 0);
260 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
261 gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0);
262
263 g_object_set_data(G_OBJECT(vbox), "toggle", toggle);
264 g_object_set_data(G_OBJECT(vbox), "header", header); // so callers can update the label
265 return vbox;
266}
267
268static gchar *_header_markup(const dt_sv_logged_event_t *ev)
269{
270 gchar *hx = _hashhex(ev->hash);
271 gchar *e_op = g_markup_escape_text(ev->op, -1);
272 gchar *e_dom = g_markup_escape_text(ev->domain, -1);
273 gchar *e_thr = g_markup_escape_text(ev->thread, -1);
274 gchar *e_mn_raw = g_markup_escape_text(ev->mnemonic[0] ? ev->mnemonic : "-", -1);
275 // Raster masks are side-band cachelines, not module outputs: dim the domain
276 // badge and tag the title so they read as secondary entries among plain
277 // image cachelines of the same module.
278 const gboolean is_raster_mask
279 = !g_strcmp0(ev->domain, "cacheline") && !g_strcmp0(ev->cache_name, "raster mask");
280 gchar *e_mn = is_raster_mask ? g_strdup_printf("%s - raster mask", e_mn_raw) : g_strdup(e_mn_raw);
281 g_free(e_mn_raw);
282 gchar *out = g_strdup_printf(
283 "<tt>%9.3f</tt> <b>%-7s</b> <span foreground=\"%s\">%-10s</span> <b>%-16s</b> "
284 "<a href=\"%s\"><tt>%s</tt></a> <span size=\"small\"><i>%s</i></span>",
285 ev->ts, e_op, is_raster_mask ? "#b78d45" : _domain_color(ev->domain), e_dom, e_mn, hx, hx, e_thr);
286 g_free(hx);
287 g_free(e_op);
288 g_free(e_dom);
289 g_free(e_thr);
290 g_free(e_mn);
291 return out;
292}
293
294// Build the detail body for an event: clickable linked hashes + full record.
295static GtkWidget *_event_body_from(const char *json, GArray *links)
296{
297 GtkWidget *detail = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
298 gtk_widget_set_margin_start(detail, 28);
299 gtk_widget_set_margin_bottom(detail, 6);
300
301 if(links && links->len)
302 {
303 GString *ls = g_string_new(NULL);
304 for(guint i = 0; i < links->len; i++)
305 {
306 const dt_sv_link_t *lk = &g_array_index(links, dt_sv_link_t, i);
307 gchar *lhx = _hashhex(lk->hash);
308 g_string_append_printf(ls, "%s%s → <a href=\"%s\"><tt>%s</tt></a>", i ? " " : "", lk->label,
309 lhx, lhx);
310 g_free(lhx);
311 }
312 GtkWidget *lw = gtk_label_new(NULL);
313 gtk_label_set_markup(GTK_LABEL(lw), ls->str);
314 gtk_label_set_xalign(GTK_LABEL(lw), 0.0);
316 gtk_box_pack_start(GTK_BOX(detail), lw, FALSE, FALSE, 0);
317 g_string_free(ls, TRUE);
318 }
319
320 gchar *pretty = _pretty_json(json);
321 gchar *escaped = g_markup_escape_text(pretty, -1);
322 gchar *mono = g_strdup_printf("<tt>%s</tt>", escaped);
323 GtkWidget *body = gtk_label_new(NULL);
324 gtk_label_set_markup(GTK_LABEL(body), mono);
325 gtk_label_set_xalign(GTK_LABEL(body), 0.0);
326 gtk_label_set_selectable(GTK_LABEL(body), TRUE);
327 gtk_box_pack_start(GTK_BOX(detail), body, FALSE, FALSE, 0);
328 g_free(pretty);
329 g_free(escaped);
330 g_free(mono);
331 return detail;
332}
333
334static void _links_free(gpointer p)
335{
336 if(p) g_array_free((GArray *)p, TRUE);
337}
338
339// Collapsed event row; the detail body is built lazily on first expand.
341{
342 gchar *hm = _header_markup(ev);
343 GtkWidget *w = _collapsible(hm, NULL, FALSE);
344 g_free(hm);
345
346 GtkWidget *toggle = (GtkWidget *)g_object_get_data(G_OBJECT(w), "toggle");
347 g_object_set_data_full(G_OBJECT(toggle), "lazy_json", g_strdup(ev->json), g_free);
348 if(ev->links && ev->links->len)
349 {
350 GArray *copy = g_array_new(FALSE, FALSE, sizeof(dt_sv_link_t));
351 g_array_append_vals(copy, ev->links->data, ev->links->len);
352 g_object_set_data_full(G_OBJECT(toggle), "lazy_links", copy, _links_free);
353 }
354
355 // The header carries exactly one hash (the event's own): stash it so right-click
356 // search works even when the list box defeats the label's link tracking.
357 GtkWidget *header = (GtkWidget *)g_object_get_data(G_OBJECT(w), "header");
358 if(header) g_object_set_data_full(G_OBJECT(header), "own-hash", _hashhex(ev->hash), g_free);
359 return w;
360}
361
362// Append one event to the timeline list and register it as a declaration when
363// it is a create event.
365{
366 GtkWidget *w = _event_widget(ev);
367 gtk_widget_show_all(w);
368 gtk_container_add(GTK_CONTAINER(_g.timeline_list), w);
369 GtkWidget *row = gtk_widget_get_parent(w); // the auto-created GtkListBoxRow
370 g_object_set_data(G_OBJECT(row), "toggle", g_object_get_data(G_OBJECT(w), "toggle"));
371 if(!g_strcmp0(ev->op, "create"))
372 {
373 gchar *hx = _hashhex(ev->hash);
374 g_object_set_data_full(G_OBJECT(row), "svhash", g_strdup(hx), g_free);
375 g_hash_table_replace(_g.decl_map, hx, row); // map owns hx
376 }
377}
378
379static void _timeline_trim(void)
380{
381 GList *children = gtk_container_get_children(GTK_CONTAINER(_g.timeline_list));
382 int excess = (int)g_list_length(children) - TIMELINE_MAX_ROWS;
383 for(GList *l = children; l && excess > 0; l = l->next, excess--)
384 {
385 GtkWidget *row = GTK_WIDGET(l->data);
386 const char *hx = (const char *)g_object_get_data(G_OBJECT(row), "svhash");
387 if(hx && g_hash_table_lookup(_g.decl_map, hx) == row) g_hash_table_remove(_g.decl_map, hx);
388 gtk_widget_destroy(row);
389 }
390 g_list_free(children);
391}
392
393static void _clear_list(GtkWidget *list)
394{
395 GList *children = gtk_container_get_children(GTK_CONTAINER(list));
396 for(GList *l = children; l; l = l->next) gtk_widget_destroy(GTK_WIDGET(l->data));
397 g_list_free(children);
398}
399
400static void _update_count(void)
401{
402 gchar *s = g_strdup_printf(_("%u events"), dt_supervisor_events_count());
403 gtk_label_set_text(GTK_LABEL(_g.count_label), s);
404 g_free(s);
405}
406
407static const char *_group_key(const dt_sv_logged_event_t *ev, const char *by)
408{
409 if(!g_strcmp0(by, "thread")) return ev->thread;
410 if(!g_strcmp0(by, "op")) return ev->op;
411 return ev->domain;
412}
413
415{
416 gchar *e = g_markup_escape_text(g->key && *g->key ? g->key : "(none)", -1);
417 gchar *hm = g_strdup_printf("<b>%s</b> <span size=\"small\">(%d)</span>", e, g->count);
418 gtk_label_set_markup(GTK_LABEL(g->header), hm);
419 g_free(e);
420 g_free(hm);
421}
422
423// Append one event into its grouped bucket (creating the bucket if needed),
424// preserving existing groups/rows and their expansion state.
425static void _grouped_append(const dt_sv_logged_event_t *ev, const char *by)
426{
427 const char *k = _group_key(ev, by);
428 _group_t *g = (_group_t *)g_hash_table_lookup(_g.group_map, k);
429 if(!g)
430 {
431 g = g_new0(_group_t, 1);
432 g->key = g_strdup(k);
433 g->body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
434 gtk_widget_set_margin_start(g->body, 12);
435 GtkWidget *coll = _collapsible("", g->body, FALSE);
436 g->header = (GtkWidget *)g_object_get_data(G_OBJECT(coll), "header");
437 gtk_container_add(GTK_CONTAINER(_g.grouped_list), coll);
438 gtk_widget_show_all(coll);
439 g_hash_table_insert(_g.group_map, g_strdup(k), g);
440 }
442 gtk_box_pack_start(GTK_BOX(g->body), row, FALSE, FALSE, 0);
443 gtk_widget_show_all(row);
444 g->count++;
446}
447
448// Fold all events newer than the grouped cursor into the buckets (live append).
449static void _grouped_catch_up(void)
450{
451 uint64_t newlast = _g.grouped_last_seq;
452 GPtrArray *evs = dt_supervisor_events_snapshot_since(_g.grouped_last_seq, &newlast);
453 if(evs->len)
454 {
455 gchar *by = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(_g.groupby));
456 for(guint i = 0; i < evs->len; i++) _grouped_append(g_ptr_array_index(evs, i), by ? by : "domain");
457 g_free(by);
458 _g.grouped_last_seq = newlast;
459 }
461}
462
463// Full rebuild (group-by change / Clear): wipe buckets and re-fold everything.
464static void _rebuild_grouped_now(void)
465{
466 _clear_list(_g.grouped_list);
467 g_hash_table_remove_all(_g.group_map);
468 _g.grouped_last_seq = 0;
470}
471
472// ---- Memory view -------------------------------------------------------------
473
474static void _add_usage_bar(GtkWidget *box, const char *title, const size_t cur, const size_t max)
475{
476 GtkWidget *hdr = gtk_label_new(NULL);
477 gchar *t = g_markup_printf_escaped("<b>%s</b>", title);
478 gtk_label_set_markup(GTK_LABEL(hdr), t);
479 gtk_label_set_xalign(GTK_LABEL(hdr), 0.0);
480 gtk_widget_set_margin_top(hdr, 8);
481 g_free(t);
482 gtk_box_pack_start(GTK_BOX(box), hdr, FALSE, FALSE, 0);
483
484 const double frac = max ? CLAMP((double)cur / (double)max, 0.0, 1.0) : 0.0;
485 GtkWidget *bar = gtk_progress_bar_new();
486 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), frac);
487 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(bar), TRUE);
488 gchar *txt = g_strdup_printf("%.1f / %.1f MiB (%.0f%%)", cur / 1048576.0, max / 1048576.0, frac * 100.0);
489 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(bar), txt);
490 g_free(txt);
491 gtk_box_pack_start(GTK_BOX(box), bar, FALSE, FALSE, 0);
492}
493
494// A clickable memory item (markup carries the navigation link). When `trailing`
495// is non-NULL it is packed at the right edge as a separate label, so the trailing
496// column (e.g. vRAM) lines up vertically across rows.
497static void _add_mem_item(GtkWidget *box, const char *markup, const char *trailing)
498{
499 GtkWidget *lbl = gtk_label_new(NULL);
500 gtk_label_set_markup(GTK_LABEL(lbl), markup);
501 gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
502 _wire_hash_label(lbl);
503
504 if(trailing && *trailing)
505 {
506 GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
507 gtk_widget_set_margin_start(row, 12);
508 gtk_box_pack_start(GTK_BOX(row), lbl, TRUE, TRUE, 0); // expands → pushes trailing to the right
509 GtkWidget *tl = gtk_label_new(NULL);
510 gtk_label_set_markup(GTK_LABEL(tl), trailing);
511 gtk_label_set_xalign(GTK_LABEL(tl), 1.0);
512 gtk_box_pack_end(GTK_BOX(row), tl, FALSE, FALSE, 0);
513 gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0);
514 }
515 else
516 {
517 gtk_widget_set_margin_start(lbl, 12);
518 gtk_box_pack_start(GTK_BOX(box), lbl, FALSE, FALSE, 0);
519 }
520}
521
522static gint _cmp_pixel_size(gconstpointer a, gconstpointer b)
523{
524 const dt_pixel_cache_stats_entry_t *x = a, *y = b;
525 return (y->size > x->size) - (y->size < x->size); // descending
526}
527
528static gint _cmp_mip_size(gconstpointer a, gconstpointer b)
529{
530 const dt_mipmap_cache_stats_entry_t *x = a, *y = b;
531 return (y->size > x->size) - (y->size < x->size); // descending
532}
533
534static gint _cmp_image_imgid(gconstpointer a, gconstpointer b)
535{
536 const dt_image_cache_stats_entry_t *x = a, *y = b;
537 return x->imgid - y->imgid; // ascending (entries are all the same size)
538}
539
540static void _rebuild_memory(void)
541{
542 if(!_g.mem_box) return;
543 _clear_list(_g.mem_box);
544
545 // Pipeline cache
546 size_t cur = 0, max = 0;
549
550 const gboolean gpu = dt_opencl_is_enabled();
551 size_t vram_used = 0;
552 for(guint i = 0; i < pe->len; i++) vram_used += g_array_index(pe, dt_pixel_cache_stats_entry_t, i).cl_bytes;
553
554 gchar *ptitle = g_strdup_printf(gpu ? _("Pipeline cache (RAM) — %u items") : _("Pipeline cache — %u items"),
555 pe->len);
556 _add_usage_bar(_g.mem_box, ptitle, cur, max);
557 g_free(ptitle);
558 if(gpu)
559 _add_usage_bar(_g.mem_box, _("Pipeline cache (vRAM)"), vram_used,
561
562 g_array_sort(pe, _cmp_pixel_size);
563 for(guint i = 0; i < pe->len && i < MEMORY_MAX_ROWS; i++)
564 {
565 const dt_pixel_cache_stats_entry_t *e = &g_array_index(pe, dt_pixel_cache_stats_entry_t, i);
566 gchar *hx = _hashhex(e->hash);
567 gchar *name = g_markup_escape_text(e->name[0] ? e->name : "-", -1);
568 // fixed-width monospace trailing column so the vRAM figures line up vertically
569 gchar *vram = (gpu && e->cl_count > 0)
570 ? g_strdup_printf("<tt>+%8.2f MiB vRAM (%2d buf)</tt>", e->cl_bytes / 1048576.0,
571 e->cl_count)
572 : NULL;
573 gchar *m = g_strdup_printf("<a href=\"%s\"><tt>%s</tt></a> %.2f MiB refs=%d hits=%d <i>%s</i>", hx, hx,
574 e->size / 1048576.0, e->refcount, e->hits, name);
575 _add_mem_item(_g.mem_box, m, vram);
576 g_free(hx);
577 g_free(name);
578 g_free(vram);
579 g_free(m);
580 }
581 g_array_free(pe, TRUE);
582
583 // Mipmap cache
586 gchar *mtitle = g_strdup_printf(_("Mipmap cache — %u items"), me->len);
587 _add_usage_bar(_g.mem_box, mtitle, cur, max);
588 g_free(mtitle);
589 g_array_sort(me, _cmp_mip_size);
590 for(guint i = 0; i < me->len && i < MEMORY_MAX_ROWS; i++)
591 {
592 const dt_mipmap_cache_stats_entry_t *e = &g_array_index(me, dt_mipmap_cache_stats_entry_t, i);
593 gchar *hx = _hashhex(dt_supervisor_mipmap_key(e->imgid, e->mip));
594 gchar *m = g_strdup_printf("<a href=\"%s\">image #%d · mip %d</a> %.2f MiB", hx, e->imgid, e->mip,
595 e->size / 1048576.0);
596 _add_mem_item(_g.mem_box, m, NULL);
597 g_free(hx);
598 g_free(m);
599 }
600 g_array_free(me, TRUE);
601
602 // Image cache
605 gchar *ititle = g_strdup_printf(_("Image cache — %u items"), ie->len);
606 _add_usage_bar(_g.mem_box, ititle, cur, max);
607 g_free(ititle);
608 g_array_sort(ie, _cmp_image_imgid);
609 for(guint i = 0; i < ie->len && i < MEMORY_MAX_ROWS; i++)
610 {
611 const dt_image_cache_stats_entry_t *e = &g_array_index(ie, dt_image_cache_stats_entry_t, i);
612 gchar *hx = _hashhex(dt_supervisor_image_key(e->imgid));
613 gchar *fn = g_markup_escape_text(e->filename[0] ? e->filename : "-", -1);
614 gchar *m = g_strdup_printf("<a href=\"%s\">image #%d</a> <i>%s</i>", hx, e->imgid, fn);
615 _add_mem_item(_g.mem_box, m, NULL);
616 g_free(hx);
617 g_free(fn);
618 g_free(m);
619 }
620 g_array_free(ie, TRUE);
621
622 gtk_widget_show_all(_g.mem_box);
623}
624
625// ---- Search ------------------------------------------------------------------
626
627// TRUE if the hash's hex (lowercased, with 0x) contains the needle.
628static gboolean _hex_contains(const uint64_t h, const char *needle)
629{
630 gchar *hx = _hashhex(h);
631 gchar *lh = g_ascii_strdown(hx, -1);
632 const gboolean m = strstr(lh, needle) != NULL;
633 g_free(hx);
634 g_free(lh);
635 return m;
636}
637
638// An event matches when:
639// - its own hash or any linked hash contains `hash_needle` (hex, 0x stripped), or
640// - its full record (JSON: module/op/domain/widget/filename/parameters/…)
641// contains `text_needle` as a case-insensitive substring.
642static gboolean _event_matches(const dt_sv_logged_event_t *ev, const char *hash_needle,
643 const char *text_needle)
644{
645 if(*hash_needle)
646 {
647 if(_hex_contains(ev->hash, hash_needle)) return TRUE;
648 if(ev->links)
649 for(guint i = 0; i < ev->links->len; i++)
650 if(_hex_contains(g_array_index(ev->links, dt_sv_link_t, i).hash, hash_needle)) return TRUE;
651 }
652 if(*text_needle && ev->json)
653 {
654 gchar *jl = g_ascii_strdown(ev->json, -1);
655 const gboolean m = strstr(jl, text_needle) != NULL;
656 g_free(jl);
657 if(m) return TRUE;
658 }
659 return FALSE;
660}
661
662static void _run_search(const char *query)
663{
664 if(!_g.search_list) return;
665 _clear_list(_g.search_list);
666 if(!query || !*query)
667 {
668 gtk_widget_show_all(_g.search_list);
669 return;
670 }
671
672 // text_needle matches any field of the record; hash_needle (0x stripped) matches hashes.
673 gchar *text_needle = g_ascii_strdown(query, -1);
674 const char *hash_needle = g_str_has_prefix(text_needle, "0x") ? text_needle + 2 : text_needle;
675
676 GPtrArray *evs = dt_supervisor_events_snapshot();
677 for(guint i = 0; i < evs->len; i++)
678 {
679 const dt_sv_logged_event_t *ev = g_ptr_array_index(evs, i);
680 if(_event_matches(ev, hash_needle, text_needle))
681 gtk_container_add(GTK_CONTAINER(_g.search_list), _event_widget(ev));
682 }
684 g_free(text_needle);
685 gtk_widget_show_all(_g.search_list);
686}
687
688static void _on_search_changed(GtkSearchEntry *e, gpointer u)
689{
690 const char *txt = gtk_entry_get_text(GTK_ENTRY(e));
691 // Typing a query jumps to the Search page automatically.
692 if(txt && *txt) gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_search);
693 _run_search(txt);
694}
695
696static gboolean _scroll_bottom_idle(gpointer u)
697{
698 if(!_g.timeline_scroll) return G_SOURCE_REMOVE;
699 GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
700 if(adj) gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj));
701 return G_SOURCE_REMOVE;
702}
703
704static gboolean _poll(gpointer u)
705{
706 if(!_g.window) return G_SOURCE_REMOVE;
707
708 _g.tick++;
709 const int page = gtk_notebook_get_current_page(GTK_NOTEBOOK(_g.notebook));
710 // The grouped view appends new events live (incrementally, preserving folds)
711 // while it is the visible page.
712 if(page == _g.page_grouped) _grouped_catch_up();
713 // The memory view reflects live cache state; refresh it on a throttled cadence.
714 // The search view is intentionally NOT auto-refreshed here: rebuilding it would
715 // collapse any rows the user expanded. It updates on input / Refresh instead.
716 if((_g.tick % MEMORY_REFRESH_TICKS) == 0 && page == _g.page_memory) _rebuild_memory();
717
718 uint64_t newlast = _g.last_seq;
719 GPtrArray *evs = dt_supervisor_events_snapshot_since(_g.last_seq, &newlast);
720 if(evs->len)
721 {
722 GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
723 const gboolean at_bottom = !adj
724 || gtk_adjustment_get_value(adj)
725 >= gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj) - 4.0;
726
727 for(guint i = 0; i < evs->len; i++) _timeline_append(g_ptr_array_index(evs, i));
728 _g.last_seq = newlast;
731 if(at_bottom) g_idle_add(_scroll_bottom_idle, NULL);
732 }
734 return G_SOURCE_CONTINUE;
735}
736
737static void _full_reload(void)
738{
739 _clear_list(_g.timeline_list);
740 g_hash_table_remove_all(_g.decl_map);
741 _g.last_seq = 0;
742 _poll(NULL); // appends everything since the start
745 _run_search(gtk_entry_get_text(GTK_ENTRY(_g.search_entry)));
746}
747
748static void _on_refresh(GtkButton *b, gpointer u) { _full_reload(); }
749
750static void _on_clear(GtkButton *b, gpointer u)
751{
753 _full_reload();
754}
755
756static void _on_groupby_changed(GtkComboBox *c, gpointer u)
757{
758 _rebuild_grouped_now(); // regroup everything under the new key
759}
760
761static void _on_record_toggled(GtkToggleButton *t, gpointer u)
762{
763 dt_supervisor_set_recording(gtk_toggle_button_get_active(t));
764}
765
766static void _on_page_changed(GtkNotebook *nb, GtkWidget *page, guint page_num, gpointer u)
767{
768 if((int)page_num == _g.page_grouped)
769 _grouped_catch_up(); // fold in whatever arrived while the page was hidden
770 else if((int)page_num == _g.page_memory)
772 else if((int)page_num == _g.page_search)
773 _run_search(gtk_entry_get_text(GTK_ENTRY(_g.search_entry)));
774}
775
776static void _on_destroy(GtkWidget *w, gpointer u)
777{
778 if(_g.timer_id) g_source_remove(_g.timer_id);
779 dt_supervisor_set_recording(FALSE); // stop capturing once the viewer is gone
780 if(_g.decl_map) g_hash_table_destroy(_g.decl_map);
781 if(_g.group_map) g_hash_table_destroy(_g.group_map);
782 g_free(_g.scroll_target);
783 memset(&_g, 0, sizeof(_g));
784}
785
787{
788 if(_g.window)
789 {
790 gtk_window_present(GTK_WINDOW(_g.window));
791 return;
792 }
793
794 _g.decl_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
795 _g.group_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, _group_free);
796
797 _g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
798 gtk_window_set_title(GTK_WINDOW(_g.window), _("Event supervisor"));
799 gtk_window_set_default_size(GTK_WINDOW(_g.window), 1000, 640);
800 gtk_window_set_transient_for(GTK_WINDOW(_g.window), GTK_WINDOW(dt_gui_main_window()));
801 g_signal_connect(_g.window, "destroy", G_CALLBACK(_on_destroy), NULL);
802
803 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
804 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
805 gtk_container_add(GTK_CONTAINER(_g.window), vbox);
806
807 // toolbar
808 GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
809 gtk_box_pack_start(GTK_BOX(vbox), bar, FALSE, FALSE, 0);
810
811 GtkWidget *record = gtk_check_button_new_with_label(_("Record"));
812 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record), TRUE);
813 dt_supervisor_set_recording(TRUE); // start capturing on open
814 g_signal_connect(record, "toggled", G_CALLBACK(_on_record_toggled), NULL);
815 gtk_box_pack_start(GTK_BOX(bar), record, FALSE, FALSE, 0);
816
817 GtkWidget *refresh = gtk_button_new_with_label(_("Refresh"));
818 g_signal_connect(refresh, "clicked", G_CALLBACK(_on_refresh), NULL);
819 gtk_box_pack_start(GTK_BOX(bar), refresh, FALSE, FALSE, 0);
820
821 GtkWidget *clear = gtk_button_new_with_label(_("Clear"));
822 g_signal_connect(clear, "clicked", G_CALLBACK(_on_clear), NULL);
823 gtk_box_pack_start(GTK_BOX(bar), clear, FALSE, FALSE, 0);
824
825 gtk_box_pack_start(GTK_BOX(bar), gtk_label_new(_("Group by")), FALSE, FALSE, 0);
826 _g.groupby = gtk_combo_box_text_new();
827 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "domain");
828 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "thread");
829 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "op");
830 gtk_combo_box_set_active(GTK_COMBO_BOX(_g.groupby), 0);
831 g_signal_connect(_g.groupby, "changed", G_CALLBACK(_on_groupby_changed), NULL);
832 gtk_box_pack_start(GTK_BOX(bar), _g.groupby, FALSE, FALSE, 0);
833
834 // global search entry: query all events by hash (own or linked)
835 _g.search_entry = gtk_search_entry_new();
836 gtk_entry_set_placeholder_text(GTK_ENTRY(_g.search_entry), _("search hash or text…"));
837 gtk_widget_set_size_request(_g.search_entry, 220, -1);
838 g_signal_connect(_g.search_entry, "search-changed", G_CALLBACK(_on_search_changed), NULL);
839 gtk_box_pack_end(GTK_BOX(bar), _g.search_entry, FALSE, FALSE, 0);
840
841 _g.count_label = gtk_label_new("");
842 gtk_box_pack_end(GTK_BOX(bar), _g.count_label, FALSE, FALSE, 0);
843
844 // notebook with the four pages, tabs spread across the full width
845 _g.notebook = gtk_notebook_new();
846 g_signal_connect(_g.notebook, "switch-page", G_CALLBACK(_on_page_changed), NULL);
847 gtk_box_pack_start(GTK_BOX(vbox), _g.notebook, TRUE, TRUE, 0);
848
849 _g.timeline_list = gtk_list_box_new();
850 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.timeline_list), GTK_SELECTION_SINGLE);
851 _g.timeline_scroll = gtk_scrolled_window_new(NULL, NULL);
852 gtk_container_add(GTK_CONTAINER(_g.timeline_scroll), _g.timeline_list);
853 _g.page_timeline = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), _g.timeline_scroll,
854 gtk_label_new(_("Timeline")));
855
856 _g.grouped_list = gtk_list_box_new();
857 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.grouped_list), GTK_SELECTION_NONE);
858 GtkWidget *sw2 = gtk_scrolled_window_new(NULL, NULL);
859 gtk_container_add(GTK_CONTAINER(sw2), _g.grouped_list);
860 _g.page_grouped = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw2, gtk_label_new(_("Grouped")));
861
862 _g.mem_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
863 gtk_container_set_border_width(GTK_CONTAINER(_g.mem_box), 6);
864 GtkWidget *sw3 = gtk_scrolled_window_new(NULL, NULL);
865 gtk_container_add(GTK_CONTAINER(sw3), _g.mem_box);
866 _g.page_memory = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw3, gtk_label_new(_("Memory")));
867
868 _g.search_list = gtk_list_box_new();
869 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.search_list), GTK_SELECTION_NONE);
870 GtkWidget *sw4 = gtk_scrolled_window_new(NULL, NULL);
871 gtk_container_add(GTK_CONTAINER(sw4), _g.search_list);
872 _g.page_search = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw4, gtk_label_new(_("Search")));
873
874 // make each tab expand to fill the notebook width
875 GList *pages = gtk_container_get_children(GTK_CONTAINER(_g.notebook));
876 for(GList *l = pages; l; l = l->next)
877 gtk_container_child_set(GTK_CONTAINER(_g.notebook), GTK_WIDGET(l->data), "tab-expand", TRUE,
878 "tab-fill", TRUE, NULL);
879 g_list_free(pages);
880
881 gtk_widget_show_all(_g.window);
882 _full_reload();
883 _g.timer_id = g_timeout_add(POLL_INTERVAL_MS, _poll, NULL);
884}
GtkWidget * dt_gui_main_window(void)
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
#define m
Definition basecurve.c:283
static const float x
const int t
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
const float max
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
GHashTable * pages
row label -> path of the illustration, relative to the destination
void dt_image_cache_get_usage(size_t *current, size_t *max)
GArray * dt_image_cache_get_entries_stats(void)
float *const restrict const size_t k
void dt_mipmap_cache_get_usage(size_t *current, size_t *max)
GArray * dt_mipmap_cache_get_entries_stats(void)
int dt_opencl_is_enabled(void)
Definition opencl.c:3262
const char * name
Definition pdf.h:90
GArray * dt_dev_pixelpipe_cache_get_entries_stats(void)
size_t dt_dev_pixelpipe_cache_get_vram_total(void)
void dt_dev_pixelpipe_cache_get_usage(size_t *current, size_t *max)
Pixelpipe cache for storing intermediate results in the pixelpipe.
void copy(TYPE *dest, TYPE *source, size_t num_el)
Copy a flat buffer used to initialize test matrices.
#define lw
Definition retouch.c:1078
unsigned __int64 uint64_t
Definition strptime.c:75
GtkWidget * body
GtkWidget * header
Definition image_cache.h:77
char filename[128]
Definition image_cache.h:80
int32_t imgid
Definition image_cache.h:78
int mip
size_t size
int32_t imgid
int refcount
char name[64]
int hits
size_t size
size_t cl_bytes
uint64_t hash
int cl_count
void dt_supervisor_events_free(GPtrArray *events)
void dt_supervisor_set_recording(const gboolean on)
Definition supervisor.c:305
void dt_supervisor_events_clear(void)
Definition supervisor.c:310
GPtrArray * dt_supervisor_events_snapshot(void)
GPtrArray * dt_supervisor_events_snapshot_since(const uint64_t after_seq, uint64_t *out_last_seq)
uint64_t dt_supervisor_image_key(const int32_t imgid)
Definition supervisor.c:195
guint dt_supervisor_events_count(void)
uint64_t dt_supervisor_mipmap_key(const int32_t imgid, const int mip)
Definition supervisor.c:185
GtkWidget * groupby
GtkWidget * mem_box
int page_memory
static GtkWidget * _event_widget(const dt_sv_logged_event_t *ev)
GtkWidget * timeline_list
guint timer_id
uint64_t last_seq
static struct @31 _g
static gboolean _poll(gpointer u)
static void _wire_hash_label(GtkWidget *label)
static void _group_set_header(_group_t *g)
GtkWidget * timeline_scroll
GtkWidget * window
guint tick
static void _run_search(const char *query)
GtkWidget * search_list
gchar * scroll_target
GtkWidget * count_label
static void _on_groupby_changed(GtkComboBox *c, gpointer u)
GtkWidget * grouped_list
uint64_t grouped_last_seq
#define TIMELINE_MAX_ROWS
static void _clear_list(GtkWidget *list)
static void _timeline_append(const dt_sv_logged_event_t *ev)
GtkWidget * notebook
#define MEMORY_REFRESH_TICKS
static gint _cmp_image_imgid(gconstpointer a, gconstpointer b)
static void _on_search_changed(GtkSearchEntry *e, gpointer u)
static void _links_free(gpointer p)
static void _group_free(gpointer p)
static const char * _domain_color(const char *d)
static void _on_toggle(GtkToggleButton *t, gpointer u)
void dt_gui_supervisor_window_show(void)
static gboolean _on_label_button(GtkWidget *label, GdkEventButton *e, gpointer u)
GHashTable * decl_map
static gboolean _on_link(GtkLabel *label, gchar *uri, gpointer user_data)
static void _grouped_append(const dt_sv_logged_event_t *ev, const char *by)
static void _update_count(void)
static void _add_usage_bar(GtkWidget *box, const char *title, const size_t cur, const size_t max)
static gint _cmp_mip_size(gconstpointer a, gconstpointer b)
int page_timeline
static gboolean _event_matches(const dt_sv_logged_event_t *ev, const char *hash_needle, const char *text_needle)
GtkWidget * search_entry
static gchar * _hashhex(const uint64_t h)
int page_search
static void _on_search_this_hash(GtkMenuItem *item, gpointer u)
static void _rebuild_grouped_now(void)
static void _on_destroy(GtkWidget *w, gpointer u)
static void _on_record_toggled(GtkToggleButton *t, gpointer u)
static void _on_page_changed(GtkNotebook *nb, GtkWidget *page, guint page_num, gpointer u)
static GtkWidget * _collapsible(const char *header_markup, GtkWidget *body, const gboolean expanded)
static gint _cmp_pixel_size(gconstpointer a, gconstpointer b)
static void _rebuild_memory(void)
static gboolean _hex_contains(const uint64_t h, const char *needle)
static const char * _group_key(const dt_sv_logged_event_t *ev, const char *by)
static void _full_reload(void)
#define MEMORY_MAX_ROWS
static void _scroll_row_into_view(GtkWidget *row)
static gchar * _header_markup(const dt_sv_logged_event_t *ev)
static GtkWidget * _event_body_from(const char *json, GArray *links)
static gchar * _pretty_json(const char *compact)
static void _on_refresh(GtkButton *b, gpointer u)
int page_grouped
static gboolean _scroll_target_idle(gpointer u)
#define POLL_INTERVAL_MS
static void _timeline_trim(void)
static void _add_mem_item(GtkWidget *box, const char *markup, const char *trailing)
static void _on_clear(GtkButton *b, gpointer u)
static gboolean _scroll_bottom_idle(gpointer u)
GHashTable * group_map
static void _grouped_catch_up(void)
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MAX(a, b)
Definition thinplate.c:29